diff --git a/src/__tests__/background-reasoning-inventory.test.ts b/src/__tests__/background-reasoning-inventory.test.ts index cafb8bf61..ccb773f6c 100644 --- a/src/__tests__/background-reasoning-inventory.test.ts +++ b/src/__tests__/background-reasoning-inventory.test.ts @@ -55,6 +55,7 @@ const ADMITTED: Record = { const NEVER = [ "lib/insights/status-batch.ts", "lib/insights/status-card-generation.ts", + "lib/insights/status-pipeline.ts", "lib/insights/metric-status.ts", "lib/insights/biomarker-status.ts", "lib/insights/derived/derived-assessment-ai.ts", diff --git a/src/__tests__/pulse-day-mean-reader-guard.test.ts b/src/__tests__/pulse-day-mean-reader-guard.test.ts index 0918eda6a..1250da987 100644 --- a/src/__tests__/pulse-day-mean-reader-guard.test.ts +++ b/src/__tests__/pulse-day-mean-reader-guard.test.ts @@ -128,7 +128,7 @@ const TS_READERS: Record = { /applyPayloadBudget\(\s*pulsePoints\s*,\s*\{[^}]*type\s*:\s*"PULSE"/, }, "lib/insights/signals-of-day.ts": { - via: "lib/insights/features.ts", + via: "lib/insights/feature-window.ts", passes: /byType\(\s*"PULSE"\s*\)[\s\S]{0,80}type\s*:\s*"PULSE"/, }, "lib/ai/coach/snapshot-blocks/core-metrics-block.ts": { diff --git a/src/app/api/insights/biomarker-assessment/__tests__/route.test.ts b/src/app/api/insights/biomarker-assessment/__tests__/route.test.ts index d596a5f57..d46454899 100644 --- a/src/app/api/insights/biomarker-assessment/__tests__/route.test.ts +++ b/src/app/api/insights/biomarker-assessment/__tests__/route.test.ts @@ -54,10 +54,6 @@ vi.mock("@/lib/insights/biomarker-status", () => ({ })), })); -vi.mock("@/lib/insights/metric-status", () => ({ - resolveMetricStatusLocale: () => "en", -})); - import { GET } from "../route"; import { getSession } from "@/lib/auth/session"; import { prisma } from "@/lib/db"; diff --git a/src/app/api/insights/biomarker-assessment/route.ts b/src/app/api/insights/biomarker-assessment/route.ts index 194acc3fb..4297cfd26 100644 --- a/src/app/api/insights/biomarker-assessment/route.ts +++ b/src/app/api/insights/biomarker-assessment/route.ts @@ -28,7 +28,7 @@ import { resolveServerLocale } from "@/lib/i18n/server-locale"; import { aiCapabilityToServe } from "@/lib/ai/capabilities/gate"; import { unavailableStatusBody } from "@/lib/insights/status-unavailable"; import { generateBiomarkerStatus } from "@/lib/insights/biomarker-status"; -import { resolveMetricStatusLocale } from "@/lib/insights/metric-status"; +import { normalizeLocale as resolveMetricStatusLocale } from "@/lib/insights/status-shared"; export const dynamic = "force-dynamic"; diff --git a/src/app/api/insights/blood-pressure-status/__tests__/route.test.ts b/src/app/api/insights/blood-pressure-status/__tests__/route.test.ts index 748572207..6bec84c6f 100644 --- a/src/app/api/insights/blood-pressure-status/__tests__/route.test.ts +++ b/src/app/api/insights/blood-pressure-status/__tests__/route.test.ts @@ -41,7 +41,6 @@ vi.mock("next/headers", () => ({ vi.mock("@/lib/insights/blood-pressure-status", () => ({ generateBloodPressureStatusForUser: vi.fn(), - resolveBloodPressureStatusLocale: () => "en", })); // The `statusText` capability decides whether the note is served; the diff --git a/src/app/api/insights/blood-pressure-status/route.ts b/src/app/api/insights/blood-pressure-status/route.ts index 9a0cec204..2671d62fd 100644 --- a/src/app/api/insights/blood-pressure-status/route.ts +++ b/src/app/api/insights/blood-pressure-status/route.ts @@ -1,9 +1,7 @@ import { NextRequest } from "next/server"; import { apiSuccess } from "@/lib/api-response"; -import { - generateBloodPressureStatusForUser, - resolveBloodPressureStatusLocale, -} from "@/lib/insights/blood-pressure-status"; +import { generateBloodPressureStatusForUser } from "@/lib/insights/blood-pressure-status"; +import { normalizeLocale as resolveBloodPressureStatusLocale } from "@/lib/insights/status-shared"; import { apiHandler, requireRecordAuth } from "@/lib/api-handler"; import { annotate } from "@/lib/logging/context"; import { resolveServerLocale } from "@/lib/i18n/server-locale"; diff --git a/src/app/api/insights/bmi-status/route.ts b/src/app/api/insights/bmi-status/route.ts index a78c2a57d..afd28f1cf 100644 --- a/src/app/api/insights/bmi-status/route.ts +++ b/src/app/api/insights/bmi-status/route.ts @@ -1,9 +1,7 @@ import { NextRequest } from "next/server"; import { apiSuccess } from "@/lib/api-response"; -import { - generateBmiStatusForUser, - resolveBmiStatusLocale, -} from "@/lib/insights/bmi-status"; +import { generateBmiStatusForUser } from "@/lib/insights/bmi-status"; +import { normalizeLocale as resolveBmiStatusLocale } from "@/lib/insights/status-shared"; import { apiHandler, requireRecordAuth } from "@/lib/api-handler"; import { annotate } from "@/lib/logging/context"; import { resolveServerLocale } from "@/lib/i18n/server-locale"; diff --git a/src/app/api/insights/medication-compliance-status/route.ts b/src/app/api/insights/medication-compliance-status/route.ts index 38d8bade3..2be9b3368 100644 --- a/src/app/api/insights/medication-compliance-status/route.ts +++ b/src/app/api/insights/medication-compliance-status/route.ts @@ -1,9 +1,7 @@ import { NextRequest } from "next/server"; import { apiSuccess } from "@/lib/api-response"; -import { - generateMedicationComplianceStatusForUser, - resolveMedicationComplianceStatusLocale, -} from "@/lib/insights/medication-compliance-status"; +import { generateMedicationComplianceStatusForUser } from "@/lib/insights/medication-compliance-status"; +import { normalizeLocale as resolveMedicationComplianceStatusLocale } from "@/lib/insights/status-shared"; import { apiHandler, requireRecordAuth } from "@/lib/api-handler"; import { annotate } from "@/lib/logging/context"; import { resolveServerLocale } from "@/lib/i18n/server-locale"; diff --git a/src/app/api/insights/metric-status/__tests__/route.test.ts b/src/app/api/insights/metric-status/__tests__/route.test.ts index 393f1a7ec..0ed114640 100644 --- a/src/app/api/insights/metric-status/__tests__/route.test.ts +++ b/src/app/api/insights/metric-status/__tests__/route.test.ts @@ -54,7 +54,6 @@ vi.mock("@/lib/insights/metric-status", () => ({ cached: true, updatedAt: new Date().toISOString(), })), - resolveMetricStatusLocale: () => "en", })); import { GET } from "../route"; diff --git a/src/app/api/insights/metric-status/route.ts b/src/app/api/insights/metric-status/route.ts index 4c105e83f..21640e778 100644 --- a/src/app/api/insights/metric-status/route.ts +++ b/src/app/api/insights/metric-status/route.ts @@ -33,10 +33,8 @@ import { requireModuleEnabled, type ModuleKey } from "@/lib/modules/gate"; import { aiCapabilityToServe } from "@/lib/ai/capabilities/gate"; import { unavailableStatusBody } from "@/lib/insights/status-unavailable"; import { prisma } from "@/lib/db"; -import { - generateMetricStatus, - resolveMetricStatusLocale, -} from "@/lib/insights/metric-status"; +import { generateMetricStatus } from "@/lib/insights/metric-status"; +import { normalizeLocale as resolveMetricStatusLocale } from "@/lib/insights/status-shared"; import { METRIC_STATUS_IDS, getMetricStatusMeta, diff --git a/src/app/api/insights/mood-status/__tests__/route.test.ts b/src/app/api/insights/mood-status/__tests__/route.test.ts index 7f411003d..10c532985 100644 --- a/src/app/api/insights/mood-status/__tests__/route.test.ts +++ b/src/app/api/insights/mood-status/__tests__/route.test.ts @@ -44,7 +44,6 @@ vi.mock("@/lib/insights/mood-status", () => ({ cached: true, updatedAt: new Date().toISOString(), })), - resolveMoodStatusLocale: () => "en", })); import { GET } from "../route"; diff --git a/src/app/api/insights/mood-status/route.ts b/src/app/api/insights/mood-status/route.ts index 1b7314d71..41fa7f304 100644 --- a/src/app/api/insights/mood-status/route.ts +++ b/src/app/api/insights/mood-status/route.ts @@ -1,9 +1,7 @@ import { NextRequest } from "next/server"; import { apiSuccess } from "@/lib/api-response"; -import { - generateMoodStatusForUser, - resolveMoodStatusLocale, -} from "@/lib/insights/mood-status"; +import { generateMoodStatusForUser } from "@/lib/insights/mood-status"; +import { normalizeLocale as resolveMoodStatusLocale } from "@/lib/insights/status-shared"; import { apiHandler, requireRecordAuth } from "@/lib/api-handler"; import { annotate } from "@/lib/logging/context"; import { resolveServerLocale } from "@/lib/i18n/server-locale"; diff --git a/src/app/api/insights/pulse-status/route.ts b/src/app/api/insights/pulse-status/route.ts index f22b1b9fe..76b5a0f2c 100644 --- a/src/app/api/insights/pulse-status/route.ts +++ b/src/app/api/insights/pulse-status/route.ts @@ -1,9 +1,7 @@ import { NextRequest } from "next/server"; import { apiSuccess } from "@/lib/api-response"; -import { - generatePulseStatusForUser, - resolvePulseStatusLocale, -} from "@/lib/insights/pulse-status"; +import { generatePulseStatusForUser } from "@/lib/insights/pulse-status"; +import { normalizeLocale as resolvePulseStatusLocale } from "@/lib/insights/status-shared"; import { apiHandler, requireRecordAuth } from "@/lib/api-handler"; import { annotate } from "@/lib/logging/context"; import { resolveServerLocale } from "@/lib/i18n/server-locale"; diff --git a/src/app/api/insights/weight-status/route.ts b/src/app/api/insights/weight-status/route.ts index bfa1e4a74..65a185874 100644 --- a/src/app/api/insights/weight-status/route.ts +++ b/src/app/api/insights/weight-status/route.ts @@ -1,9 +1,7 @@ import { NextRequest } from "next/server"; import { apiSuccess } from "@/lib/api-response"; -import { - generateWeightStatusForUser, - resolveWeightStatusLocale, -} from "@/lib/insights/weight-status"; +import { generateWeightStatusForUser } from "@/lib/insights/weight-status"; +import { normalizeLocale as resolveWeightStatusLocale } from "@/lib/insights/status-shared"; import { apiHandler, requireRecordAuth } from "@/lib/api-handler"; import { annotate } from "@/lib/logging/context"; import { resolveServerLocale } from "@/lib/i18n/server-locale"; diff --git a/src/lib/insights/biomarker-status.ts b/src/lib/insights/biomarker-status.ts index c14ae65db..5a178c3e1 100644 --- a/src/lib/insights/biomarker-status.ts +++ b/src/lib/insights/biomarker-status.ts @@ -3,12 +3,12 @@ * * Mirrors `generateMetricStatus` (the generic HealthKit-metric generator) * but reads `LabResult` rows for one user-scoped `Biomarker` instead of - * `Measurement` rows for a `MeasurementType`. It reuses the exact same - * machinery the metric cards use: - * - the shared provider runner (`runStatusCompletion`), - * - the stale-while-revalidate read-only path (`resolveReadOnlyStatusMiss`), - * - the standard cache-row persist (`persistStatusInsight`), - * - the cheap INPUT gate (`gateUnchangedStatusInput`) keyed on the latest + * `Measurement` rows for a `MeasurementType`. It runs on the same status + * pipeline stages (`status-pipeline.ts`) the metric cards use: + * - the single-card provider run (`runPreparedStatusCard`), + * - the stale-while-revalidate read-only path (`resolveReadOnlyStatus`), + * - the standard note persist (`textStatusCodec`), + * - the cheap INPUT gate (`gateUnchangedInputs`) keyed on the latest * reading fingerprint so a fresh assessment is produced ONLY when a new * reading lands — an idle day re-stamps the cached text without an LLM * round-trip. @@ -35,25 +35,25 @@ import { } from "@/lib/labs/reference-range"; import { getNoKeyBiomarkerStatusText } from "@/lib/insights/no-key-fallbacks"; import { hashInsightSnapshot } from "@/lib/insights/snapshot-hash"; -import { runStatusCompletion } from "@/lib/insights/status-provider"; +import { summarizeSeries } from "@/lib/insights/status-shared"; import { - normalizeLocale, - finalizeStatusSummary, - persistStatusInsight, - summarizeSeries, -} from "@/lib/insights/status-shared"; + runPreparedStatusCard, + type PreparedStatusCard, +} from "@/lib/insights/status-card-generation"; import { - gateUnchangedStatusInput, - readFreshStatusText, - resolveReadOnlyStatusMiss, - statusCacheAction, -} from "@/lib/insights/status-cache"; -import { returnTimeoutFallback } from "@/lib/insights/timeout-fallback"; + gateUnchangedInputs, + openStatusCard, + pendingStatusCard, + readCachedStatus, + resolveReadOnlyStatus, + textStatusCodec, + type StatusCardContext, +} from "@/lib/insights/status-pipeline"; import { annotate } from "@/lib/logging/context"; -import { resolveUserTimezone, userDayKey } from "@/lib/tz/resolver"; +import { userDayKey } from "@/lib/tz/resolver"; import { sanitizeForPrompt } from "@/lib/insights/sanitize"; -export interface BiomarkerStatusResult { +export type BiomarkerStatusResult = { hasProvider: boolean; text: string | null; cached: boolean; @@ -63,7 +63,7 @@ export interface BiomarkerStatusResult { revalidating?: boolean; /** True when the marker has no numeric readings; no LLM was called. */ insufficient?: boolean; -} +}; /** The `biomarker:` cache/queue scope for one marker. */ export function biomarkerStatusScope( @@ -91,6 +91,14 @@ function insufficient(): BiomarkerStatusResult { }; } +interface BiomarkerMarker { + id: string; + name: string; + unit: string; + lowerBound: number | null; + upperBound: number | null; +} + /** * Generate (or read from cache) the assessment for one biomarker. Mirrors * the metric generator's contract: @@ -123,39 +131,39 @@ export async function generateBiomarkerStatus(args: { return insufficient(); } - const locale = normalizeLocale(args.locale); - const force = args.force === true; - const readOnly = args.readOnly === true; - const scope = biomarkerStatusScope(marker.id); - const cacheAction = statusCacheAction(scope, locale); - // v1.30.3 — resolve the user's own tz BEFORE the day-key so the - // cache rolls over at the user's local midnight, not Berlin's. Has to - // happen ahead of the cache read below (the earliest possible return - // path). - const userTz = await resolveUserTimezone(args.userId); - const todayKey = userDayKey(new Date(), userTz); + const prepared = await prepareBiomarkerStatus(marker, args); + return (await runPreparedStatusCard(prepared)) as BiomarkerStatusResult; +} - const cached = await readFreshStatusText({ - userId: args.userId, - cacheAction, - todayKey, - force, - }); - if (cached) { - return { - hasProvider: true, - text: cached.text, - cached: true, - updatedAt: cached.updatedAt, - }; - } +/** + * Everything up to (not including) the provider call, on the shared + * pipeline stages, with the empty-data gate between the cache read and the + * read-only path and the input gate in place of the snapshot gate. + */ +async function prepareBiomarkerStatus( + marker: BiomarkerMarker, + args: { + userId: string; + locale?: string | null; + force?: boolean; + readOnly?: boolean; + }, +): Promise { + const ctx = await openStatusCard( + args.userId, + biomarkerStatusScope(marker.id), + args, + ); + + const cached = await readCachedStatus(ctx, textStatusCodec); + if (cached) return { phase: "served", result: cached }; // Numeric readings only — a qualitative-only marker carries no range verdict // or trend, so there is nothing to assess. Done BEFORE any enqueue so an // empty marker never queues an LLM job nor blocks on the provider. const rows = await prisma.labResult.findMany({ where: { - userId: args.userId, + userId: ctx.userId, biomarkerId: marker.id, deletedAt: null, value: { not: null }, @@ -180,7 +188,7 @@ export async function generateBiomarkerStatus(args: { action: { name: "insights.biomarker-status.insufficient" }, meta: { biomarkerId: marker.id }, }); - return insufficient(); + return { phase: "served", result: insufficient() }; } const readings: BiomarkerReading[] = rows.map((r) => ({ @@ -199,34 +207,21 @@ export async function generateBiomarkerStatus(args: { ); const totalCount = await prisma.labResult.count({ where: { - userId: args.userId, + userId: ctx.userId, biomarkerId: marker.id, deletedAt: null, value: { not: null }, }, }); - if (readOnly) { - const outcome = await resolveReadOnlyStatusMiss({ - userId: args.userId, - metric: scope, - locale, - }); - if (outcome.kind === "unavailable") { - return { - hasProvider: outcome.hasProvider, - text: getNoKeyBiomarkerStatusText(locale, marker.name), - cached: true, - updatedAt: null, - }; - } + if (ctx.readOnly) { return { - hasProvider: true, - text: outcome.lastGood?.text ?? null, - cached: outcome.lastGood !== null, - updatedAt: outcome.lastGood?.updatedAt ?? null, - preparing: outcome.lastGood === null, - revalidating: outcome.revalidating, + phase: "served", + result: await resolveReadOnlyStatus( + ctx, + textStatusCodec, + getNoKeyBiomarkerStatusText(ctx.locale, marker.name), + ), }; } @@ -245,8 +240,8 @@ export async function generateBiomarkerStatus(args: { // letting the input gate run keeps an idle marker to one cheap re-stamp per // day instead of a daily regeneration. const inputHash = hashInsightSnapshot({ - scope, - locale, + scope: ctx.scope, + locale: ctx.locale, promptVersion: PROMPT_VERSION, count: totalCount, bounds: { lower: latestRange.low, upper: latestRange.high }, @@ -256,22 +251,58 @@ export async function generateBiomarkerStatus(args: { value: latest.value, }, }); - const unchanged = await gateUnchangedStatusInput({ - userId: args.userId, - cacheAction, - todayKey, - inputHash, - force: false, - }); - if (unchanged) { - return { - hasProvider: true, - text: unchanged.text, - cached: true, - updatedAt: unchanged.updatedAt, - }; - } + const unchanged = await gateUnchangedInputs(ctx, inputHash, false); + if (unchanged) return { phase: "served", result: unchanged }; + const { snapshotJson, safeMarkerName } = biomarkerSnapshot({ + ctx, + marker, + readings, + latestRange, + totalCount, + }); + + annotate({ + action: { name: ctx.cacheAction }, + meta: { payload_size_bytes: snapshotJson.length }, + }); + + return pendingStatusCard(ctx, textStatusCodec, { + systemPrompt: getBiomarkerSystemPrompt( + safeMarkerName, + ctx.locale as Locale, + ), + userPrompt: getBiomarkerUserPrompt( + snapshotJson, + ctx.todayKey, + ctx.locale as Locale, + // The rotating opener hint the sibling cards carry, keyed per + // (user, marker, day) so consecutive markers do not all open alike. + openerArchetypeHint( + `${ctx.userId}:biomarker:${marker.id}:${ctx.todayKey}`, + ctx.locale as Locale, + ), + ), + hashes: { inputHash }, + temperature: AI_BUDGETS.statusArchetype.temperature, + maxTokens: AI_BUDGETS.statusArchetype.maxTokens, + fallbackText: getNoKeyBiomarkerStatusText(ctx.locale, marker.name), + emptyMessage: `Biomarker-status summary was empty for ${marker.id}`, + detail: undefined, + }); +} + +/** The marker snapshot: the range, the latest verdict and the series. */ +function biomarkerSnapshot(args: { + ctx: StatusCardContext; + marker: BiomarkerMarker; + readings: BiomarkerReading[]; + latestRange: ReturnType; + totalCount: number; +}): { snapshotJson: string; safeMarkerName: string } { + const { marker, readings, latestRange, totalCount } = args; + const { locale, todayKey, userTz } = args.ctx; + const latest = readings[0]; // Ascending series (oldest → newest) for the trend summary the prompt reads. const ascending = [...readings].reverse(); const summary = summarizeSeries(ascending.map((r) => ({ value: r.value }))); @@ -317,76 +348,5 @@ export async function generateBiomarkerStatus(args: { }; const snapshotJson = JSON.stringify(snapshot); - annotate({ - action: { name: cacheAction }, - meta: { payload_size_bytes: snapshotJson.length }, - }); - - const outcome = await runStatusCompletion({ - userId: args.userId, - cacheAction, - capability: "statusText", - systemPrompt: getBiomarkerSystemPrompt(safeMarkerName, locale as Locale), - userPrompt: getBiomarkerUserPrompt( - snapshotJson, - todayKey, - locale as Locale, - // The rotating opener hint the sibling cards carry, keyed per - // (user, marker, day) so consecutive markers do not all open alike. - openerArchetypeHint( - `${args.userId}:biomarker:${marker.id}:${todayKey}`, - locale as Locale, - ), - ), - temperature: AI_BUDGETS.statusArchetype.temperature, - maxTokens: AI_BUDGETS.statusArchetype.maxTokens, - }); - - if (outcome.kind === "none") { - return { - hasProvider: false, - text: getNoKeyBiomarkerStatusText(locale, marker.name), - cached: true, - updatedAt: null, - }; - } - if (outcome.kind === "timeout" || outcome.kind === "error") { - return returnTimeoutFallback({ - cacheAction, - reason: outcome.kind, - userId: args.userId, - todayKey, - stubText: getNoKeyBiomarkerStatusText(locale, marker.name), - }); - } - - // Outbound safety screen — WITHHOLD policy for a background-generated card. - const screened = finalizeStatusSummary(outcome.content, locale); - if (!screened.ok) { - annotate({ - action: { name: "insights.status.outbound_blocked" }, - meta: { cacheAction, reason: screened.reason }, - }); - return returnTimeoutFallback({ - cacheAction, - reason: "screened", - userId: args.userId, - todayKey, - stubText: getNoKeyBiomarkerStatusText(locale, marker.name), - }); - } - const text = screened.text; - if (!text) { - throw new Error(`Biomarker-status summary was empty for ${marker.id}`); - } - - const updatedAt = await persistStatusInsight({ - userId: args.userId, - cacheAction, - todayKey, - text, - inputHash, - }); - - return { hasProvider: true, text, cached: false, updatedAt }; + return { snapshotJson, safeMarkerName }; } diff --git a/src/lib/insights/blood-pressure-status.ts b/src/lib/insights/blood-pressure-status.ts index 2063bbed6..f5e5d80bd 100644 --- a/src/lib/insights/blood-pressure-status.ts +++ b/src/lib/insights/blood-pressure-status.ts @@ -4,18 +4,7 @@ import { getBloodPressureSystemPrompt, getBloodPressureUserPrompt, } from "@/lib/ai/prompts/blood-pressure"; -import { openerArchetypeHint } from "@/lib/ai/prompts/opener-archetype"; -import type { Locale } from "@/lib/i18n/config"; -import { - formatPreviousContextForPrompt, - getPreviousInsightContext, -} from "@/lib/insights/memory"; -import { - buildAssessmentContextBlock, - computeSteadyRun, - pickVarietyLead, -} from "@/lib/insights/assessment-context"; -import { getRelevantCorrelationsForMetric } from "@/lib/insights/metric-correlation-context"; +import { computeSteadyRun } from "@/lib/insights/assessment-context"; import { getBpTargets } from "@/lib/analytics/bp-targets"; import { isBpReadingInTarget } from "@/lib/analytics/bp-in-target"; import { @@ -41,33 +30,24 @@ import { import { buildGradedSeriesFromPoints, buildGradedSeriesWithRollups, - degradeStatusSnapshotToBudget, } from "@/lib/insights/graded-series"; -import { statusSnapshotCharsForRecord } from "@/lib/ai/capabilities/usage"; import { buildMetricSignal } from "@/lib/insights/metric-signal"; -import { - type SupportedLocale, - normalizeLocale, - finalizeStatusSummary, - persistStatusInsight, - round, - summarizeSeries, -} from "@/lib/insights/status-shared"; -import { - readFreshStatusText, - refreshUnchangedStatusInsight, - resolveReadOnlyStatusMiss, - statusCacheAction, -} from "@/lib/insights/status-cache"; -import { hashInsightSnapshot } from "@/lib/insights/snapshot-hash"; -import { returnTimeoutFallback } from "@/lib/insights/timeout-fallback"; +import { round, summarizeSeries } from "@/lib/insights/status-shared"; import { runPreparedStatusCard, type PreparedStatusCard, - type StatusCardResult, } from "@/lib/insights/status-card-generation"; -import { annotate } from "@/lib/logging/context"; -import { resolveUserTimezone, userDayKey } from "@/lib/tz/resolver"; +import { + coverageSpan, + prepareStatusCard, + textStatusCodec, + type StatusCardBuild, + type StatusCardContext, + type StatusCardOptions, + type StatusCardPlugin, + type StatusTextResult, +} from "@/lib/insights/status-pipeline"; +import { userDayKey } from "@/lib/tz/resolver"; import { DEFAULT_TIMEZONE } from "@/lib/tz/format"; import { TRACKED_INTAKE_WHERE } from "@/lib/medications/intake-tracking"; import { canonicalDailyTimestamp } from "@/lib/measurements/consolidation-tz"; @@ -122,108 +102,32 @@ function pairDailyBuckets( */ export async function generateBloodPressureStatusForUser( userId: string, - options?: { - locale?: string | null; - force?: boolean; - readOnly?: boolean; - }, -): Promise<{ - hasProvider: boolean; - text: string | null; - cached: boolean; - updatedAt: string | null; - preparing?: boolean; - revalidating?: boolean; -}> { + options?: StatusCardOptions, +): Promise { const prepared = await prepareBloodPressureStatusForUser(userId, options); - const result = await runPreparedStatusCard(prepared); - return result as { - hasProvider: boolean; - text: string | null; - cached: boolean; - updatedAt: string | null; - preparing?: boolean; - revalidating?: boolean; - }; + return (await runPreparedStatusCard(prepared)) as StatusTextResult; } /** - * v1.18.7 — everything up to (not including) the provider call. - * See `status-card-generation.ts` for the prepare/run/finalize contract. + * Everything up to (not including) the provider call, through the shared + * status pipeline (`status-pipeline.ts`); this card supplies its snapshot. */ -export async function prepareBloodPressureStatusForUser( +export function prepareBloodPressureStatusForUser( userId: string, - options?: { - locale?: string | null; - force?: boolean; - /** v1.8.3 — read-only navigation path; see weight-status for the rationale. */ - readOnly?: boolean; - }, + options?: StatusCardOptions, ): Promise { - const locale = normalizeLocale(options?.locale); - const force = options?.force === true; - const readOnly = options?.readOnly === true; - const cacheAction = statusCacheAction("blood-pressure", locale); - // v1.30.3 — resolve the user's own tz BEFORE the day-key so the - // cache rolls over at the user's local midnight, not Berlin's. Has to - // happen ahead of the cache read below (the earliest possible return - // path), not just the later day-bucketing reads. - const userTz = await resolveUserTimezone(userId); - const todayKey = userDayKey(new Date(), userTz); - - const cached = await readFreshStatusText({ - userId, - cacheAction, - todayKey, - force, - }); - if (cached) { - return { - phase: "served", - result: { - hasProvider: true, - text: cached.text, - cached: true, - updatedAt: cached.updatedAt, - }, - }; - } - - if (readOnly) { - const outcome = await resolveReadOnlyStatusMiss({ - userId, - metric: "blood-pressure", - locale, - }); - // Unavailable for any reason serves the deterministic line and enqueues - // nothing. `hasProvider` is provider presence only. - if (outcome.kind === "unavailable") { - return { - phase: "served", - result: { - hasProvider: outcome.hasProvider, - text: getNoKeyBloodPressureStatusText(locale), - cached: true, - updatedAt: null, - }, - }; - } - // v1.8.7 — stale-while-revalidate: serve the last good assessment - // (if any) instantly while the worker re-warms; only fall to the empty - // preparing skeleton when none was ever produced. - return { - phase: "served", - result: { - hasProvider: true, - text: outcome.lastGood?.text ?? null, - cached: outcome.lastGood !== null, - updatedAt: outcome.lastGood?.updatedAt ?? null, - preparing: outcome.lastGood === null, - revalidating: outcome.revalidating, - }, - }; - } + return prepareStatusCard(BLOOD_PRESSURE_STATUS_CARD, userId, options); +} +/** + * The blood-pressure snapshot: both channels against the person's targets, + * weight against systolic, BP-medication adherence against systolic, and + * mood context. + */ +async function buildBloodPressureSnapshot( + ctx: StatusCardContext, +): Promise> { + const { userId, locale, userTz, todayKey } = ctx; const user = await prisma.user.findUnique({ where: { id: userId }, select: { @@ -231,6 +135,188 @@ export async function prepareBloodPressureStatusForUser( }, }); + const { measurements, now, weightSeries, sysSeries, diaSeries } = + await readBpChannels(userId, userTz); + + const bpTargets = getBpTargets(user?.dateOfBirth ?? null); + const { pairedBloodPressure, bpInTargetPctLast30DailyPoints } = + pairBloodPressure(sysSeries.daily, diaSeries.daily, bpTargets, now, userTz); + + const weightVsSystolicPairs = pairDailyBuckets( + weightSeries.daily, + sysSeries.daily, + now, + userTz, + ); + const weightVsSystolicCorrelation = significantPearsonCorrelation( + weightVsSystolicPairs, + ); + + const medicationContext = await readBpMedicationContext( + userId, + now, + userTz, + sysSeries.daily, + ); + + const { moodSeries, moodMean, moodGraded } = await readBpMoodContext( + userId, + now, + userTz, + ); + + // The two BP channels source their recent / weekly slices from a + // bounded raw read and their monthly / yearly tail from the MONTH / + // YEAR rollup tier (full-history in-memory fallback on a cold-tier + // coverage miss). Mood has no rollup tier, so it stays an in-memory fold. + const [sysGraded, diaGraded] = await Promise.all([ + buildGradedSeriesWithRollups(userId, "BLOOD_PRESSURE_SYS", now, userTz), + buildGradedSeriesWithRollups(userId, "BLOOD_PRESSURE_DIA", now, userTz), + ]); + + const { totalSpanDays, newestDaysAgo: newestMeasurementDaysAgo } = + coverageSpan( + measurements.length > 0 ? measurements[0].measuredAt : null, + measurements.length > 0 + ? measurements[measurements.length - 1].measuredAt + : null, + ); + + // v1.18.10 — hand the model the finished recent-vs-baseline + // comparison + normal-swing verdict per channel instead of asking it to + // derive them. BP needs two signals; the user's own targets are the coarse + // band anchor when present. + const sysSignal = buildMetricSignal({ + metric: locale === "en" ? "your systolic" : "deine Systole", + unit: "mmHg", + direction: "lower-better", + graded: sysGraded, + normalRange: bpTargets + ? { low: bpTargets.sysLow, high: bpTargets.sysHigh } + : null, + newestDaysAgo: newestMeasurementDaysAgo, + }); + const diaSignal = buildMetricSignal({ + metric: locale === "en" ? "your diastolic" : "deine Diastole", + unit: "mmHg", + direction: "lower-better", + graded: diaGraded, + normalRange: bpTargets + ? { low: bpTargets.diaLow, high: bpTargets.diaHigh } + : null, + newestDaysAgo: newestMeasurementDaysAgo, + }); + + const snapshot = { + locale, + generatedForDay: todayKey, + focus: "blood_pressure", + dataCoverage: { + totalMeasurements: measurements.length, + totalSpanDays, + newestMeasurementDaysAgo, + }, + bloodPressure: { + systolic: { + ...(sysSignal ? { signal: sysSignal } : {}), + summary: summarizeSeries( + sysSeries.daily.map((bucket) => ({ value: bucket.value })), + ), + series: sysGraded, + }, + diastolic: { + ...(diaSignal ? { signal: diaSignal } : {}), + summary: summarizeSeries( + diaSeries.daily.map((bucket) => ({ value: bucket.value })), + ), + series: diaGraded, + }, + paired: { + summary: summarizeSeries( + pairedBloodPressure.map((entry) => ({ + value: (entry.sys + entry.dia) / 2, + })), + ), + series: pairedBloodPressure.slice(-CORRELATION_PAIR_CAP), + }, + targets: bpTargets + ? { + systolic: { min: bpTargets.sysLow, max: bpTargets.sysHigh }, + diastolic: { min: bpTargets.diaLow, max: bpTargets.diaHigh }, + inTargetPctLast30DailyPoints: bpInTargetPctLast30DailyPoints, + } + : null, + }, + weightVsSystolic: { + correlation: weightVsSystolicCorrelation, + pairs: weightVsSystolicPairs + .slice(-CORRELATION_PAIR_CAP) + .map((entry) => ({ + day: entry.dayKey, + weight: round(entry.a, 2), + systolic: round(entry.b, 2), + })), + }, + bpMedicationContinuityVsSystolic: { + expectedIntakesPerDay: medicationContext.expectedIntakesPerDay, + medicationCount: medicationContext.medicationCount, + correlation: medicationContext.continuityVsSystolicCorrelation, + series: + medicationContext.continuityVsSystolicSeries.slice( + -CORRELATION_PAIR_CAP, + ), + }, + bpMedications: medicationContext.medicationCompliance, + moodContext: + moodSeries.daily.length >= 3 + ? { + points: moodSeries.daily.length, + mean: moodMean, + latest: moodSeries.daily[0]?.value ?? null, + series: moodGraded, + moodVsSystolicCorrelation: (() => { + const moodVsSysPairs = pairDailyBuckets( + moodSeries.daily, + sysSeries.daily, + now, + userTz, + ); + return significantPearsonCorrelation(moodVsSysPairs); + })(), + } + : null, + }; + + return { + snapshot, + finish: () => ({ + context: { + repeatCount: computeSteadyRun(sysGraded.weekly, sysGraded.monthly), + points: sysSeries.daily.length, + newestDaysAgo: newestMeasurementDaysAgo, + relationsFor: "BLOOD_PRESSURE_SYS", + }, + fallbackText: getNoKeyBloodPressureStatusText(locale, sysSignal), + prompts: (snapshotJson, context) => ({ + systemPrompt: getBloodPressureSystemPrompt(locale), + userPrompt: getBloodPressureUserPrompt( + snapshotJson, + todayKey, + locale, + context.previousContextBlock, + context.assessmentContextBlock, + context.openerHint, + ), + }), + detail: undefined, + }), + }; +} + +/** + * Weight and the two BP channels, read once and bucketed per user-local day. + */ +async function readBpChannels(userId: string, userTz: string) { // v1.4.28 — cap the snapshot input. The downstream // `applyPayloadBudget` trims further; this `take` keeps the read // bounded for Apple-Health-rich accounts. BP captures three types @@ -291,10 +377,23 @@ export async function prepareBloodPressureStatusForUser( { now, tz: userTz }, ); - const bpTargets = getBpTargets(user?.dateOfBirth ?? null); + return { measurements, now, weightSeries, sysSeries, diaSeries }; +} + +/** + * Systolic paired with diastolic per day, each day judged against the + * person's targets, and the share of paired days in target. + */ +function pairBloodPressure( + sysDaily: DailyBucket[], + diaDaily: DailyBucket[], + bpTargets: ReturnType, + now: Date, + userTz: string, +) { const pairedBloodPressure = pairDailyBuckets( - sysSeries.daily, - diaSeries.daily, + sysDaily, + diaDaily, now, userTz, ).map((entry) => ({ @@ -320,16 +419,58 @@ export async function prepareBloodPressureStatusForUser( 1, ); - const weightVsSystolicPairs = pairDailyBuckets( - weightSeries.daily, - sysSeries.daily, - now, - userTz, - ); - const weightVsSystolicCorrelation = significantPearsonCorrelation( - weightVsSystolicPairs, - ); + return { pairedBloodPressure, bpInTargetPctLast30DailyPoints }; +} +/** The mood context: daily buckets, their mean, and the graded series. */ +async function readBpMoodContext(userId: string, now: Date, userTz: string) { + // Fetch mood context (optional — for enrichment only). v1.4.28 + // Cap at 90 entries (~3 months) so power users don't pull + // unbounded rows before the bucket-series budget runs. + const moodEntries = await readRecentMoodScores(userId, 90); + + const moodSeries = applyPayloadBudget( + moodEntries.map((entry) => ({ + measuredAt: entry.moodLoggedAt, + value: entry.score, + })), + { now, tz: userTz }, + ); + const moodSummary = summarizeSeries( + moodSeries.daily.map((bucket) => ({ value: bucket.value })), + ); + const moodMean = moodSummary?.mean ?? null; + + // Compact graded series for the prompt. The `*Series.daily` buckets + // above stay for the correlation pairing + in-target gate; only the + // graded shape is embedded so the full daily arrays never ship. + const gradedFromDaily = (buckets: DailyBucket[]) => + buildGradedSeriesFromPoints( + buckets.map((b) => ({ + measuredAt: canonicalDailyTimestamp( + dayOffsetToDayKey(now, b.dayOffset, userTz), + userTz, + ), + value: b.value, + })), + now, + userTz, + ); + const moodGraded = gradedFromDaily(moodSeries.daily); + return { moodSeries, moodMean, moodGraded }; +} + +/** + * The active, scheduled BP medications with their 7- and 30-day compliance, + * and the per-day intake continuity against the systolic series with its + * significance-gated correlation. + */ +async function readBpMedicationContext( + userId: string, + now: Date, + userTz: string, + sysDaily: DailyBucket[], +) { const activeMedicationRows = await prisma.medication.findMany({ // v1.16.11 — as-needed (PRN) medications never feed the BP-status // compliance gate (no expected doses, no rate). @@ -440,7 +581,7 @@ export async function prepareBloodPressureStatusForUser( takenByDay.set(dayKey, (takenByDay.get(dayKey) ?? 0) + 1); } - const continuityVsSystolicSeries = sysSeries.daily.map((point) => { + const continuityVsSystolicSeries = sysDaily.map((point) => { // DST-safe: dayOffsetToDayKey computes calendar days, not 24h ticks. const dayKey = dayOffsetToDayKey(now, point.dayOffset, userTz); const taken = takenByDay.get(dayKey) ?? 0; @@ -456,48 +597,6 @@ export async function prepareBloodPressureStatusForUser( }; }); - // Fetch mood context (optional — for enrichment only). v1.4.28 - // Cap at 90 entries (~3 months) so power users don't pull - // unbounded rows before the bucket-series budget runs. - const moodEntries = await readRecentMoodScores(userId, 90); - - const moodSeries = applyPayloadBudget( - moodEntries.map((entry) => ({ - measuredAt: entry.moodLoggedAt, - value: entry.score, - })), - { now, tz: userTz }, - ); - const moodSummary = summarizeSeries( - moodSeries.daily.map((bucket) => ({ value: bucket.value })), - ); - const moodMean = moodSummary?.mean ?? null; - - // Compact graded series for the prompt. The `*Series.daily` buckets - // above stay for the correlation pairing + in-target gate; only the - // graded shape is embedded so the full daily arrays never ship. - const gradedFromDaily = (buckets: DailyBucket[]) => - buildGradedSeriesFromPoints( - buckets.map((b) => ({ - measuredAt: canonicalDailyTimestamp( - dayOffsetToDayKey(now, b.dayOffset, userTz), - userTz, - ), - value: b.value, - })), - now, - userTz, - ); - // The two BP channels source their recent / weekly slices from a - // bounded raw read and their monthly / yearly tail from the MONTH / - // YEAR rollup tier (full-history in-memory fallback on a cold-tier - // coverage miss). Mood has no rollup tier, so it stays an in-memory fold. - const [sysGraded, diaGraded] = await Promise.all([ - buildGradedSeriesWithRollups(userId, "BLOOD_PRESSURE_SYS", now, userTz), - buildGradedSeriesWithRollups(userId, "BLOOD_PRESSURE_DIA", now, userTz), - ]); - const moodGraded = gradedFromDaily(moodSeries.daily); - const continuityVsSystolicPairs: PairedPoint[] = continuityVsSystolicSeries .map((entry) => { if (entry.continuityPct == null) return null; @@ -514,281 +613,22 @@ export async function prepareBloodPressureStatusForUser( continuityVsSystolicPairs, ); - const oldestMeasurement = - measurements.length > 0 ? measurements[0].measuredAt : null; - const newestMeasurement = - measurements.length > 0 - ? measurements[measurements.length - 1].measuredAt - : null; - const totalSpanDays = - oldestMeasurement && newestMeasurement - ? Math.round( - (newestMeasurement.getTime() - oldestMeasurement.getTime()) / - (24 * 60 * 60 * 1000), - ) - : 0; - const newestMeasurementDaysAgo = newestMeasurement - ? Math.round( - (Date.now() - newestMeasurement.getTime()) / (24 * 60 * 60 * 1000), - ) - : null; - - // v1.18.10 — hand the model the finished recent-vs-baseline - // comparison + normal-swing verdict per channel instead of asking it to - // derive them. BP needs two signals; the user's own targets are the coarse - // band anchor when present. - const sysSignal = buildMetricSignal({ - metric: locale === "en" ? "your systolic" : "deine Systole", - unit: "mmHg", - direction: "lower-better", - graded: sysGraded, - normalRange: bpTargets - ? { low: bpTargets.sysLow, high: bpTargets.sysHigh } - : null, - newestDaysAgo: newestMeasurementDaysAgo, - }); - const diaSignal = buildMetricSignal({ - metric: locale === "en" ? "your diastolic" : "deine Diastole", - unit: "mmHg", - direction: "lower-better", - graded: diaGraded, - normalRange: bpTargets - ? { low: bpTargets.diaLow, high: bpTargets.diaHigh } - : null, - newestDaysAgo: newestMeasurementDaysAgo, - }); - - const snapshot = { - locale, - generatedForDay: todayKey, - focus: "blood_pressure", - dataCoverage: { - totalMeasurements: measurements.length, - totalSpanDays, - newestMeasurementDaysAgo, - }, - bloodPressure: { - systolic: { - ...(sysSignal ? { signal: sysSignal } : {}), - summary: summarizeSeries( - sysSeries.daily.map((bucket) => ({ value: bucket.value })), - ), - series: sysGraded, - }, - diastolic: { - ...(diaSignal ? { signal: diaSignal } : {}), - summary: summarizeSeries( - diaSeries.daily.map((bucket) => ({ value: bucket.value })), - ), - series: diaGraded, - }, - paired: { - summary: summarizeSeries( - pairedBloodPressure.map((entry) => ({ - value: (entry.sys + entry.dia) / 2, - })), - ), - series: pairedBloodPressure.slice(-CORRELATION_PAIR_CAP), - }, - targets: bpTargets - ? { - systolic: { min: bpTargets.sysLow, max: bpTargets.sysHigh }, - diastolic: { min: bpTargets.diaLow, max: bpTargets.diaHigh }, - inTargetPctLast30DailyPoints: bpInTargetPctLast30DailyPoints, - } - : null, - }, - weightVsSystolic: { - correlation: weightVsSystolicCorrelation, - pairs: weightVsSystolicPairs - .slice(-CORRELATION_PAIR_CAP) - .map((entry) => ({ - day: entry.dayKey, - weight: round(entry.a, 2), - systolic: round(entry.b, 2), - })), - }, - bpMedicationContinuityVsSystolic: { - expectedIntakesPerDay: expectedBpIntakesPerDay, - medicationCount: bpMedications.length, - correlation: continuityVsSystolicCorrelation, - series: continuityVsSystolicSeries.slice(-CORRELATION_PAIR_CAP), - }, - bpMedications: medicationCompliance, - moodContext: - moodSeries.daily.length >= 3 - ? { - points: moodSeries.daily.length, - mean: moodMean, - latest: moodSeries.daily[0]?.value ?? null, - series: moodGraded, - moodVsSystolicCorrelation: (() => { - const moodVsSysPairs = pairDailyBuckets( - moodSeries.daily, - sysSeries.daily, - now, - userTz, - ); - return significantPearsonCorrelation(moodVsSysPairs); - })(), - } - : null, - }; - - const shed = degradeStatusSnapshotToBudget( - snapshot as unknown as Record, - { maxChars: await statusSnapshotCharsForRecord(userId) }, - ); - const snapshotJson = JSON.stringify(snapshot); - - annotate({ - action: { name: cacheAction }, - meta: { - payload_size_bytes: snapshotJson.length, - ...(shed.length > 0 ? { snapshot_shed: shed } : {}), - }, - }); - - // Content-hash gate (v1.16.8): when the snapshot is unchanged since the - // last real assessment, refresh the cache timestamp and skip the LLM. - const snapshotHash = hashInsightSnapshot(snapshot); - const unchanged = await refreshUnchangedStatusInsight({ - userId, - cacheAction, - todayKey, - snapshotHash, - }); - if (unchanged) { - return { - phase: "served", - result: { - hasProvider: true, - text: unchanged.text, - cached: true, - updatedAt: unchanged.updatedAt, - }, - }; - } - - const previousContext = await getPreviousInsightContext( - userId, - "blood-pressure-status", - locale, - 12, - ); - const previousContextBlock = formatPreviousContextForPrompt( - previousContext, - locale, - ); - - // v1.12.7 — same diversity / anti-repetition block the archetype cards - // carry, now threaded into this headline card. Pure formatting of - // already-computed data; the correlation fetch is best-effort and resolves - // to no block on failure, never a generation failure. - const varietyLead = pickVarietyLead(userId, "blood-pressure", todayKey); - const steadyRun = computeSteadyRun(sysGraded.weekly, sysGraded.monthly); - const relations = await getRelevantCorrelationsForMetric( - userId, - "BLOOD_PRESSURE_SYS", - locale, - ); - const assessmentContextBlock = buildAssessmentContextBlock( - { - varietyLead, - dataStrength: { - points: sysSeries.daily.length, - newestDaysAgo: newestMeasurementDaysAgo, - }, - repeatCount: steadyRun, - relations, - }, - locale, - ); - - // The provider call is deferred to the caller — single-card path runs ONE - // completion; the batch path folds this prompt into one shared call. return { - phase: "pending", - metric: "blood-pressure", - userId, - cacheAction, - systemPrompt: getBloodPressureSystemPrompt(locale), - userPrompt: getBloodPressureUserPrompt( - snapshotJson, - todayKey, - locale, - previousContextBlock, - assessmentContextBlock, - // v1.28.40 — rotating opener hint, per (user, metric, day); activates the - // base prompt's verdict-first opener branch on the per-metric card. - openerArchetypeHint( - `${userId}:blood-pressure:${todayKey}`, - locale as Locale, - ), - ), - snapshotHash, - // v1.12.7 — match the archetype cards' 0.45: more cadence entropy while - // FACTS stay pinned by the snapshot + the forbidden-phrase guards. - temperature: 0.45, - noProvider: { - hasProvider: false, - text: getNoKeyBloodPressureStatusText(locale, sysSignal), - cached: true, - updatedAt: null, - }, - timeout: (reason): StatusCardResult => - returnTimeoutFallback({ - cacheAction, - reason, - userId, - todayKey, - stubText: getNoKeyBloodPressureStatusText(locale, sysSignal), - }), - finalize: async (outcome): Promise => { - // Outbound safety screen. The only transform here used to be - // whitespace-normalisation, so a dose-change imperative or a fabricated - // clinical risk score persisted as the day's cached assessment. Policy - // for a background-generated card is WITHHOLD: serve the deterministic - // stub and persist no model text (see `finalizeStatusSummary`). - const screened = finalizeStatusSummary(outcome.content, locale); - if (!screened.ok) { - annotate({ - action: { name: "insights.status.outbound_blocked" }, - meta: { cacheAction, reason: screened.reason }, - }); - return returnTimeoutFallback({ - cacheAction, - reason: "screened", - userId: userId, - todayKey: todayKey, - stubText: getNoKeyBloodPressureStatusText(locale, sysSignal), - }); - } - const summary = screened.text; - if (!summary) { - throw new Error( - "Blood-pressure-status summary was empty after normalization", - ); - } - const updatedAt = await persistStatusInsight({ - userId, - cacheAction, - todayKey, - text: summary, - snapshotHash, - }); - return { - hasProvider: true, - text: summary, - cached: false, - updatedAt, - }; - }, + medicationCompliance, + expectedIntakesPerDay: expectedBpIntakesPerDay, + medicationCount: bpMedications.length, + continuityVsSystolicSeries, + continuityVsSystolicCorrelation, }; } -export function resolveBloodPressureStatusLocale( - locale: string | null | undefined, -): SupportedLocale { - return normalizeLocale(locale); -} +const BLOOD_PRESSURE_STATUS_CARD: StatusCardPlugin< + StatusTextResult, + undefined +> = { + scope: "blood-pressure", + codec: textStatusCodec, + unavailableText: (locale) => getNoKeyBloodPressureStatusText(locale), + emptyMessage: "Blood-pressure-status summary was empty after normalization", + build: buildBloodPressureSnapshot, +}; diff --git a/src/lib/insights/bmi-status.ts b/src/lib/insights/bmi-status.ts index 2a3ae4955..18074b770 100644 --- a/src/lib/insights/bmi-status.ts +++ b/src/lib/insights/bmi-status.ts @@ -1,51 +1,32 @@ import { prisma } from "@/lib/db"; import { getBmiSystemPrompt, getBmiUserPrompt } from "@/lib/ai/prompts/bmi"; -import { openerArchetypeHint } from "@/lib/ai/prompts/opener-archetype"; -import type { Locale } from "@/lib/i18n/config"; import { classifyBMI } from "@/lib/analytics/classifications"; import { getNoKeyBmiStatusText } from "@/lib/insights/no-key-fallbacks"; -import { - formatPreviousContextForPrompt, - getPreviousInsightContext, -} from "@/lib/insights/memory"; -import { - buildAssessmentContextBlock, - computeSteadyRun, - pickVarietyLead, -} from "@/lib/insights/assessment-context"; +import { computeSteadyRun } from "@/lib/insights/assessment-context"; import { applyPayloadBudget } from "@/lib/insights/bucket-series"; import { buildGradedSeriesWithRollups, scaleGradedSeries, - degradeStatusSnapshotToBudget, } from "@/lib/insights/graded-series"; -import { statusSnapshotCharsForRecord } from "@/lib/ai/capabilities/usage"; import { buildMetricSignal } from "@/lib/insights/metric-signal"; -import { - type SupportedLocale, - normalizeLocale, - finalizeStatusSummary, - persistStatusInsight, - round, - summarizeSeries, -} from "@/lib/insights/status-shared"; -import { - computeStatusInputFingerprint, - gateUnchangedStatusInput, - readFreshStatusText, - refreshUnchangedStatusInsight, - resolveReadOnlyStatusMiss, - statusCacheAction, -} from "@/lib/insights/status-cache"; -import { hashInsightSnapshot } from "@/lib/insights/snapshot-hash"; -import { returnTimeoutFallback } from "@/lib/insights/timeout-fallback"; +import { round, summarizeSeries } from "@/lib/insights/status-shared"; +import { computeStatusInputFingerprint } from "@/lib/insights/status-cache"; import { runPreparedStatusCard, type PreparedStatusCard, - type StatusCardResult, } from "@/lib/insights/status-card-generation"; -import { annotate } from "@/lib/logging/context"; -import { resolveUserTimezone, userDayKey } from "@/lib/tz/resolver"; +import { + coverageSpan, + gateUnchangedInputs, + prepareStatusCard, + textStatusCodec, + type ServedBuild, + type StatusCardBuild, + type StatusCardContext, + type StatusCardOptions, + type StatusCardPlugin, + type StatusTextResult, +} from "@/lib/insights/status-pipeline"; /** * Public entry — unchanged signature. Prepares the card (cache-read, @@ -56,112 +37,29 @@ import { resolveUserTimezone, userDayKey } from "@/lib/tz/resolver"; */ export async function generateBmiStatusForUser( userId: string, - options?: { - locale?: string | null; - force?: boolean; - /** v1.8.3 — read-only navigation path; see weight-status for the rationale. */ - readOnly?: boolean; - }, -): Promise<{ - hasProvider: boolean; - text: string | null; - cached: boolean; - updatedAt: string | null; - preparing?: boolean; - /** v1.9.0 — last-good text served while a refresh is in flight; keep polling. */ - revalidating?: boolean; -}> { + options?: StatusCardOptions, +): Promise { const prepared = await prepareBmiStatusForUser(userId, options); - const result = await runPreparedStatusCard(prepared); - return result as { - hasProvider: boolean; - text: string | null; - cached: boolean; - updatedAt: string | null; - preparing?: boolean; - revalidating?: boolean; - }; + return (await runPreparedStatusCard(prepared)) as StatusTextResult; } /** - * v1.18.7 — everything up to (not including) the provider call. - * Returns a finished `served` result for every path that never needed the - * LLM, or a `pending` descriptor the single-card path and the batch path - * both drive. See `status-card-generation.ts` for the contract. + * Everything up to (not including) the provider call, through the shared + * status pipeline (`status-pipeline.ts`); this card supplies its height + * check, its input gate and its snapshot. */ -export async function prepareBmiStatusForUser( +export function prepareBmiStatusForUser( userId: string, - options?: { - locale?: string | null; - force?: boolean; - /** v1.8.3 — read-only navigation path; see weight-status for the rationale. */ - readOnly?: boolean; - }, + options?: StatusCardOptions, ): Promise { - const locale = normalizeLocale(options?.locale); - const force = options?.force === true; - const readOnly = options?.readOnly === true; - const cacheAction = statusCacheAction("bmi", locale); - // v1.30.3 — resolve the user's own tz BEFORE the day-key so the - // cache rolls over at the user's local midnight, not Berlin's. Has to - // happen ahead of the cache read below (the earliest possible return - // path), not just the later day-bucketing reads. - const userTz = await resolveUserTimezone(userId); - const todayKey = userDayKey(new Date(), userTz); - - const cached = await readFreshStatusText({ - userId, - cacheAction, - todayKey, - force, - }); - if (cached) { - return { - phase: "served", - result: { - hasProvider: true, - text: cached.text, - cached: true, - updatedAt: cached.updatedAt, - }, - }; - } - - if (readOnly) { - const outcome = await resolveReadOnlyStatusMiss({ - userId, - metric: "bmi", - locale, - }); - // Unavailable for any reason serves the deterministic line and enqueues - // nothing. `hasProvider` is provider presence only. - if (outcome.kind === "unavailable") { - return { - phase: "served", - result: { - hasProvider: outcome.hasProvider, - text: getNoKeyBmiStatusText(locale), - cached: true, - updatedAt: null, - }, - }; - } - // v1.8.7 — stale-while-revalidate: serve the last good assessment - // (if any) instantly while the worker re-warms; only fall to the empty - // preparing skeleton when none was ever produced. - return { - phase: "served", - result: { - hasProvider: true, - text: outcome.lastGood?.text ?? null, - cached: outcome.lastGood !== null, - updatedAt: outcome.lastGood?.updatedAt ?? null, - preparing: outcome.lastGood === null, - revalidating: outcome.revalidating, - }, - }; - } + return prepareStatusCard(BMI_STATUS_CARD, userId, options); +} +/** The BMI snapshot: the weight series scaled by the profile height. */ +async function buildBmiSnapshot( + ctx: StatusCardContext, +): Promise | ServedBuild> { + const { userId, locale, userTz, todayKey } = ctx; const user = await prisma.user.findUnique({ where: { id: userId }, select: { @@ -195,25 +93,9 @@ export async function prepareBmiStatusForUser( types: ["WEIGHT"], extra: { heightCm: user.heightCm }, }); - if (!force) { - const unchangedInput = await gateUnchangedStatusInput({ - userId, - cacheAction, - todayKey, - inputHash, - force, - }); - if (unchangedInput) { - return { - phase: "served", - result: { - hasProvider: true, - text: unchangedInput.text, - cached: true, - updatedAt: unchangedInput.updatedAt, - }, - }; - } + if (!ctx.force) { + const unchangedInput = await gateUnchangedInputs(ctx, inputHash, ctx.force); + if (unchangedInput) return { phase: "served", result: unchangedInput }; } // v1.4.28 — cap the snapshot input (weight runs at most once @@ -270,24 +152,13 @@ export async function prepareBmiStatusForUser( const previousBmi = bmiSeries.daily[1] ?? null; const latestClassification = latestBmi ? classifyBMI(latestBmi.value) : null; - const oldestMeasurement = - measurements.length > 0 ? measurements[0].measuredAt : null; - const newestMeasurement = - measurements.length > 0 - ? measurements[measurements.length - 1].measuredAt - : null; - const totalSpanDays = - oldestMeasurement && newestMeasurement - ? Math.round( - (newestMeasurement.getTime() - oldestMeasurement.getTime()) / - (24 * 60 * 60 * 1000), - ) - : 0; - const newestMeasurementDaysAgo = newestMeasurement - ? Math.round( - (Date.now() - newestMeasurement.getTime()) / (24 * 60 * 60 * 1000), - ) - : null; + const { totalSpanDays, newestDaysAgo: newestMeasurementDaysAgo } = + coverageSpan( + measurements.length > 0 ? measurements[0].measuredAt : null, + measurements.length > 0 + ? measurements[measurements.length - 1].measuredAt + : null, + ); // v1.18.10 — hand the model the finished recent-vs-baseline // comparison + normal-swing verdict instead of asking it to derive them. @@ -333,152 +204,40 @@ export async function prepareBmiStatusForUser( }, }; - const shed = degradeStatusSnapshotToBudget( - snapshot as unknown as Record, - { maxChars: await statusSnapshotCharsForRecord(userId) }, - ); - const snapshotJson = JSON.stringify(snapshot); - - annotate({ - action: { name: cacheAction }, - meta: { - payload_size_bytes: snapshotJson.length, - ...(shed.length > 0 ? { snapshot_shed: shed } : {}), - }, - }); - - // Content-hash gate (v1.16.8): when the snapshot is unchanged since the - // last real assessment, refresh the cache timestamp and skip the LLM. - const snapshotHash = hashInsightSnapshot(snapshot); - const unchanged = await refreshUnchangedStatusInsight({ - userId, - cacheAction, - todayKey, - snapshotHash, - }); - if (unchanged) { - return { - phase: "served", - result: { - hasProvider: true, - text: unchanged.text, - cached: true, - updatedAt: unchanged.updatedAt, - }, - }; - } - - const previousContext = await getPreviousInsightContext( - userId, - "bmi-status", - locale, - 12, - ); - const previousContextBlock = formatPreviousContextForPrompt( - previousContext, - locale, - ); - - // v1.12.7 — diversity / anti-repetition block (see blood-pressure-status). - // BMI is a per-user height transform of WEIGHT (no discovery channel of its - // own), so the RELATIONS sub-block stays empty; the variety / data-strength - // / repetition signals carry the rotation. - const varietyLead = pickVarietyLead(userId, "bmi", todayKey); - const steadyRun = computeSteadyRun(bmiGraded.weekly, bmiGraded.monthly); - const assessmentContextBlock = buildAssessmentContextBlock( - { - varietyLead, - dataStrength: { + return { + snapshot, + inputHash, + finish: () => ({ + // BMI is a per-user height transform of WEIGHT (no discovery channel + // of its own), so the RELATIONS sub-block stays empty; the variety / + // data-strength / repetition signals carry the rotation. + context: { + repeatCount: computeSteadyRun(bmiGraded.weekly, bmiGraded.monthly), points: bmiSeries.daily.length, newestDaysAgo: newestMeasurementDaysAgo, + relationsFor: null, }, - repeatCount: steadyRun, - relations: [], - }, - locale, - ); - - // The provider call is deferred to the caller — the single-card path runs - // ONE completion, the batch path folds this prompt into one shared call. - // A timeout / error is a transient miss (fallback served, no assessment - // persisted); `none` (no provider / no consent) serves the no-key text. - return { - phase: "pending", - metric: "bmi", - userId, - cacheAction, - systemPrompt: getBmiSystemPrompt(locale), - userPrompt: getBmiUserPrompt( - snapshotJson, - todayKey, - locale, - previousContextBlock, - assessmentContextBlock, - // v1.28.40 — rotating opener hint, per (user, metric, day). - openerArchetypeHint(`${userId}:bmi:${todayKey}`, locale as Locale), - ), - snapshotHash, - // v1.12.7 — match the archetype cards' 0.45. - temperature: 0.45, - noProvider: { - hasProvider: false, - text: getNoKeyBmiStatusText(locale, bmiSignal), - cached: true, - updatedAt: null, - }, - timeout: (reason): StatusCardResult => - returnTimeoutFallback({ - cacheAction, - reason, - userId, - todayKey, - stubText: getNoKeyBmiStatusText(locale, bmiSignal), + fallbackText: getNoKeyBmiStatusText(locale, bmiSignal), + prompts: (snapshotJson, context) => ({ + systemPrompt: getBmiSystemPrompt(locale), + userPrompt: getBmiUserPrompt( + snapshotJson, + todayKey, + locale, + context.previousContextBlock, + context.assessmentContextBlock, + context.openerHint, + ), }), - finalize: async (outcome): Promise => { - // Outbound safety screen. The only transform here used to be - // whitespace-normalisation, so a dose-change imperative or a fabricated - // clinical risk score persisted as the day's cached assessment. Policy - // for a background-generated card is WITHHOLD: serve the deterministic - // stub and persist no model text (see `finalizeStatusSummary`). - const screened = finalizeStatusSummary(outcome.content, locale); - if (!screened.ok) { - annotate({ - action: { name: "insights.status.outbound_blocked" }, - meta: { cacheAction, reason: screened.reason }, - }); - return returnTimeoutFallback({ - cacheAction, - reason: "screened", - userId: userId, - todayKey: todayKey, - stubText: getNoKeyBmiStatusText(locale, bmiSignal), - }); - } - const summary = screened.text; - if (!summary) { - throw new Error("Bmi-status summary was empty after normalization"); - } - const updatedAt = await persistStatusInsight({ - userId, - cacheAction, - todayKey, - text: summary, - snapshotHash, - // v1.18.11 — persist the input fingerprint for tomorrow's gate. - inputHash, - }); - return { - hasProvider: true, - text: summary, - cached: false, - updatedAt, - }; - }, + detail: undefined, + }), }; } -export function resolveBmiStatusLocale( - locale: string | null | undefined, -): SupportedLocale { - return normalizeLocale(locale); -} +const BMI_STATUS_CARD: StatusCardPlugin = { + scope: "bmi", + codec: textStatusCodec, + unavailableText: (locale) => getNoKeyBmiStatusText(locale), + emptyMessage: "Bmi-status summary was empty after normalization", + build: buildBmiSnapshot, +}; diff --git a/src/lib/insights/feature-window.ts b/src/lib/insights/feature-window.ts new file mode 100644 index 000000000..1dac15e78 --- /dev/null +++ b/src/lib/insights/feature-window.ts @@ -0,0 +1,58 @@ +/** + * The window arithmetic the feature extractor and the signals-of-day builder + * share: a reading list as trend points, a mean over a days-ago window, and + * a spread rounded to the payload's one decimal. A leaf module, so the + * signals-of-day builder does not import the extractor that re-exports it. + */ +import type { DataPoint } from "@/lib/analytics/trends"; +import { readingsMean } from "@/lib/measurements/day-mean"; +import { usesHourlyMeanDay } from "@/lib/measurements/day-statistic"; +import { standardDeviation, type VarianceKind } from "@/lib/stats/spread"; + +/** + * A spread rounded to the payload's one decimal, `null` below two values: a + * single reading has no spread to report, not a spread of zero. + */ +export function roundedSpread( + values: readonly number[], + kind: VarianceKind, +): number | null { + if (values.length < 2) return null; + const sd = standardDeviation(values, kind); + return sd === null ? null : Math.round(sd * 10) / 10; +} + +export function toDataPoints( + records: Array<{ value: number; measuredAt: Date }>, +): DataPoint[] { + return records.map((r) => ({ date: r.measuredAt, value: r.value })); +} + +/** + * Compute average of values within a time window (days ago from now). + * + * `day` names the type and the zone its days are read in. For a type whose + * day is the mean of its hours' means (pulse, see `day-mean.ts`) the window + * is then the mean of its day values; every other type keeps the plain mean. + */ +export function avgInWindow( + records: Array<{ value: number; measuredAt: Date }>, + now: number, + fromDaysAgo: number, + toDaysAgo: number = 0, + day?: { type: string; tz: string }, +): number | null { + const fromMs = now - fromDaysAgo * 24 * 60 * 60 * 1000; + const toMs = now - toDaysAgo * 24 * 60 * 60 * 1000; + const filtered = records.filter((r) => { + const t = r.measuredAt.getTime(); + return t >= fromMs && t <= toMs; + }); + if (filtered.length === 0) return null; + if (day && usesHourlyMeanDay(day.type)) { + const mean = readingsMean(day.type, filtered, day.tz); + return mean === null ? null : Math.round(mean * 100) / 100; + } + const sum = filtered.reduce((s, r) => s + r.value, 0); + return Math.round((sum / filtered.length) * 100) / 100; +} diff --git a/src/lib/insights/features.ts b/src/lib/insights/features.ts index 18a7cbc3e..9d5bbc908 100644 --- a/src/lib/insights/features.ts +++ b/src/lib/insights/features.ts @@ -21,7 +21,6 @@ import { } from "@/lib/tz/format"; import { readMoodDailyPoints } from "@/lib/insights/mood-daily-points"; import { readingsMean } from "@/lib/measurements/day-mean"; -import { usesHourlyMeanDay } from "@/lib/measurements/day-statistic"; import { isCurrentForTodayClaim } from "@/lib/insights/measurement-freshness"; import { computeMoodDimensionSeries } from "@/lib/insights/mood-dimension-series"; import { computeContextMoodComparison } from "@/lib/insights/mood-context-crosstab"; @@ -35,11 +34,12 @@ import { import { loadUserSourcePriority } from "@/lib/rollups/measurement-read"; import { getBpTargets } from "@/lib/analytics/bp-targets"; import { isBpReadingInTarget } from "@/lib/analytics/bp-in-target"; +import { READING_SPREAD } from "@/lib/stats/spread"; import { - READING_SPREAD, - standardDeviation, - type VarianceKind, -} from "@/lib/stats/spread"; + avgInWindow, + roundedSpread, + toDataPoints, +} from "@/lib/insights/feature-window"; import { pairByTimestamp, significantPearsonCorrelation, @@ -900,25 +900,6 @@ export class FeaturesPayloadTooLargeError extends Error { } } -/** - * A spread rounded to the payload's one decimal, `null` below two values: a - * single reading has no spread to report, not a spread of zero. - */ -export function roundedSpread( - values: readonly number[], - kind: VarianceKind, -): number | null { - if (values.length < 2) return null; - const sd = standardDeviation(values, kind); - return sd === null ? null : Math.round(sd * 10) / 10; -} - -export function toDataPoints( - records: Array<{ value: number; measuredAt: Date }>, -): DataPoint[] { - return records.map((r) => ({ date: r.measuredAt, value: r.value })); -} - function computeCoverage( records: Array<{ measuredAt: Date }>, now: number, @@ -948,35 +929,6 @@ function computeCoverage( }; } -/** - * Compute average of values within a time window (days ago from now). - * - * `day` names the type and the zone its days are read in. For a type whose - * day is the mean of its hours' means (pulse, see `day-mean.ts`) the window - * is then the mean of its day values; every other type keeps the plain mean. - */ -export function avgInWindow( - records: Array<{ value: number; measuredAt: Date }>, - now: number, - fromDaysAgo: number, - toDaysAgo: number = 0, - day?: { type: string; tz: string }, -): number | null { - const fromMs = now - fromDaysAgo * 24 * 60 * 60 * 1000; - const toMs = now - toDaysAgo * 24 * 60 * 60 * 1000; - const filtered = records.filter((r) => { - const t = r.measuredAt.getTime(); - return t >= fromMs && t <= toMs; - }); - if (filtered.length === 0) return null; - if (day && usesHourlyMeanDay(day.type)) { - const mean = readingsMean(day.type, filtered, day.tz); - return mean === null ? null : Math.round(mean * 100) / 100; - } - const sum = filtered.reduce((s, r) => s + r.value, 0); - return Math.round((sum / filtered.length) * 100) / 100; -} - // ─── v1.22 — cross-signal integration blocks (briefing-scoped) ──────────── /** diff --git a/src/lib/insights/general-status.ts b/src/lib/insights/general-status.ts index 1bb525a4e..9612b8975 100644 --- a/src/lib/insights/general-status.ts +++ b/src/lib/insights/general-status.ts @@ -5,48 +5,28 @@ import { getGeneralStatusSystemPrompt, getGeneralStatusUserPrompt, } from "@/lib/ai/prompts/general-status"; -import { openerArchetypeHint } from "@/lib/ai/prompts/opener-archetype"; -import type { Locale } from "@/lib/i18n/config"; import { getBpTargets } from "@/lib/analytics/bp-targets"; import { isBpReadingInTarget } from "@/lib/analytics/bp-in-target"; import { getNoKeyGeneralStatusText } from "@/lib/insights/no-key-fallbacks"; import { measurementTypeEnum } from "@/lib/validations/measurement"; -import { - formatPreviousContextForPrompt, - getPreviousInsightContext, -} from "@/lib/insights/memory"; -import { - buildAssessmentContextBlock, - pickVarietyLead, -} from "@/lib/insights/assessment-context"; import { applyPayloadBudget } from "@/lib/insights/bucket-series"; -import { - buildGradedSeriesFromPoints, - degradeStatusSnapshotToBudget, -} from "@/lib/insights/graded-series"; -import { statusSnapshotCharsForRecord } from "@/lib/ai/capabilities/usage"; -import { - normalizeLocale, - finalizeStatusSummary, - persistStatusInsight, - round, - summarizeSeries, -} from "@/lib/insights/status-shared"; -import { - readFreshStatusText, - refreshUnchangedStatusInsight, - resolveReadOnlyStatusMiss, - statusCacheAction, -} from "@/lib/insights/status-cache"; -import { hashInsightSnapshot } from "@/lib/insights/snapshot-hash"; -import { returnTimeoutFallback } from "@/lib/insights/timeout-fallback"; +import { buildGradedSeriesFromPoints } from "@/lib/insights/graded-series"; +import { round, summarizeSeries } from "@/lib/insights/status-shared"; import { runPreparedStatusCard, type PreparedStatusCard, - type StatusCardResult, } from "@/lib/insights/status-card-generation"; -import { annotate } from "@/lib/logging/context"; -import { resolveUserTimezone, userDayKey } from "@/lib/tz/resolver"; +import { + coverageSpan, + prepareStatusCard, + textStatusCodec, + type StatusCardBuild, + type StatusCardContext, + type StatusCardOptions, + type StatusCardPlugin, + type StatusTextResult, +} from "@/lib/insights/status-pipeline"; +import { userDayKey } from "@/lib/tz/resolver"; import { TRACKED_INTAKE_EVENT_WHERE } from "@/lib/medications/intake-tracking"; // Derived from canonical enum so a new measurement type is auto-included @@ -63,106 +43,31 @@ const MEASUREMENT_TYPES = measurementTypeEnum.options; export async function generateGeneralStatusForUser( userId: string, - options?: { - locale?: string | null; - force?: boolean; - /** v1.8.3 — read-only navigation path; see weight-status for the rationale. */ - readOnly?: boolean; - }, -): Promise<{ - hasProvider: boolean; - text: string | null; - cached: boolean; - updatedAt: string | null; - preparing?: boolean; - /** v1.9.0 — last-good text served while a refresh is in flight; keep polling. */ - revalidating?: boolean; -}> { + options?: StatusCardOptions, +): Promise { const prepared = await prepareGeneralStatusForUser(userId, options); - const result = await runPreparedStatusCard(prepared); - return result as { - hasProvider: boolean; - text: string | null; - cached: boolean; - updatedAt: string | null; - preparing?: boolean; - revalidating?: boolean; - }; + return (await runPreparedStatusCard(prepared)) as StatusTextResult; } -export async function prepareGeneralStatusForUser( +/** + * Everything up to (not including) the provider call, through the shared + * status pipeline (`status-pipeline.ts`); this card supplies its snapshot. + */ +export function prepareGeneralStatusForUser( userId: string, - options?: { - locale?: string | null; - force?: boolean; - /** v1.8.3 — read-only navigation path; see weight-status for the rationale. */ - readOnly?: boolean; - }, + options?: StatusCardOptions, ): Promise { - const locale = normalizeLocale(options?.locale); - const force = options?.force === true; - const readOnly = options?.readOnly === true; - const cacheAction = statusCacheAction("general", locale); - // v1.30.3 — resolve the user's own tz BEFORE the day-key so the - // cache rolls over at the user's local midnight, not Berlin's. Has to - // happen ahead of the cache read below (the earliest possible return - // path), not just the later day-bucketing reads. - const userTz = await resolveUserTimezone(userId); - const todayKey = userDayKey(new Date(), userTz); - - const cached = await readFreshStatusText({ - userId, - cacheAction, - todayKey, - force, - }); - if (cached) { - return { - phase: "served", - result: { - hasProvider: true, - text: cached.text, - cached: true, - updatedAt: cached.updatedAt, - }, - }; - } - - if (readOnly) { - const outcome = await resolveReadOnlyStatusMiss({ - userId, - metric: "general", - locale, - }); - // Unavailable for any reason serves the deterministic line and enqueues - // nothing. `hasProvider` is provider presence only. - if (outcome.kind === "unavailable") { - return { - phase: "served", - result: { - hasProvider: outcome.hasProvider, - text: getNoKeyGeneralStatusText(locale), - cached: true, - updatedAt: null, - }, - }; - } - // v1.8.7 — stale-while-revalidate: serve the last good assessment - // (if any) instantly while the worker re-warms; only fall to the empty - // preparing skeleton when none was ever produced. - return { - phase: "served", - result: { - hasProvider: true, - text: outcome.lastGood?.text ?? null, - cached: outcome.lastGood !== null, - updatedAt: outcome.lastGood?.updatedAt ?? null, - preparing: outcome.lastGood === null, - revalidating: outcome.revalidating, - }, - }; - } + return prepareStatusCard(GENERAL_STATUS_CARD, userId, options); +} +/** + * The overview snapshot: every measured type as a summary + graded series, + * medication adherence, the BP targets, and mood context. + */ +async function buildGeneralSnapshot( + ctx: StatusCardContext, +): Promise> { + const { userId, locale, userTz, todayKey } = ctx; const user = await prisma.user.findUnique({ where: { id: userId }, select: { @@ -222,6 +127,114 @@ export async function prepareGeneralStatusForUser( }), ); + const { adherenceSeries, adherenceGraded } = await readAdherenceSeries( + userId, + now, + userTz, + ); + + // Fetch mood context (optional — for enrichment only). v1.4.28 + // Cap at 90 entries. + const moodEntries = await readRecentMoodScores(userId, 90); + + const moodRecords = moodEntries.map((entry) => ({ + measuredAt: entry.moodLoggedAt, + value: entry.score, + })); + const moodSeries = applyPayloadBudget(moodRecords, { now, tz: userTz }); + const moodGraded = buildGradedSeriesFromPoints(moodRecords, now, userTz); + const moodSummary = summarizeSeries( + moodSeries.daily.map((bucket) => ({ value: bucket.value })), + ); + const moodMean = moodSummary?.mean ?? null; + + const bpTargets = getBpTargets(user?.dateOfBirth ?? null); + const bpInTargetLast30Days = bpTargets + ? bpInTargetLast30DailyPoints(dailyByType, bpTargets) + : null; + + // Compute overall data coverage info + const { totalSpanDays, newestDaysAgo } = coverageSpan( + measurements.length > 0 ? measurements[0].measuredAt : null, + measurements.length > 0 + ? measurements[measurements.length - 1].measuredAt + : null, + ); + + const snapshot = { + locale, + generatedForDay: todayKey, + interpretationHint: + "Use trend direction and deltas. Prioritize the newest data if trends conflict. Consider dataCoverage for reliability assessment.", + dataCoverage: { + totalMeasurements: measurements.length, + totalSpanDays, + newestMeasurementDaysAgo: newestDaysAgo, + avgDaysBetweenMeasurements: + measurements.length > 1 + ? Math.round((totalSpanDays / (measurements.length - 1)) * 10) / 10 + : null, + }, + measurementSeries, + medicationAdherence: { + summary: summarizeSeries( + adherenceSeries.daily.map((bucket) => ({ value: bucket.value })), + ), + series: adherenceGraded, + }, + bloodPressureTargets: bpTargets + ? { + systolic: { min: bpTargets.sysLow, max: bpTargets.sysHigh }, + diastolic: { min: bpTargets.diaLow, max: bpTargets.diaHigh }, + inTargetPctLast30DailyPoints: bpInTargetLast30Days, + } + : null, + moodContext: + moodSeries.daily.length >= 3 + ? { + points: moodSeries.daily.length, + mean: moodMean, + latest: moodSeries.daily[0]?.value ?? null, + series: moodGraded, + } + : null, + }; + + return { + snapshot, + finish: () => ({ + // The overview spans many metrics with no single graded mean to band, + // so the steady-run signal is left to the previous-context comparison + // (repeatCount 0) and there is no single discovery channel (relations + // empty); the variety lead + overall data-strength carry the rotation. + context: { + repeatCount: 0, + points: measurements.length, + newestDaysAgo, + relationsFor: null, + }, + fallbackText: getNoKeyGeneralStatusText(locale), + prompts: (snapshotJson, context) => ({ + systemPrompt: getGeneralStatusSystemPrompt(locale), + userPrompt: getGeneralStatusUserPrompt( + snapshotJson, + todayKey, + locale, + context.previousContextBlock, + context.assessmentContextBlock, + context.openerHint, + ), + }), + detail: undefined, + }), + }; +} + +/** + * Medication adherence as one rate per user-local day, bucketed and graded + * so the model gets the canonical 360+24 view. + */ +async function readAdherenceSeries(userId: string, now: Date, userTz: string) { // v1.12.7 — bound the intake read to the ~365-day window the adherence // series renders. Without a `scheduledFor` floor this scanned all intake // history; the adherence fold below only consumes the trailing year. @@ -292,250 +305,50 @@ export async function prepareGeneralStatusForUser( userTz, ); - // Fetch mood context (optional — for enrichment only). v1.4.28 - // Cap at 90 entries. - const moodEntries = await readRecentMoodScores(userId, 90); - - const moodRecords = moodEntries.map((entry) => ({ - measuredAt: entry.moodLoggedAt, - value: entry.score, - })); - const moodSeries = applyPayloadBudget(moodRecords, { now, tz: userTz }); - const moodGraded = buildGradedSeriesFromPoints(moodRecords, now, userTz); - const moodSummary = summarizeSeries( - moodSeries.daily.map((bucket) => ({ value: bucket.value })), - ); - const moodMean = moodSummary?.mean ?? null; - - const bpTargets = getBpTargets(user?.dateOfBirth ?? null); - let bpInTargetLast30Days: number | null = null; - if (bpTargets) { - const sysDaily = dailyByType.get("BLOOD_PRESSURE_SYS")?.daily ?? []; - const diaMap = new Map( - (dailyByType.get("BLOOD_PRESSURE_DIA")?.daily ?? []).map( - (bucket) => [bucket.dayOffset, bucket.value] as const, - ), - ); - const paired = sysDaily - .map((bucket) => { - const dia = diaMap.get(bucket.dayOffset); - if (dia == null) return null; - return { dayOffset: bucket.dayOffset, sys: bucket.value, dia }; - }) - .filter( - (entry): entry is { dayOffset: number; sys: number; dia: number } => - !!entry, - ) - // Keep the legacy "last 30 daily points" semantics — newest 30 - // paired daily buckets (dayOffset 0..29). - .filter((point) => point.dayOffset < 30); - - if (paired.length > 0) { - const inTargetCount = paired.filter((point) => - // v1.4.16 — one-sided ceiling semantics with hypotension - // floor. See lib/analytics/bp-in-target.ts. - isBpReadingInTarget(point.sys, point.dia, bpTargets), - ).length; - bpInTargetLast30Days = round((inTargetCount / paired.length) * 100, 1); - } - } - - // Compute overall data coverage info - const oldestDay = measurements.length > 0 ? measurements[0].measuredAt : null; - const newestDay = - measurements.length > 0 - ? measurements[measurements.length - 1].measuredAt - : null; - const totalSpanDays = - oldestDay && newestDay - ? Math.round( - (newestDay.getTime() - oldestDay.getTime()) / (24 * 60 * 60 * 1000), - ) - : 0; - const newestDaysAgo = newestDay - ? Math.round((Date.now() - newestDay.getTime()) / (24 * 60 * 60 * 1000)) - : null; - - const snapshot = { - locale, - generatedForDay: todayKey, - interpretationHint: - "Use trend direction and deltas. Prioritize the newest data if trends conflict. Consider dataCoverage for reliability assessment.", - dataCoverage: { - totalMeasurements: measurements.length, - totalSpanDays, - newestMeasurementDaysAgo: newestDaysAgo, - avgDaysBetweenMeasurements: - measurements.length > 1 - ? Math.round((totalSpanDays / (measurements.length - 1)) * 10) / 10 - : null, - }, - measurementSeries, - medicationAdherence: { - summary: summarizeSeries( - adherenceSeries.daily.map((bucket) => ({ value: bucket.value })), - ), - series: adherenceGraded, - }, - bloodPressureTargets: bpTargets - ? { - systolic: { min: bpTargets.sysLow, max: bpTargets.sysHigh }, - diastolic: { min: bpTargets.diaLow, max: bpTargets.diaHigh }, - inTargetPctLast30DailyPoints: bpInTargetLast30Days, - } - : null, - moodContext: - moodSeries.daily.length >= 3 - ? { - points: moodSeries.daily.length, - mean: moodMean, - latest: moodSeries.daily[0]?.value ?? null, - series: moodGraded, - } - : null, - }; - - const shed = degradeStatusSnapshotToBudget( - snapshot as unknown as Record, - { maxChars: await statusSnapshotCharsForRecord(userId) }, - ); - const snapshotJson = JSON.stringify(snapshot); - - annotate({ - action: { name: cacheAction }, - meta: { - payload_size_bytes: snapshotJson.length, - ...(shed.length > 0 ? { snapshot_shed: shed } : {}), - }, - }); - - // Content-hash gate (v1.16.8): when the snapshot is unchanged since the - // last real assessment, refresh the cache timestamp and skip the LLM. - const snapshotHash = hashInsightSnapshot(snapshot); - const unchanged = await refreshUnchangedStatusInsight({ - userId, - cacheAction, - todayKey, - snapshotHash, - }); - if (unchanged) { - return { - phase: "served", - result: { - hasProvider: true, - text: unchanged.text, - cached: true, - updatedAt: unchanged.updatedAt, - }, - }; - } - - // v1.4: pull the previous cached general-status into the prompt so - // the model can compare to the user's last analysis. Falls back - // gracefully when there's no history (first-run users). - const previousContext = await getPreviousInsightContext( - userId, - "general-status", - locale, - 12, - ); - const previousContextBlock = formatPreviousContextForPrompt( - previousContext, - locale, - ); - - // v1.12.7 — diversity / anti-repetition block (see blood-pressure-status). - // The overview spans many metrics with no single graded mean to band, so - // the steady-run signal is left to the previous-context comparison - // (repeatCount 0) and there is no single discovery channel (relations - // empty); the variety lead + overall data-strength carry the rotation. - const varietyLead = pickVarietyLead(userId, "general", todayKey); - const assessmentContextBlock = buildAssessmentContextBlock( - { - varietyLead, - dataStrength: { - points: measurements.length, - newestDaysAgo, - }, - repeatCount: 0, - relations: [], - }, - locale, - ); - - // The provider call is deferred to the caller — the single-card path runs - // ONE completion, the batch path folds this prompt into one shared call. - // A timeout / error is a transient miss (fallback served, no assessment - // persisted); `none` (no provider / no consent) serves the no-key text. - return { - phase: "pending", - metric: "general", - userId, - cacheAction, - systemPrompt: getGeneralStatusSystemPrompt(locale), - userPrompt: getGeneralStatusUserPrompt( - snapshotJson, - todayKey, - locale, - previousContextBlock, - assessmentContextBlock, - // v1.28.40 — rotating opener hint, per (user, metric, day). - openerArchetypeHint(`${userId}:general:${todayKey}`, locale as Locale), - ), - snapshotHash, - // v1.12.7 — match the archetype cards' 0.45. - temperature: 0.45, - noProvider: { - hasProvider: false, - text: getNoKeyGeneralStatusText(locale), - cached: true, - updatedAt: null, - }, - timeout: (reason): StatusCardResult => - returnTimeoutFallback({ - cacheAction, - reason, - userId, - todayKey, - stubText: getNoKeyGeneralStatusText(locale), - }), - finalize: async (outcome): Promise => { - // Outbound safety screen. The only transform here used to be - // whitespace-normalisation, so a dose-change imperative or a fabricated - // clinical risk score persisted as the day's cached assessment. Policy - // for a background-generated card is WITHHOLD: serve the deterministic - // stub and persist no model text (see `finalizeStatusSummary`). - const screened = finalizeStatusSummary(outcome.content, locale); - if (!screened.ok) { - annotate({ - action: { name: "insights.status.outbound_blocked" }, - meta: { cacheAction, reason: screened.reason }, - }); - return returnTimeoutFallback({ - cacheAction, - reason: "screened", - userId: userId, - todayKey: todayKey, - stubText: getNoKeyGeneralStatusText(locale), - }); - } - const summary = screened.text; - if (!summary) { - throw new Error("General-status summary was empty after normalization"); - } - const updatedAt = await persistStatusInsight({ - userId, - cacheAction, - todayKey, - text: summary, - snapshotHash, - }); - return { - hasProvider: true, - text: summary, - cached: false, - updatedAt, - }; - }, - }; + return { adherenceSeries, adherenceGraded }; } + +/** + * The share of the newest 30 paired systolic / diastolic daily buckets that + * sit in the person's BP targets, or `null` with no paired day. + */ +function bpInTargetLast30DailyPoints( + dailyByType: ReadonlyMap>, + bpTargets: NonNullable>, +): number | null { + const sysDaily = dailyByType.get("BLOOD_PRESSURE_SYS")?.daily ?? []; + const diaMap = new Map( + (dailyByType.get("BLOOD_PRESSURE_DIA")?.daily ?? []).map( + (bucket) => [bucket.dayOffset, bucket.value] as const, + ), + ); + const paired = sysDaily + .map((bucket) => { + const dia = diaMap.get(bucket.dayOffset); + if (dia == null) return null; + return { dayOffset: bucket.dayOffset, sys: bucket.value, dia }; + }) + .filter( + (entry): entry is { dayOffset: number; sys: number; dia: number } => + !!entry, + ) + // Keep the legacy "last 30 daily points" semantics — newest 30 + // paired daily buckets (dayOffset 0..29). + .filter((point) => point.dayOffset < 30); + + if (paired.length === 0) return null; + const inTargetCount = paired.filter((point) => + // v1.4.16 — one-sided ceiling semantics with hypotension + // floor. See lib/analytics/bp-in-target.ts. + isBpReadingInTarget(point.sys, point.dia, bpTargets), + ).length; + return round((inTargetCount / paired.length) * 100, 1); +} + +const GENERAL_STATUS_CARD: StatusCardPlugin = { + scope: "general", + codec: textStatusCodec, + unavailableText: (locale) => getNoKeyGeneralStatusText(locale), + emptyMessage: "General-status summary was empty after normalization", + build: buildGeneralSnapshot, +}; diff --git a/src/lib/insights/medication-compliance-status.ts b/src/lib/insights/medication-compliance-status.ts index e944508e1..a71719ec9 100644 --- a/src/lib/insights/medication-compliance-status.ts +++ b/src/lib/insights/medication-compliance-status.ts @@ -3,8 +3,6 @@ import { getMedicationComplianceSystemPrompt, getMedicationComplianceUserPrompt, } from "@/lib/ai/prompts/medication-compliance"; -import { openerArchetypeHint } from "@/lib/ai/prompts/opener-archetype"; -import type { Locale } from "@/lib/i18n/config"; import { buildComplianceMedicationContext, COURSES_COMPLIANCE_SELECT, @@ -14,44 +12,37 @@ import { lastNonSkippedTakenAt, SCHEDULE_COMPLIANCE_SELECT, } from "@/lib/analytics/compliance"; -import { resolveUserTimezone, userDayKey } from "@/lib/tz/resolver"; import { getMedicationCategories } from "@/lib/medication-category"; import { sanitizeForPrompt } from "@/lib/insights/sanitize"; import { getNoKeyMedicationComplianceStatusText } from "@/lib/insights/no-key-fallbacks"; -import { - formatPreviousContextForPrompt, - getPreviousInsightContext, -} from "@/lib/insights/memory"; -import { - buildAssessmentContextBlock, - pickVarietyLead, -} from "@/lib/insights/assessment-context"; import { applyPayloadBudget } from "@/lib/insights/bucket-series"; import { buildGradedSeriesFromPoints } from "@/lib/insights/graded-series"; -import { statusSnapshotCharsForRecord } from "@/lib/ai/capabilities/usage"; import { buildMetricSignal } from "@/lib/insights/metric-signal"; -import { degradeStatusSnapshotToBudget } from "@/lib/insights/graded-series"; import { - type SupportedLocale, - finalizeStatusSummary, - normalizeLocale, normalizeSummaryText, round, + type SupportedLocale, } from "@/lib/insights/status-shared"; import { readFreshStatusItems, refreshUnchangedStatusItems, - resolveReadOnlyStatusMiss, - statusCacheAction, writeStatusNote, } from "@/lib/insights/status-cache"; -import { hashInsightSnapshot } from "@/lib/insights/snapshot-hash"; import { returnTimeoutFallback } from "@/lib/insights/timeout-fallback"; import { runPreparedStatusCard, type PreparedStatusCard, } from "@/lib/insights/status-card-generation"; -import { annotate } from "@/lib/logging/context"; +import { + coverageSpan, + prepareStatusCard, + type ServedBuild, + type StatusCardBuild, + type StatusCardContext, + type StatusCardOptions, + type StatusCardPlugin, + type StatusNoteCodec, +} from "@/lib/insights/status-pipeline"; import { TRACKED_INTAKE_WHERE } from "@/lib/medications/intake-tracking"; // 360 daily days + 24 monthly windows ≈ 1080 days of intake history. @@ -80,7 +71,7 @@ function parseMedicationItems(items: unknown[]): MedicationSummaryItem[] { * status result. It flows through the shared `PreparedStatusCard` union via * the open index signature on `StatusCardResult`. */ -interface MedicationComplianceResult { +type MedicationComplianceResult = { hasProvider: boolean; summary: string | null; medications: MedicationSummaryItem[]; @@ -88,7 +79,7 @@ interface MedicationComplianceResult { updatedAt: string | null; preparing?: boolean; revalidating?: boolean; -} +}; /** * Public entry — unchanged signature. Prepares the card then runs ONE @@ -97,11 +88,7 @@ interface MedicationComplianceResult { */ export async function generateMedicationComplianceStatusForUser( userId: string, - options?: { - locale?: string | null; - force?: boolean; - readOnly?: boolean; - }, + options?: StatusCardOptions, ): Promise { const prepared = await prepareMedicationComplianceStatusForUser( userId, @@ -112,114 +99,157 @@ export async function generateMedicationComplianceStatusForUser( } /** - * v1.18.7 — everything up to (not including) the provider call. - * Carries the richer `{ summary, medications }` result through the shared - * prepare/run/finalize contract. See `status-card-generation.ts`. + * Everything up to (not including) the provider call, through the shared + * status pipeline (`status-pipeline.ts`). Carries the richer + * `{ summary, medications }` result through its own note codec. */ -export async function prepareMedicationComplianceStatusForUser( +export function prepareMedicationComplianceStatusForUser( userId: string, - options?: { - locale?: string | null; - force?: boolean; - /** v1.8.3 — read-only navigation path; see weight-status for the rationale. */ - readOnly?: boolean; - }, + options?: StatusCardOptions, ): Promise { - const locale = normalizeLocale(options?.locale); - const force = options?.force === true; - const readOnly = options?.readOnly === true; - const cacheAction = statusCacheAction("medication-compliance", locale); - // v1.30.3 — resolve the user's own tz BEFORE the day-key so the - // cache rolls over at the user's local midnight, not Berlin's. Has to - // happen ahead of the cache read below (the earliest possible return - // path), not just the later day-bucketing reads. - const userTz = await resolveUserTimezone(userId); - const todayKey = userDayKey(new Date(), userTz); + return prepareStatusCard(MEDICATION_COMPLIANCE_STATUS_CARD, userId, options); +} - // This card stores per-medication lines next to its summary, so it reads - // its note through the items variant of the shared reader. - if (!force) { +/** + * This card stores per-medication lines next to its summary, so it reads and + * re-dates its note through the items variants of the shared readers, and + * its result carries the lines as `medications`. + */ +const medicationComplianceCodec: StatusNoteCodec< + MedicationComplianceResult, + MedicationSummaryItem[] +> = { + async readFresh(ctx) { + if (ctx.force) return null; const cached = await readFreshStatusItems({ - userId, - cacheAction, - todayKey, + userId: ctx.userId, + cacheAction: ctx.cacheAction, + todayKey: ctx.todayKey, }); - if (cached && Array.isArray(cached.items)) { - return { - phase: "served", - result: { - hasProvider: true, - summary: cached.text, - medications: parseMedicationItems(cached.items), - cached: true, - updatedAt: cached.updatedAt, - }, - }; - } - } - - // v1.8.3 — read-only navigation path: never block on the provider. - // Enqueue generation out of band and return preparing / no-provider. - if (readOnly) { - const outcome = await resolveReadOnlyStatusMiss({ - userId, - metric: "medication-compliance", - locale, + if (!cached || !Array.isArray(cached.items)) return null; + return { + hasProvider: true, + summary: cached.text, + medications: parseMedicationItems(cached.items), + cached: true, + updatedAt: cached.updatedAt, + }; + }, + async refreshUnchanged(ctx, snapshotHash) { + const unchanged = await refreshUnchangedStatusItems({ + userId: ctx.userId, + cacheAction: ctx.cacheAction, + todayKey: ctx.todayKey, + snapshotHash, }); + if (!unchanged || !Array.isArray(unchanged.items)) return null; + return { + hasProvider: true, + summary: unchanged.text, + medications: parseMedicationItems(unchanged.items), + cached: true, + updatedAt: unchanged.updatedAt, + }; + }, + readOnly(outcome, unavailableText) { // Unavailable for any reason: the deterministic line, no enqueue. // `hasProvider` is provider presence only. if (outcome.kind === "unavailable") { return { - phase: "served", - result: { - hasProvider: outcome.hasProvider, - summary: getNoKeyMedicationComplianceStatusText(locale), - medications: [], - cached: true, - updatedAt: null, - }, + hasProvider: outcome.hasProvider, + summary: unavailableText, + medications: [], + cached: true, + updatedAt: null, }; } - // v1.8.7 — stale-while-revalidate: serve the last good narrative (if - // any) while the worker re-warms; only show the empty preparing - // skeleton when none was ever produced. + // Stale-while-revalidate: serve the last good narrative (if any) while + // the worker re-warms; only show the empty preparing skeleton when none + // was ever produced. return { - phase: "served", - result: { - hasProvider: true, - summary: outcome.lastGood?.text ?? null, - medications: [], - cached: outcome.lastGood !== null, - updatedAt: outcome.lastGood?.updatedAt ?? null, - preparing: outcome.lastGood === null, - revalidating: outcome.revalidating, - }, + hasProvider: true, + summary: outcome.lastGood?.text ?? null, + medications: [], + cached: outcome.lastGood !== null, + updatedAt: outcome.lastGood?.updatedAt ?? null, + preparing: outcome.lastGood === null, + revalidating: outcome.revalidating, }; - } + }, + noProvider(text) { + return { + hasProvider: false, + summary: text, + medications: [], + cached: true, + updatedAt: null, + }; + }, + fallback(ctx, reason, stubText, medicationSummaries) { + // Transient miss or a withheld reply — write the short-TTL negative stub + // (so the read-only route does not re-enqueue while the provider is + // degraded) and serve the fallback for this render without persisting an + // assessment. A withheld reply still renders the per-medication rows. + returnTimeoutFallback({ + cacheAction: ctx.cacheAction, + reason, + userId: ctx.userId, + todayKey: ctx.todayKey, + stubText, + }); + return { + hasProvider: true, + summary: stubText, + medications: reason === "screened" ? medicationSummaries : [], + cached: true, + updatedAt: null, + }; + }, + async persist(ctx, summary, hashes, medicationSummaries) { + // The compliance prompt returns a single `{ summary }` envelope — it + // does not emit per-medication text. The per-medication cards carry a + // placeholder so the UI surfaces a row per active medication; the + // overall `summary` is the model-authored assessment. + const generatedAt = await writeStatusNote({ + userId: ctx.userId, + cacheAction: ctx.cacheAction, + todayKey: ctx.todayKey, + text: summary, + items: medicationSummaries, + snapshotHash: hashes.snapshotHash, + }); + return { + hasProvider: true, + summary, + medications: medicationSummaries, + cached: false, + updatedAt: generatedAt.toISOString(), + }; + }, +}; - const medicationRows = await prisma.medication.findMany({ - // v1.16.11 — as-needed (PRN) medications never surface a compliance - // rate (no expected doses). - where: { userId, active: true, asNeeded: false, ...TRACKED_INTAKE_WHERE }, - // v1.15.20 — schedules through the shared compliance select so the - // configured per-dose windows reach this surface like every other. - include: { - schedules: { select: SCHEDULE_COMPLIANCE_SELECT }, - // v1.16.3 — archived schedule eras for era-aware compliance. - scheduleRevisions: { orderBy: { validFrom: "asc" } }, - // v1.25 — pause eras so paused days drop out of the denominator. - pauseEras: { select: { pausedAt: true, resumedAt: true } }, - // v1.40 (#1024) — the courses, so a gap between two expects nothing. - courses: COURSES_COMPLIANCE_SELECT, - }, - orderBy: { name: "asc" }, - }); +type ComplianceMedication = Awaited< + ReturnType +>[number]; +type ComplianceIntakeEvent = { + medicationId: string; + scheduledFor: Date; + takenAt: Date | null; + skipped: boolean; +}; - // A medication with no schedule expects no dose, and `calculateCompliance` - // answers 100 % for that case. The `asNeeded: false` filter above catches - // only the PRN arm; the empty schedule is reachable on a scheduled - // medication and would otherwise report perfect adherence. - const medications = medicationRows.filter(expectsDoses); +/** + * The adherence snapshot: one block per active, scheduled medication with + * its compliance, streak and per-day series, and the overall averages. + */ +async function buildMedicationComplianceSnapshot( + ctx: StatusCardContext, +): Promise< + | StatusCardBuild + | ServedBuild +> { + const { userId, locale, userTz, todayKey } = ctx; + const medications = await readComplianceMedications(userId); if (medications.length === 0) { return { @@ -269,89 +299,16 @@ export async function prepareMedicationComplianceStatusForUser( // the canonical engine on the user's own tz. `userTz` was already // resolved above for the cache day-key. - const medicationSnapshots = medications.map((medication) => { - const events = medicationEvents.filter( - (event) => event.medicationId === medication.id, - ); - - const medicationContext = buildComplianceMedicationContext( + const medicationSnapshots = medications.map((medication) => + medicationSnapshot({ medication, - lastNonSkippedTakenAt(events), - userTz, - ); - // v1.18.0 — one shared cadence-aware expansion pass yields BOTH the - // canonical compliance7 / compliance30 (the values shown in this same - // snapshot) AND the unified dose-history ledger the per-day series is - // built from, so the series can never disagree with compliance30. - const bundle = buildMedicationComplianceBundle( - events, - medication.schedules, - medicationContext, + medicationEvents, + categoryMap, now, - ); - const compliance7 = bundle.compliance7; - const compliance30 = bundle.compliance30; - - // v1.18.0 — the per-day series honours the medication's cadence - // (daysOfWeek + intervalWeeks + rolling / RRULE / cyclic): only days the - // schedule actually expected a dose produce a point, and each day's rate - // uses the SAME taken / (taken + missed) tally as compliance30. The old - // path divided every event-day by `schedules.length`, treating every - // calendar day as fully dose-due — which made a weekly-cadence med's - // per-day series contradict its own compliance30. Reusing the canonical - // ledger fixes that by construction. - const perDayRecords = dailyComplianceRatesFromLedger( - bundle.ledgerRows, userTz, - ).map((day) => ({ - measuredAt: day.date, - value: round(day.rate, 1), - })); - - // `applyPayloadBudget` gives the latest-day focus; the compact - // graded series is what reaches the prompt. - const dailyBudgeted = applyPayloadBudget(perDayRecords, { - now, - tz: userTz, - }); - const dailySeries = buildGradedSeriesFromPoints(perDayRecords, now, userTz); - const latestDay = dailyBudgeted.daily[0] ?? null; - - // v1.18.10 — the finished recent-vs-baseline adherence comparison - // + normal-swing verdict, computed from the canonical per-day rate series - // so the model states the trend rather than deriving it. The compliance % - // itself stays server-computed (compliance7/30 above); this adds only the - // trend signal. Adherence reads higher-better; the band is 0–100%. - const signal = buildMetricSignal({ - metric: - locale === "en" - ? `adherence for ${sanitizeForPrompt(medication.name)}` - : `Einnahmetreue für ${sanitizeForPrompt(medication.name)}`, - unit: "%", - direction: "higher-better", - graded: dailySeries, - // Recency is surfaced separately via dataCoverage.newestMeasurementDaysAgo - // (computed after this map); the per-med signal leaves it null. - newestDaysAgo: null, - }); - - return { - medicationId: medication.id, - name: sanitizeForPrompt(medication.name), - dose: sanitizeForPrompt(medication.dose, 50), - category: categoryMap[medication.id] ?? "OTHER", - schedulesPerDay: medication.schedules.length, - compliance7: compliance7.rate, - compliance30: compliance30.rate, - streak: compliance7.streak, - taken7: compliance7.taken, - skipped7: compliance7.skipped, - missed7: compliance7.missed, - ...(signal ? { signal } : {}), - dailySeries, - latestDay, - }; - }); + locale, + }), + ); const avgCompliance30 = medicationSnapshots.length > 0 @@ -364,22 +321,13 @@ export async function prepareMedicationComplianceStatusForUser( ) : null; - const oldestEvent = - medicationEvents.length > 0 ? medicationEvents[0].scheduledFor : null; - const newestEvent = - medicationEvents.length > 0 - ? medicationEvents[medicationEvents.length - 1].scheduledFor - : null; - const totalSpanDays = - oldestEvent && newestEvent - ? Math.round( - (newestEvent.getTime() - oldestEvent.getTime()) / - (24 * 60 * 60 * 1000), - ) - : 0; - const newestMeasurementDaysAgo = newestEvent - ? Math.round((Date.now() - newestEvent.getTime()) / (24 * 60 * 60 * 1000)) - : null; + const { totalSpanDays, newestDaysAgo: newestMeasurementDaysAgo } = + coverageSpan( + medicationEvents.length > 0 ? medicationEvents[0].scheduledFor : null, + medicationEvents.length > 0 + ? medicationEvents[medicationEvents.length - 1].scheduledFor + : null, + ); const snapshot = { locale, @@ -404,206 +352,185 @@ export async function prepareMedicationComplianceStatusForUser( medications: medicationSnapshots, }; - const shed = degradeStatusSnapshotToBudget( - snapshot as unknown as Record, - { maxChars: await statusSnapshotCharsForRecord(userId) }, - ); - const snapshotJson = JSON.stringify(snapshot); - - annotate({ - action: { name: cacheAction }, - meta: { - payload_size_bytes: snapshotJson.length, - ...(shed.length > 0 ? { snapshot_shed: shed } : {}), - }, - }); - - // Content-hash gate (v1.16.8): when the snapshot is unchanged since the - // last real assessment, refresh the cache timestamp and skip the LLM. - // This card stores per-medication lines next to the summary, so it uses - // the items variant of the shared gate. - const snapshotHash = hashInsightSnapshot(snapshot); - const unchanged = await refreshUnchangedStatusItems({ - userId, - cacheAction, - todayKey, - snapshotHash, - }); - if (unchanged && Array.isArray(unchanged.items)) { - return { - phase: "served", - result: { - hasProvider: true, - summary: unchanged.text, - medications: parseMedicationItems(unchanged.items), - cached: true, - updatedAt: unchanged.updatedAt, - }, - }; - } - - const previousContext = await getPreviousInsightContext( - userId, - "medication-compliance-status", - locale, - 12, - ); - const previousContextBlock = formatPreviousContextForPrompt( - previousContext, - locale, - ); - - // v1.12.7 — diversity / anti-repetition block (see blood-pressure-status). - // Adherence spans multiple medications with no single card-level graded - // mean to band, so the steady-run signal is left to the previous-context - // comparison (repeatCount 0) and there is no measurement discovery channel - // (relations empty); the variety lead + data-strength carry the rotation. - const varietyLead = pickVarietyLead( - userId, - "medication-compliance", - todayKey, - ); - const assessmentContextBlock = buildAssessmentContextBlock( - { - varietyLead, - dataStrength: { + return { + snapshot, + finish: () => ({ + // Adherence spans multiple medications with no single card-level graded + // mean to band, so the steady-run signal is left to the previous-context + // comparison (repeatCount 0) and there is no measurement discovery + // channel (relations empty); the variety lead + data-strength carry the + // rotation. + context: { + repeatCount: 0, points: medicationEvents.length, newestDaysAgo: newestMeasurementDaysAgo, + relationsFor: null, }, - repeatCount: 0, - relations: [], - }, - locale, - ); - - // The per-medication placeholder rows the UI surfaces alongside the - // model-authored overall `summary`. Computed here so both `finalize` - // (success) reuses them; the model's compliance prompt returns only the - // single `{ summary }` envelope. - const medicationSummaries: MedicationSummaryItem[] = medicationSnapshots.map( - (medication) => ({ - medicationId: medication.medicationId, - text: normalizeSummaryText( - locale === "de" - ? `${medication.name}: Es liegen noch nicht genügend konsistente Detaildaten für eine belastbare Kurzbewertung vor.` - : `${medication.name}: There is currently not enough consistent detail data for a robust short assessment.`, - ), - }), - ); - - // The provider call is deferred to the caller — single-card path runs ONE - // completion; the batch path folds this prompt into one shared call. - return { - phase: "pending", - metric: "medication-compliance", - userId, - cacheAction, - systemPrompt: getMedicationComplianceSystemPrompt(locale), - userPrompt: getMedicationComplianceUserPrompt( - snapshotJson, - todayKey, - locale, - previousContextBlock, - assessmentContextBlock, - // v1.28.40 — rotating opener hint, per (user, metric, day). - openerArchetypeHint( - `${userId}:medication-compliance:${todayKey}`, - locale as Locale, - ), - ), - snapshotHash, - // v1.12.7 — match the archetype cards' 0.45. - temperature: 0.45, - noProvider: { - hasProvider: false, - summary: getNoKeyMedicationComplianceStatusText(locale), - medications: [], - cached: true, - updatedAt: null, - }, - timeout: (reason) => { - // Transient miss — write the short-TTL negative stub (so the read-only - // route does not re-enqueue while the provider is degraded) and serve - // the fallback for this render without persisting an assessment. - returnTimeoutFallback({ - cacheAction, - reason, - userId, - todayKey, - stubText: getNoKeyMedicationComplianceStatusText(locale), - }); - return { - hasProvider: true, - summary: getNoKeyMedicationComplianceStatusText(locale), - medications: [], - cached: true, - updatedAt: null, - }; - }, - finalize: async (outcome) => { - // The compliance prompt returns a single `{ summary }` envelope — it - // does not emit per-medication text. The per-medication cards carry a - // placeholder so the UI surfaces a row per active medication; the - // overall `summary` is the model-authored assessment. - // - // v1.32.20 — route the completion through the shared status chokepoint, - // so this card enforces the exact contracts its seven siblings do rather - // than only withholding unparseable envelopes. `finalizeStatusSummary` - // parses the `{ summary }` envelope, scrubs chart tokens + whitespace, - // then SCREENS the prose against `INSIGHTS_CONTRACTS` (dose / risk / - // causal). This is the highest-leverage surface for the outbound screen — - // a medication card is exactly where a dose-change imperative or a - // fabricated figure lands. WITHHOLD on any block or unparseable envelope: - // serve the deterministic no-key line, write a short negative stub, and - // persist no model text. The per-medication rows still render. - const finalized = finalizeStatusSummary(outcome.content, locale); - if (!finalized.ok) { - annotate({ - action: { name: "insights.status.outbound_blocked" }, - meta: { cacheAction, reason: finalized.reason }, - }); - returnTimeoutFallback({ - cacheAction, - reason: "screened", - userId, + fallbackText: getNoKeyMedicationComplianceStatusText(locale), + prompts: (snapshotJson, context) => ({ + systemPrompt: getMedicationComplianceSystemPrompt(locale), + userPrompt: getMedicationComplianceUserPrompt( + snapshotJson, todayKey, - stubText: getNoKeyMedicationComplianceStatusText(locale), - }); - return { - hasProvider: true, - summary: getNoKeyMedicationComplianceStatusText(locale), - medications: medicationSummaries, - cached: true, - updatedAt: null, - }; - } - const summary = finalized.text; - if (!summary) { - throw new Error( - "Medication-compliance-status summary was empty after normalization", - ); - } - const generatedAt = await writeStatusNote({ - userId, - cacheAction, - todayKey, - text: summary, - items: medicationSummaries, - snapshotHash, - }); - return { - hasProvider: true, - summary, - medications: medicationSummaries, - cached: false, - updatedAt: generatedAt.toISOString(), - }; - }, + locale, + context.previousContextBlock, + context.assessmentContextBlock, + context.openerHint, + ), + }), + detail: medicationPlaceholderLines(medicationSnapshots, locale), + }), }; } -export function resolveMedicationComplianceStatusLocale( - locale: string | null | undefined, -): SupportedLocale { - return normalizeLocale(locale); +/** The active, scheduled medications whose adherence the card reports. */ +async function readComplianceMedications(userId: string) { + const medicationRows = await prisma.medication.findMany({ + // v1.16.11 — as-needed (PRN) medications never surface a compliance + // rate (no expected doses). + where: { userId, active: true, asNeeded: false, ...TRACKED_INTAKE_WHERE }, + // v1.15.20 — schedules through the shared compliance select so the + // configured per-dose windows reach this surface like every other. + include: { + schedules: { select: SCHEDULE_COMPLIANCE_SELECT }, + // v1.16.3 — archived schedule eras for era-aware compliance. + scheduleRevisions: { orderBy: { validFrom: "asc" } }, + // v1.25 — pause eras so paused days drop out of the denominator. + pauseEras: { select: { pausedAt: true, resumedAt: true } }, + // v1.40 (#1024) — the courses, so a gap between two expects nothing. + courses: COURSES_COMPLIANCE_SELECT, + }, + orderBy: { name: "asc" }, + }); + + // A medication with no schedule expects no dose, and `calculateCompliance` + // answers 100 % for that case. The `asNeeded: false` filter above catches + // only the PRN arm; the empty schedule is reachable on a scheduled + // medication and would otherwise report perfect adherence. + return medicationRows.filter(expectsDoses); } + +/** One medication's block in the adherence snapshot. */ +function medicationSnapshot(args: { + medication: ComplianceMedication; + medicationEvents: ComplianceIntakeEvent[]; + categoryMap: Record; + now: Date; + userTz: string; + locale: SupportedLocale; +}) { + const { medication, medicationEvents, categoryMap, now, userTz, locale } = + args; + const events = medicationEvents.filter( + (event) => event.medicationId === medication.id, + ); + + const medicationContext = buildComplianceMedicationContext( + medication, + lastNonSkippedTakenAt(events), + userTz, + ); + // v1.18.0 — one shared cadence-aware expansion pass yields BOTH the + // canonical compliance7 / compliance30 (the values shown in this same + // snapshot) AND the unified dose-history ledger the per-day series is + // built from, so the series can never disagree with compliance30. + const bundle = buildMedicationComplianceBundle( + events, + medication.schedules, + medicationContext, + now, + ); + const compliance7 = bundle.compliance7; + const compliance30 = bundle.compliance30; + + // v1.18.0 — the per-day series honours the medication's cadence + // (daysOfWeek + intervalWeeks + rolling / RRULE / cyclic): only days the + // schedule actually expected a dose produce a point, and each day's rate + // uses the SAME taken / (taken + missed) tally as compliance30. The old + // path divided every event-day by `schedules.length`, treating every + // calendar day as fully dose-due — which made a weekly-cadence med's + // per-day series contradict its own compliance30. Reusing the canonical + // ledger fixes that by construction. + const perDayRecords = dailyComplianceRatesFromLedger( + bundle.ledgerRows, + userTz, + ).map((day) => ({ + measuredAt: day.date, + value: round(day.rate, 1), + })); + + // `applyPayloadBudget` gives the latest-day focus; the compact + // graded series is what reaches the prompt. + const dailyBudgeted = applyPayloadBudget(perDayRecords, { + now, + tz: userTz, + }); + const dailySeries = buildGradedSeriesFromPoints(perDayRecords, now, userTz); + const latestDay = dailyBudgeted.daily[0] ?? null; + + // v1.18.10 — the finished recent-vs-baseline adherence comparison + // + normal-swing verdict, computed from the canonical per-day rate series + // so the model states the trend rather than deriving it. The compliance % + // itself stays server-computed (compliance7/30 above); this adds only the + // trend signal. Adherence reads higher-better; the band is 0–100%. + const signal = buildMetricSignal({ + metric: + locale === "en" + ? `adherence for ${sanitizeForPrompt(medication.name)}` + : `Einnahmetreue für ${sanitizeForPrompt(medication.name)}`, + unit: "%", + direction: "higher-better", + graded: dailySeries, + // Recency is surfaced separately via dataCoverage.newestMeasurementDaysAgo + // (computed after this map); the per-med signal leaves it null. + newestDaysAgo: null, + }); + + return { + medicationId: medication.id, + name: sanitizeForPrompt(medication.name), + dose: sanitizeForPrompt(medication.dose, 50), + category: categoryMap[medication.id] ?? "OTHER", + schedulesPerDay: medication.schedules.length, + compliance7: compliance7.rate, + compliance30: compliance30.rate, + streak: compliance7.streak, + taken7: compliance7.taken, + skipped7: compliance7.skipped, + missed7: compliance7.missed, + ...(signal ? { signal } : {}), + dailySeries, + latestDay, + }; +} + +/** + * The per-medication placeholder rows the UI surfaces alongside the + * model-authored overall `summary`; the model's compliance prompt returns + * only the single `{ summary }` envelope. + */ +function medicationPlaceholderLines( + medicationSnapshots: ReadonlyArray<{ medicationId: string; name: string }>, + locale: SupportedLocale, +): MedicationSummaryItem[] { + return medicationSnapshots.map((medication) => ({ + medicationId: medication.medicationId, + text: normalizeSummaryText( + locale === "de" + ? `${medication.name}: Es liegen noch nicht genügend konsistente Detaildaten für eine belastbare Kurzbewertung vor.` + : `${medication.name}: There is currently not enough consistent detail data for a robust short assessment.`, + ), + })); +} + +const MEDICATION_COMPLIANCE_STATUS_CARD: StatusCardPlugin< + MedicationComplianceResult, + MedicationSummaryItem[] +> = { + scope: "medication-compliance", + codec: medicationComplianceCodec, + unavailableText: (locale) => getNoKeyMedicationComplianceStatusText(locale), + emptyMessage: + "Medication-compliance-status summary was empty after normalization", + build: buildMedicationComplianceSnapshot, +}; diff --git a/src/lib/insights/metric-status.ts b/src/lib/insights/metric-status.ts index 69c1aff79..6ad6fea7c 100644 --- a/src/lib/insights/metric-status.ts +++ b/src/lib/insights/metric-status.ts @@ -1,17 +1,17 @@ /** - * v1.8.7.1 — generic per-HealthKit-metric assessment generator. + * Generic per-HealthKit-metric assessment generator. * - * The seven specialised generators (`pulse-status.ts`, …) each hand-roll - * a metric-specific snapshot + prompt. This one generator covers every - * registered HealthKit metric (`metric-status-registry.ts`) through the - * archetype prompt templates (`prompts/metric-archetypes.ts`), reusing - * the exact same machinery the specialised cards use: + * The specialised cards (`pulse-status.ts`, …) each plug a metric-specific + * snapshot + prompt into the status pipeline (`status-pipeline.ts`). This one + * generator covers every registered HealthKit metric + * (`metric-status-registry.ts`) through the archetype prompt templates + * (`prompts/metric-archetypes.ts`), on the same pipeline stages: * - the graded-series snapshot builder (`buildGradedSeriesWithRollups`), - * - the shared provider runner (`runStatusCompletion`), - * - the stale-while-revalidate cache (`readFreshStatusText` / - * `resolveReadOnlyStatusMiss`), - * - the standard cache-row persist (`persistStatusInsight`), - * - the previous-context comparison block (`getPreviousInsightContext`). + * - the stale-while-revalidate cache (`readCachedStatus` / + * `resolveReadOnlyStatus`), + * - the snapshot seal and content-hash gate, + * - the previous-context and anti-repetition blocks, + * - the single-card provider run and the standard note persist. * * The return shape is byte-identical to the specialised generators so * `InsightStatusCard` consumes it unchanged. @@ -34,23 +34,12 @@ import { type MetricStatusMeta, type MetricStatusMetricId, } from "@/lib/insights/metric-status-registry"; -import { - formatPreviousContextForPrompt, - getPreviousInsightContext, -} from "@/lib/insights/memory"; -import { - buildAssessmentContextBlock, - computeSteadyRun, - pickVarietyLead, -} from "@/lib/insights/assessment-context"; -import { getRelevantCorrelationsForMetric } from "@/lib/insights/metric-correlation-context"; +import { computeSteadyRun } from "@/lib/insights/assessment-context"; import { getAgeFromDateOfBirth } from "@/lib/analytics/pulse-targets"; import { lookupNormalRange } from "@/lib/insights/derived/norms"; import { buildMetricSignal } from "@/lib/insights/metric-signal"; import { PROMPT_VERSION } from "@/lib/ai/prompts/base-system"; import { AI_BUDGETS } from "@/lib/ai/ai-budgets"; -import { openerArchetypeHint } from "@/lib/ai/prompts/opener-archetype"; -import type { Locale } from "@/lib/i18n/config"; import { getNoKeyGeneralStatusText, getNoKeyMetricStatusText, @@ -60,29 +49,27 @@ import { buildGradedSeriesFromPoints, buildGradedSeriesWithRollups, convertGradedSeries, - degradeStatusSnapshotToBudget, type GradedSeries, } from "@/lib/insights/graded-series"; -import { statusSnapshotCharsForRecord } from "@/lib/ai/capabilities/usage"; +import { summarizeSeries } from "@/lib/insights/status-shared"; import { - type SupportedLocale, - normalizeLocale, - finalizeStatusSummary, - persistStatusInsight, - summarizeSeries, -} from "@/lib/insights/status-shared"; -import { runStatusCompletion } from "@/lib/insights/status-provider"; + runPreparedStatusCard, + type PreparedStatusCard, +} from "@/lib/insights/status-card-generation"; +import { + buildStatusPromptContext, + coverageSpan, + openStatusCard, + pendingStatusCard, + readCachedStatus, + refreshUnchangedStatus, + resolveReadOnlyStatus, + sealStatusSnapshot, + textStatusCodec, + type StatusCardContext, +} from "@/lib/insights/status-pipeline"; import { readSleepNights } from "@/lib/measurements/list/sleep-nights"; import { loadUserSourcePriority } from "@/lib/rollups/measurement-read"; -import { resolveUserTimezone, userDayKey } from "@/lib/tz/resolver"; -import { - readFreshStatusText, - refreshUnchangedStatusInsight, - resolveReadOnlyStatusMiss, - statusCacheAction, -} from "@/lib/insights/status-cache"; -import { hashInsightSnapshot } from "@/lib/insights/snapshot-hash"; -import { returnTimeoutFallback } from "@/lib/insights/timeout-fallback"; import { annotate } from "@/lib/logging/context"; import { binaryReferenceSex } from "@/lib/profile/sex"; import { @@ -155,33 +142,32 @@ export async function generateMetricStatus(args: { insufficient: true, }; } + const prepared = await prepareMetricStatus(meta, args); + return (await runPreparedStatusCard(prepared)) as MetricStatusResult; +} - const locale = normalizeLocale(args.locale); - const force = args.force === true; - const readOnly = args.readOnly === true; - const scope = metricStatusScope(meta.id); - const cacheAction = statusCacheAction(scope, locale); - // v1.30.3 — resolve the user's own tz BEFORE the day-key so the - // cache rolls over at the user's local midnight, not Berlin's. Has to - // happen ahead of the cache read below (the earliest possible return - // path), not just the later day-bucketing reads. - const userTz = await resolveUserTimezone(args.userId); - const todayKey = userDayKey(new Date(), userTz); +/** + * Everything up to (not including) the provider call, on the shared + * pipeline stages, with the empty-data gate between the cache read and the + * read-only path. + */ +async function prepareMetricStatus( + meta: MetricStatusMeta, + args: { + userId: string; + locale?: string | null; + force?: boolean; + readOnly?: boolean; + }, +): Promise { + const ctx = await openStatusCard( + args.userId, + metricStatusScope(meta.id), + args, + ); - const cached = await readFreshStatusText({ - userId: args.userId, - cacheAction, - todayKey, - force, - }); - if (cached) { - return { - hasProvider: true, - text: cached.text, - cached: true, - updatedAt: cached.updatedAt, - }; - } + const cached = await readCachedStatus(ctx, textStatusCodec); + if (cached) return { phase: "served", result: cached }; // Empty-data guard — no readings means no assessment to generate. Done // BEFORE the read-only enqueue so an empty metric never queues an LLM @@ -194,45 +180,143 @@ export async function generateMetricStatus(args: { meta: { metric: meta.id }, }); return { - hasProvider: true, - text: null, - cached: false, - updatedAt: null, - insufficient: true, - }; - } - - if (readOnly) { - const outcome = await resolveReadOnlyStatusMiss({ - userId: args.userId, - metric: scope, - locale, - }); - // Unavailable for any reason serves the deterministic line and enqueues - // nothing. `hasProvider` is provider presence only. - if (outcome.kind === "unavailable") { - return { - hasProvider: outcome.hasProvider, - text: getNoKeyGeneralStatusText(locale), - cached: true, + phase: "served", + result: { + hasProvider: true, + text: null, + cached: false, updatedAt: null, - }; - } - // Stale-while-revalidate: serve the last good assessment (if any) - // instantly while the worker re-warms the cache; only fall to the - // empty preparing skeleton when none was ever produced. - return { - hasProvider: true, - text: outcome.lastGood?.text ?? null, - cached: outcome.lastGood !== null, - updatedAt: outcome.lastGood?.updatedAt ?? null, - preparing: outcome.lastGood === null, - revalidating: outcome.revalidating, + insufficient: true, + }, }; } + if (ctx.readOnly) { + // The read-only miss serves the general deterministic line when notes + // are unavailable; it has no signal to ground a metric line in. + return { + phase: "served", + result: await resolveReadOnlyStatus( + ctx, + textStatusCodec, + getNoKeyGeneralStatusText(ctx.locale), + ), + }; + } + + const built = await buildMetricSnapshot(ctx, meta); + const { snapshotJson, snapshotHash } = await sealStatusSnapshot( + ctx, + built.snapshot, + ); + const unchanged = await refreshUnchangedStatus( + ctx, + textStatusCodec, + snapshotHash, + ); + if (unchanged) return { phase: "served", result: unchanged }; + + const { + graded, + summary, + series, + newestDaysAgo, + signal, + latest, + reading, + sex, + } = built; + const { locale, todayKey } = ctx; + + // v1.12.1 — diversity / anti-repetition context, all from already-computed + // data (no new statistics, no new persistence). Grounding is untouched: + // these blocks add a rotating opening angle, an explicit data-strength + // line, a steady-run repetition signal, and the FDR-surviving cross-metric + // correlations that involve THIS metric. The correlation fetch is + // best-effort and decorative — a failure resolves to no block, never a + // generation failure. The opener hint is keyed per (user, metric, day) + // exactly as `derived-assessment-ai.ts` does for the score cards, so the + // per-metric card opens verdict-first and varies run-to-run. + const context = await buildStatusPromptContext(ctx, { + varietyKey: meta.id, + repeatCount: computeSteadyRun(graded.weekly, graded.monthly), + points: summary?.points ?? series.daily.length, + newestDaysAgo, + relationsFor: meta.measurementType, + }); + + // v1.27.13 — 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. + // The guideline bands are canonical, so the band position is classified on + // the canonical value; the block then prints in the reader's unit. + const interpretationValue = signal?.current ?? latest?.value ?? null; + const interpretationBlock = + interpretationValue !== null + ? buildInterpretationBlock({ + metricKey: meta.id, + value: reading.toCanonical(interpretationValue), + sex, + locale, + ...(reading.converts + ? { + display: { + unit: reading.unit, + convert: reading.toDisplay, + }, + } + : {}), + }) + : undefined; + + const fallbackText = getNoKeyMetricStatusText( + locale, + signal, + reading.decimals, + ); + return pendingStatusCard(ctx, textStatusCodec, { + systemPrompt: getMetricArchetypeSystemPrompt(built.shownMeta, locale), + userPrompt: getMetricArchetypeUserPrompt( + built.shownMeta, + snapshotJson, + todayKey, + locale, + context.previousContextBlock, + context.assessmentContextBlock, + interpretationBlock, + context.openerHint, + ), + hashes: { snapshotHash }, + // v1.12.1 — the phrasing task benefits from a touch more sampling + // entropy while the FACTS stay pinned by the snapshot + the + // forbidden-phrase guards. 0.3 was conservative for a 2-4 sentence prose + // task; 0.45 varies cadence without loosening grounding. The pair used to + // sit inline here, bypassing the registry that documents every surface's + // ceiling; it now reads from `AI_BUDGETS.statusArchetype`. + temperature: AI_BUDGETS.statusArchetype.temperature, + maxTokens: AI_BUDGETS.statusArchetype.maxTokens, + fallbackText, + emptyMessage: `Metric-status summary was empty for ${meta.id}`, + detail: undefined, + }); +} + +/** + * The metric snapshot, in the reader's units: the series, the reference + * band (sharpened by age and sex where the norms cover the metric), the + * signal, and the data coverage. + */ +async function buildMetricSnapshot( + ctx: StatusCardContext, + meta: MetricStatusMeta, +) { + const { userId, locale, userTz, todayKey } = ctx; const user = await prisma.user.findUnique({ - where: { id: args.userId }, + where: { id: userId }, select: { dateOfBirth: true, gender: true, @@ -269,57 +353,14 @@ export async function generateMetricStatus(args: { const now = new Date(); - // Bounded raw read for the derived recent stats; the monthly/yearly - // tail comes from the rollup tier (full-history fallback on a miss), - // exactly as the specialised cards do. - // - // SLEEP_DURATION is the one type stored ONE ROW PER STAGE per night - // (IN_BED / AWAKE / CORE / DEEP / REM, minutes each), so a raw per-row - // read would feed the status prose a single ~16 min stage as the - // "current sleep" instead of the night total. Route it through - // `reconstructSleepNights` (the same helper the dashboard / series / - // targets use) so every derived value here — `series`, `summary`, - // `latest` — is a per-night TIME-ASLEEP total in minutes (the registry - // unit + normalRange are already minutes). The night reconstruction - // clusters stages into sessions, dedups multi-source nights via the - // user's `sleep` priority ladder, and handles the midnight split. Gated - // to SLEEP_DURATION only so no other metric's status path changes. - const isSleep = meta.measurementType === "SLEEP_DURATION"; - - // v1.2.5 — the day bucketing below keys on the user's own - // calendar regardless of branch. `userTz` was already resolved above for - // the cache day-key. - - const points: Array<{ measuredAt: Date; value: number }> = []; - let measurements: Array<{ measuredAt: Date }> = []; - if (isSleep) { - // The latest 365 nights, the sleep twin of the 365 latest readings the - // other types read (`readRecentReadings`). A cut on stage rows instead (2190 of them) reached - // back a few weeks on a stage-rich watch and began mid-night. - const nights = ( - await readSleepNights( - args.userId, - userTz, - await loadUserSourcePriority(args.userId), - { maxNights: 365, newest: true }, - ) - ).filter((n) => n.asleepMinutes > 0); - for (const n of nights) { - points.push({ measuredAt: n.measuredAt, value: n.asleepMinutes }); - } - measurements = nights.map((n) => ({ measuredAt: n.measuredAt })); - } else { - // The latest 365 readings, never less than the last 30 days. - const rows = await readRecentReadings(args.userId, meta.measurementType); - for (const m of rows) { - points.push({ - measuredAt: m.measuredAt, - value: reading.toDisplay(m.value), - }); - } - measurements = rows.map((m) => ({ measuredAt: m.measuredAt })); - } + const { points, measurements } = await readMetricPoints( + userId, + meta, + reading, + userTz, + ); const series = applyPayloadBudget(points, { now, tz: userTz }); + const isSleep = meta.measurementType === "SLEEP_DURATION"; // SLEEP_DURATION must never go through `buildGradedSeriesWithRollups`: that // path reads RAW per-stage rows (and the stage-summed `measurement_rollups` // tier) and folds IN_BED + AWAKE + bare ASLEEP + CORE/DEEP/REM into one @@ -331,7 +372,7 @@ export async function generateMetricStatus(args: { ? buildGradedSeriesFromPoints(points, now, userTz) : reading.convertGraded( await buildGradedSeriesWithRollups( - args.userId, + userId, meta.measurementType, now, userTz, @@ -343,52 +384,23 @@ export async function generateMetricStatus(args: { const latest = series.daily[0] ?? null; - const oldest = measurements.length > 0 ? measurements[0].measuredAt : null; - const newest = + const { totalSpanDays, newestDaysAgo } = coverageSpan( + measurements.length > 0 ? measurements[0].measuredAt : null, measurements.length > 0 ? measurements[measurements.length - 1].measuredAt - : null; - const totalSpanDays = - oldest && newest - ? Math.round( - (newest.getTime() - oldest.getTime()) / (24 * 60 * 60 * 1000), - ) - : 0; - const newestDaysAgo = newest - ? Math.round((Date.now() - newest.getTime()) / (24 * 60 * 60 * 1000)) - : null; + : null, + ); - const ageYears = getAgeFromDateOfBirth(user?.dateOfBirth ?? null); - const sex = binaryReferenceSex(user?.gender); - - // v1.10.0 — sharpen the coarse registry `normalRange` with the - // age/sex reference-range enabler when one is available for this - // metric + profile; otherwise keep the existing flat anchor (strictly - // additive, never a regression). Age + sex only — no ancestry/region. - const sharpenedRange = lookupNormalRange(meta.id, ageYears, sex); - const canonicalRange = sharpenedRange ?? meta.normalRange; - const normalRange = canonicalRange - ? { - ...canonicalRange, - low: reading.toDisplay(canonicalRange.low), - high: reading.toDisplay(canonicalRange.high), - } - : canonicalRange; - - // v1.13.x — the SIGNAL block: the recent-vs-baseline comparison + the - // normal-swing verdict, computed server-side so the model states it rather - // than re-deriving it from the raw buckets (research-ai-quality.md §4/§5C). - // Built from the same graded series the snapshot already carries; null only - // when the recent window is empty, in which case it is omitted. - const signal = buildMetricSignal({ - metric: meta.displayName, - unit: shownMeta.unit, - direction: meta.direction, - graded, - normalRange: normalRange ?? null, - normalRangeSource: sharpenedRange ? "age-sex-adjusted" : undefined, - newestDaysAgo, - }); + const { ageYears, sex, sharpenedRange, normalRange, signal } = + metricRangeAndSignal({ + meta, + shownUnit: shownMeta.unit, + reading, + graded, + newestDaysAgo, + dateOfBirth: user?.dateOfBirth ?? null, + gender: user?.gender ?? null, + }); const snapshot = { locale, @@ -425,199 +437,134 @@ export async function generateMetricStatus(args: { }, }; - const shed = degradeStatusSnapshotToBudget( - snapshot as unknown as Record, - { maxChars: await statusSnapshotCharsForRecord(args.userId) }, - ); - const snapshotJson = JSON.stringify(snapshot); - - annotate({ - action: { name: cacheAction }, - meta: { - payload_size_bytes: snapshotJson.length, - ...(shed.length > 0 ? { snapshot_shed: shed } : {}), - }, - }); - - // Content-hash gate (v1.16.8): when the snapshot is unchanged since the - // last real assessment, refresh the cache timestamp and skip the LLM. - const snapshotHash = hashInsightSnapshot(snapshot); - const unchanged = await refreshUnchangedStatusInsight({ - userId: args.userId, - cacheAction, - todayKey, - snapshotHash, - }); - if (unchanged) { - return { - hasProvider: true, - text: unchanged.text, - cached: true, - updatedAt: unchanged.updatedAt, - }; - } - - const previousContext = await getPreviousInsightContext( - args.userId, - // memory.ts keys previous context by the `insights..` - // action; the generic scope (`metric:-status`) carries the same - // `-status` suffix so the comparison row reads back correctly. The - // `getPreviousInsightContext` param is typed to accept this scope form - // (`PreviousContextScope`), so the argument type-checks honestly. - `${scope}-status`, - locale, - 12, - ); - const previousContextBlock = formatPreviousContextForPrompt( - previousContext, - locale, - ); - - // v1.12.1 — diversity / anti-repetition context, all from already-computed - // data (no new statistics, no new persistence). Grounding is untouched: - // these blocks add a rotating opening angle, an explicit data-strength - // line, a steady-run repetition signal, and the FDR-surviving cross-metric - // correlations that involve THIS metric. The correlation fetch is - // best-effort and decorative — a failure resolves to no block, never a - // generation failure. - const varietyLead = pickVarietyLead(args.userId, meta.id, todayKey); - const steadyRun = computeSteadyRun(graded.weekly, graded.monthly); - const relations = await getRelevantCorrelationsForMetric( - args.userId, - meta.measurementType, - locale, - ); - const contextBlock = buildAssessmentContextBlock( - { - varietyLead, - dataStrength: { - points: summary?.points ?? series.daily.length, - newestDaysAgo, - }, - repeatCount: steadyRun, - relations, - }, - locale, - ); - - // v1.27.13 — 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. - // The guideline bands are canonical, so the band position is classified on - // the canonical value; the block then prints in the reader's unit. - const interpretationValue = signal?.current ?? latest?.value ?? null; - const interpretationBlock = - interpretationValue !== null - ? buildInterpretationBlock({ - metricKey: meta.id, - value: reading.toCanonical(interpretationValue), - sex, - locale, - ...(reading.converts - ? { - display: { - unit: reading.unit, - convert: reading.toDisplay, - }, - } - : {}), - }) - : undefined; - - // v1.28.40 — the rotating opener-archetype hint, keyed per (user, metric, - // day) exactly as `derived-assessment-ai.ts` does for the score cards. This - // activates the base prompt's dormant "Lead per the OPENER HINT … do NOT open - // with the number" branch so the per-metric card opens verdict-first and - // varies run-to-run. No new model call — one arg on the existing completion. - const openerHint = openerArchetypeHint( - `${args.userId}:${scope}:${todayKey}`, - locale as Locale, - ); - - const outcome = await runStatusCompletion({ - userId: args.userId, - cacheAction, - capability: "statusText", - systemPrompt: getMetricArchetypeSystemPrompt(shownMeta, locale), - userPrompt: getMetricArchetypeUserPrompt( - shownMeta, - snapshotJson, - todayKey, - locale, - previousContextBlock, - contextBlock, - interpretationBlock, - openerHint, - ), - // v1.12.1 — the phrasing task benefits from a touch more sampling - // entropy while the FACTS stay pinned by the snapshot + the - // forbidden-phrase guards. 0.3 was conservative for a 2-4 sentence prose - // task; 0.45 varies cadence without loosening grounding. The pair used to - // sit inline here, bypassing the registry that documents every surface's - // ceiling; it now reads from `AI_BUDGETS.statusArchetype`. - temperature: AI_BUDGETS.statusArchetype.temperature, - maxTokens: AI_BUDGETS.statusArchetype.maxTokens, - }); - - if (outcome.kind === "none") { - return { - hasProvider: false, - text: getNoKeyMetricStatusText(locale, signal, reading.decimals), - cached: true, - updatedAt: null, - }; - } - if (outcome.kind === "timeout" || outcome.kind === "error") { - return returnTimeoutFallback({ - cacheAction, - reason: outcome.kind, - userId: args.userId, - todayKey, - stubText: getNoKeyMetricStatusText(locale, signal, reading.decimals), - }); - } - - // Outbound safety screen — WITHHOLD policy for a background-generated card. - const screened = finalizeStatusSummary(outcome.content, locale); - if (!screened.ok) { - annotate({ - action: { name: "insights.status.outbound_blocked" }, - meta: { cacheAction, reason: screened.reason }, - }); - return returnTimeoutFallback({ - cacheAction, - reason: "screened", - userId: args.userId, - todayKey, - stubText: getNoKeyMetricStatusText(locale, signal, reading.decimals), - }); - } - const text = screened.text; - if (!text) { - throw new Error(`Metric-status summary was empty for ${meta.id}`); - } - - const updatedAt = await persistStatusInsight({ - userId: args.userId, - cacheAction, - todayKey, - text, - snapshotHash, - }); - - return { hasProvider: true, text, cached: false, updatedAt }; + return { + snapshot, + shownMeta, + reading, + graded, + summary, + series, + latest, + signal, + sex, + newestDaysAgo, + }; } -export function resolveMetricStatusLocale( - locale: string | null | undefined, -): SupportedLocale { - return normalizeLocale(locale); +/** + * The metric's points in the reader's units, and the readings they came + * from. SLEEP_DURATION reads per-night totals, every other type its latest + * readings. + */ +async function readMetricPoints( + userId: string, + meta: MetricStatusMeta, + reading: ReadingDisplay, + userTz: string, +) { + // Bounded raw read for the derived recent stats; the monthly/yearly + // tail comes from the rollup tier (full-history fallback on a miss), + // exactly as the specialised cards do. + // + // SLEEP_DURATION is the one type stored ONE ROW PER STAGE per night + // (IN_BED / AWAKE / CORE / DEEP / REM, minutes each), so a raw per-row + // read would feed the status prose a single ~16 min stage as the + // "current sleep" instead of the night total. Route it through + // `reconstructSleepNights` (the same helper the dashboard / series / + // targets use) so every derived value here — `series`, `summary`, + // `latest` — is a per-night TIME-ASLEEP total in minutes (the registry + // unit + normalRange are already minutes). The night reconstruction + // clusters stages into sessions, dedups multi-source nights via the + // user's `sleep` priority ladder, and handles the midnight split. Gated + // to SLEEP_DURATION only so no other metric's status path changes. + const isSleep = meta.measurementType === "SLEEP_DURATION"; + + // v1.2.5 — the day bucketing below keys on the user's own + // calendar regardless of branch. `userTz` was already resolved above for + // the cache day-key. + + const points: Array<{ measuredAt: Date; value: number }> = []; + let measurements: Array<{ measuredAt: Date }> = []; + if (isSleep) { + // The latest 365 nights, the sleep twin of the 365 latest readings the + // other types read (`readRecentReadings`). A cut on stage rows instead (2190 of them) reached + // back a few weeks on a stage-rich watch and began mid-night. + const nights = ( + await readSleepNights( + userId, + userTz, + await loadUserSourcePriority(userId), + { maxNights: 365, newest: true }, + ) + ).filter((n) => n.asleepMinutes > 0); + for (const n of nights) { + points.push({ measuredAt: n.measuredAt, value: n.asleepMinutes }); + } + measurements = nights.map((n) => ({ measuredAt: n.measuredAt })); + } else { + // The latest 365 readings, never less than the last 30 days. + const rows = await readRecentReadings(userId, meta.measurementType); + for (const m of rows) { + points.push({ + measuredAt: m.measuredAt, + value: reading.toDisplay(m.value), + }); + } + measurements = rows.map((m) => ({ measuredAt: m.measuredAt })); + } + return { points, measurements }; } +/** + * The reference band for this profile and the finished signal. + */ +function metricRangeAndSignal(args: { + meta: MetricStatusMeta; + shownUnit: string; + reading: ReadingDisplay; + graded: GradedSeries; + newestDaysAgo: number | null; + dateOfBirth: Date | null; + gender: string | null; +}) { + const { meta, reading, graded, newestDaysAgo } = args; + const ageYears = getAgeFromDateOfBirth(args.dateOfBirth); + const sex = binaryReferenceSex(args.gender); + + // v1.10.0 — sharpen the coarse registry `normalRange` with the + // age/sex reference-range enabler when one is available for this + // metric + profile; otherwise keep the existing flat anchor (strictly + // additive, never a regression). Age + sex only — no ancestry/region. + const sharpenedRange = lookupNormalRange(meta.id, ageYears, sex); + const canonicalRange = sharpenedRange ?? meta.normalRange; + const normalRange = canonicalRange + ? { + ...canonicalRange, + low: reading.toDisplay(canonicalRange.low), + high: reading.toDisplay(canonicalRange.high), + } + : canonicalRange; + + // v1.13.x — the SIGNAL block: the recent-vs-baseline comparison + the + // normal-swing verdict, computed server-side so the model states it rather + // than re-deriving it from the raw buckets (research-ai-quality.md §4/§5C). + // Built from the same graded series the snapshot already carries; null only + // when the recent window is empty, in which case it is omitted. + const signal = buildMetricSignal({ + metric: meta.displayName, + unit: args.shownUnit, + direction: meta.direction, + graded, + normalRange: normalRange ?? null, + normalRangeSource: sharpenedRange ? "age-sex-adjusted" : undefined, + newestDaysAgo, + }); + + return { ageYears, sex, sharpenedRange, normalRange, signal }; +} + +type ReadingDisplay = ReturnType; + /** * How one metric's numbers are written for this reader: the unit symbol, the * conversions in both directions, and the decimals a value is read at. diff --git a/src/lib/insights/mood-aggregates.ts b/src/lib/insights/mood-aggregates.ts index 772621ed5..5112f498c 100644 --- a/src/lib/insights/mood-aggregates.ts +++ b/src/lib/insights/mood-aggregates.ts @@ -283,16 +283,14 @@ export const MOOD_CROSS_METRIC_ROW_CAP = 50_000; // --- Target bands (mirror mood-chart VALUE_BANDS + mood-status) --- -/** - * The mood-status generator grades "in target" as score >= 3.5. The - * mood-chart paints three reference bands (1–2 red, 2–3 orange, 3–5 - * green). We keep both anchored here so the heatmap colour map and the - * in-target headline never drift from the line chart. - */ -export const MOOD_GREEN_MIN = 3.5; -export const MOOD_GREEN_MAX = 5; -export const MOOD_ORANGE_MIN = 2; -export const MOOD_ORANGE_MAX = 3.5; +// Defined in a leaf module so the per-domain modules this hub re-exports can +// read them without importing the hub back. +export { + MOOD_GREEN_MAX, + MOOD_GREEN_MIN, + MOOD_ORANGE_MAX, + MOOD_ORANGE_MIN, +} from "@/lib/insights/mood-bands"; // --- Pair two daily-bucket series on dayOffset (lifted from mood-status) --- diff --git a/src/lib/insights/mood-bands.ts b/src/lib/insights/mood-bands.ts new file mode 100644 index 000000000..3bd1a43db --- /dev/null +++ b/src/lib/insights/mood-bands.ts @@ -0,0 +1,12 @@ +/** + * The mood scale's target bands (mirror mood-chart VALUE_BANDS + mood-status). + * + * The mood-status generator grades "in target" as score >= 3.5. The + * mood-chart paints three reference bands (1–2 red, 2–3 orange, 3–5 + * green). We keep both anchored here so the heatmap colour map and the + * in-target headline never drift from the line chart. + */ +export const MOOD_GREEN_MIN = 3.5; +export const MOOD_GREEN_MAX = 5; +export const MOOD_ORANGE_MIN = 2; +export const MOOD_ORANGE_MAX = 3.5; diff --git a/src/lib/insights/mood-patterns.ts b/src/lib/insights/mood-patterns.ts index 289748095..855485325 100644 --- a/src/lib/insights/mood-patterns.ts +++ b/src/lib/insights/mood-patterns.ts @@ -11,11 +11,10 @@ */ import { dayOffsetToDayKey } from "@/lib/insights/bucket-series"; -import { - MOOD_GREEN_MAX, - MOOD_GREEN_MIN, - type DailyPoint, - type MoodAggregateEntry, +import { MOOD_GREEN_MAX, MOOD_GREEN_MIN } from "@/lib/insights/mood-bands"; +import type { + DailyPoint, + MoodAggregateEntry, } from "@/lib/insights/mood-aggregates"; import { round } from "@/lib/insights/status-shared"; import { DEFAULT_TIMEZONE } from "@/lib/tz/format"; diff --git a/src/lib/insights/mood-status.ts b/src/lib/insights/mood-status.ts index e19388e09..565f18c8e 100644 --- a/src/lib/insights/mood-status.ts +++ b/src/lib/insights/mood-status.ts @@ -1,19 +1,12 @@ import { prisma } from "@/lib/db"; import { getMoodSystemPrompt, getMoodUserPrompt } from "@/lib/ai/prompts/mood"; -import { openerArchetypeHint } from "@/lib/ai/prompts/opener-archetype"; -import type { Locale } from "@/lib/i18n/config"; import { getNoKeyMoodStatusText } from "@/lib/insights/no-key-fallbacks"; -import { - formatPreviousContextForPrompt, - getPreviousInsightContext, -} from "@/lib/insights/memory"; -import { - buildAssessmentContextBlock, - computeSteadyRun, - pickVarietyLead, -} from "@/lib/insights/assessment-context"; +import { computeSteadyRun } from "@/lib/insights/assessment-context"; import { significantPearsonCorrelation } from "@/lib/analytics/correlations"; -import { applyPayloadBudget } from "@/lib/insights/bucket-series"; +import { + applyPayloadBudget, + type DailyBucket, +} from "@/lib/insights/bucket-series"; import { type TagInfluenceRow, MOOD_GREEN_MAX, @@ -34,37 +27,26 @@ import { pairDailyBuckets, } from "@/lib/insights/mood-aggregates"; import { computeMoodNarratives } from "@/lib/insights/mood-narratives"; -import { - buildGradedSeriesFromPoints, - degradeStatusSnapshotToBudget, -} from "@/lib/insights/graded-series"; -import { statusSnapshotCharsForRecord } from "@/lib/ai/capabilities/usage"; +import { buildGradedSeriesFromPoints } from "@/lib/insights/graded-series"; import { buildMetricSignal } from "@/lib/insights/metric-signal"; -import { - type SupportedLocale, - normalizeLocale, - finalizeStatusSummary, - persistStatusInsight, - round, - summarizeSeries, -} from "@/lib/insights/status-shared"; -import { - readFreshStatusText, - refreshUnchangedStatusInsight, - resolveReadOnlyStatusMiss, - statusCacheAction, -} from "@/lib/insights/status-cache"; -import { hashInsightSnapshot } from "@/lib/insights/snapshot-hash"; -import { returnTimeoutFallback } from "@/lib/insights/timeout-fallback"; +import { round, summarizeSeries } from "@/lib/insights/status-shared"; import { runPreparedStatusCard, type PreparedStatusCard, - type StatusCardResult, } from "@/lib/insights/status-card-generation"; +import { + coverageSpan, + prepareStatusCard, + textStatusCodec, + type StatusCardBuild, + type StatusCardContext, + type StatusCardOptions, + type StatusCardPlugin, + type StatusTextResult, +} from "@/lib/insights/status-pipeline"; import { annotate } from "@/lib/logging/context"; import { createCustomLabelResolver } from "@/lib/mood/custom-tags"; import { loadUserSourcePriority } from "@/lib/rollups/measurement-read"; -import { resolveUserTimezone, userDayKey } from "@/lib/tz/resolver"; /** * Drop the ranking-only `pooledSd` before a tag-influence row enters the @@ -127,117 +109,212 @@ export function crossMetricDailySeries( */ export async function generateMoodStatusForUser( userId: string, - options?: { - locale?: string | null; - force?: boolean; - /** v1.8.3 — read-only navigation path; see weight-status for the rationale. */ - readOnly?: boolean; - }, -): Promise<{ - hasProvider: boolean; - text: string | null; - cached: boolean; - updatedAt: string | null; - preparing?: boolean; - /** v1.9.0 — last-good text served while a refresh is in flight; keep polling. */ - revalidating?: boolean; -}> { + options?: StatusCardOptions, +): Promise { const prepared = await prepareMoodStatusForUser(userId, options); - const result = await runPreparedStatusCard(prepared); - return result as { - hasProvider: boolean; - text: string | null; - cached: boolean; - updatedAt: string | null; - preparing?: boolean; - revalidating?: boolean; + return (await runPreparedStatusCard(prepared)) as StatusTextResult; +} + +/** + * Everything up to (not including) the provider call, through the shared + * status pipeline (`status-pipeline.ts`); this card supplies its snapshot. + */ +export function prepareMoodStatusForUser( + userId: string, + options?: StatusCardOptions, +): Promise { + return prepareStatusCard(MOOD_STATUS_CARD, userId, options); +} + +type MoodStatusEntry = Awaited< + ReturnType +>[number]; +type CrossMetricMeasurement = Awaited< + ReturnType +>[number]; + +/** + * The mood snapshot: the mood series against its band, the tag and factor + * boards the mood page shows, the narrative feed, and the cross-metric + * context. + */ +async function buildMoodSnapshot( + ctx: StatusCardContext, +): Promise> { + const { userId, locale, userTz, todayKey } = ctx; + const entries = await readMoodEntriesForStatus(userId); + + const now = new Date(); + + // v1.2.5 — every day-bucketing pass below keys on the user's own + // calendar day. `userTz` was already resolved above for the cache day-key. + + const moodPoints = entries.map((entry) => ({ + measuredAt: entry.moodLoggedAt, + value: entry.score, + })); + // `applyPayloadBudget` daily buckets drive the in-target %, latest, + // and correlations; the compact graded series reaches the prompt. + const moodSeries = applyPayloadBudget(moodPoints, { now, tz: userTz }); + const moodGraded = buildGradedSeriesFromPoints(moodPoints, now, userTz); + const moodSummary = summarizeSeries( + moodSeries.daily.map((bucket) => ({ value: bucket.value })), + ); + + const greenMin = MOOD_GREEN_MIN; + const greenMax = MOOD_GREEN_MAX; + const orangeMin = MOOD_ORANGE_MIN; + const orangeMax = MOOD_ORANGE_MAX; + + // "Last 30 daily points" = the newest 30 daily buckets (offsets 0..29). + const inTargetPctLast30DailyPoints = computeInTargetPct(moodSeries.daily); + + // daily[0] = newest bucket (lowest dayOffset). + const latestMood = moodSeries.daily[0] ?? null; + const previousMood = moodSeries.daily[1] ?? null; + + const { totalSpanDays, newestDaysAgo: newestEntryDaysAgo } = coverageSpan( + entries.length > 0 ? entries[0].moodLoggedAt : null, + entries.length > 0 ? entries[entries.length - 1].moodLoggedAt : null, + ); + + const measurements = await readCrossMetricMeasurements(userId, now); + const relations = await computeMoodRelations({ + userId, + entries, + measurements, + moodDaily: moodSeries.daily, + inTargetPctLast30DailyPoints, + now, + userTz, + }); + + // v1.18.10 — hand the model the finished recent-vs-baseline + // comparison + normal-swing verdict instead of asking it to derive them. + // Mood reads higher-better against the green band. + const moodSignal = buildMetricSignal({ + metric: locale === "en" ? "your mood" : "deine Stimmung", + unit: "/5", + direction: "higher-better", + graded: moodGraded, + normalRange: { low: greenMin, high: greenMax }, + newestDaysAgo: newestEntryDaysAgo, + }); + + const snapshot = { + locale, + generatedForDay: todayKey, + focus: "mood", + dataCoverage: { + totalEntries: entries.length, + totalSpanDays, + newestEntryDaysAgo, + }, + mood: { + ...(moodSignal ? { signal: moodSignal } : {}), + summary: moodSummary, + series: moodGraded, + latestDayFocus: latestMood + ? { + dayOffset: latestMood.dayOffset, + value: latestMood.value, + deltaToPreviousDailyPoint: + previousMood == null + ? null + : round(latestMood.value - previousMood.value, 2), + } + : null, + target: { + greenMin, + greenMax, + orangeMin, + orangeMax, + inTargetPctLast30DailyPoints, + }, + tags: relations.tagSummary.length > 0 ? relations.tagSummary : null, + // v1.11.5 — with-vs-without tag influence (only the populated + // axes are emitted so the prompt stays compact). + tagInfluence: + relations.tagInfluence.flat.length > 0 || + relations.tagInfluence.structured.length > 0 + ? { + flat: + relations.tagInfluence.flat.length > 0 + ? relations.tagInfluence.flat.map(stripRankingOnly) + : null, + structured: + relations.tagInfluence.structured.length > 0 + ? relations.tagInfluence.structured.map(stripRankingOnly) + : null, + } + : null, + // v1.11.5 — the ranked "better days" board the user sees, so the + // model's relations prose matches the on-screen list (shown == sent). + betterDays: relations.betterDays.length > 0 ? relations.betterDays : null, + // v1.14.0 — RATED-factor → vital deviation rows (low-vs-high-day Welch + // delta, FDR-gated). Lets the model say "your data shows sleep runs ~X + // shorter on days you rate work low (n=N, association not cause)". Only + // the surviving rows reach the prompt; an empty board emits null. + factorCrosstab: + relations.factorCrosstab.length > 0 + ? relations.factorCrosstab.map(stripFactorCrosstab) + : null, + narratives: relations.narratives.length > 0 ? relations.narratives : null, + // v1.9.0 — only emit the daypart pattern when it cleared its + // spread/sample floors, so the model never reasons over a once-a-day + // logger's single-bucket artefact. + timeOfDay: relations.timeOfDay.reliable + ? { + buckets: relations.timeOfDay.buckets, + best: relations.timeOfDay.best, + worst: relations.timeOfDay.worst, + } + : null, + stability: relations.stability, + }, + crossMetricContext: moodCrossMetricContext(relations), + }; + + // MOOD is mood-entry backed (not a MeasurementType), so the cross-metric + // RELATIONS sub-block is sourced from the snapshot's own crossMetricContext + // rather than the measurement discovery engine; relations stays empty here + // and the variety / data-strength / repetition signals carry the rotation. + return { + snapshot, + finish: () => ({ + context: { + repeatCount: computeSteadyRun(moodGraded.weekly, moodGraded.monthly), + points: moodSeries.daily.length, + newestDaysAgo: newestEntryDaysAgo, + relationsFor: null, + }, + fallbackText: getNoKeyMoodStatusText(locale, moodSignal), + prompts: (snapshotJson, context) => ({ + systemPrompt: getMoodSystemPrompt(locale), + userPrompt: getMoodUserPrompt( + snapshotJson, + todayKey, + locale, + context.previousContextBlock, + context.assessmentContextBlock, + context.openerHint, + ), + }), + detail: undefined, + }), }; } /** - * v1.18.7 — everything up to (not including) the provider call. - * Returns a finished `served` result for every path that never needed the - * LLM, or a `pending` descriptor the single-card path and the batch path - * both drive. See `status-card-generation.ts` for the contract. + * The latest 90 mood entries, oldest first, with their structured tags and + * their rated factors. */ -export async function prepareMoodStatusForUser( - userId: string, - options?: { - locale?: string | null; - force?: boolean; - /** v1.8.3 — read-only navigation path; see weight-status for the rationale. */ - readOnly?: boolean; - }, -): Promise { - const locale = normalizeLocale(options?.locale); - const force = options?.force === true; - const readOnly = options?.readOnly === true; - const cacheAction = statusCacheAction("mood", locale); - // v1.30.3 — resolve the user's own tz BEFORE the day-key so the - // cache rolls over at the user's local midnight, not Berlin's. Has to - // happen ahead of the cache read below (the earliest possible return - // path), not just the later day-bucketing reads. - const userTz = await resolveUserTimezone(userId); - const todayKey = userDayKey(new Date(), userTz); - - const cached = await readFreshStatusText({ - userId, - cacheAction, - todayKey, - force, - }); - if (cached) { - return { - phase: "served", - result: { - hasProvider: true, - text: cached.text, - cached: true, - updatedAt: cached.updatedAt, - }, - }; - } - - if (readOnly) { - const outcome = await resolveReadOnlyStatusMiss({ - userId, - metric: "mood", - locale, - }); - // Unavailable for any reason serves the deterministic line and enqueues - // nothing. `hasProvider` is provider presence only. - if (outcome.kind === "unavailable") { - return { - phase: "served", - result: { - hasProvider: outcome.hasProvider, - text: getNoKeyMoodStatusText(locale), - cached: true, - updatedAt: null, - }, - }; - } - // v1.8.7 — stale-while-revalidate: serve the last good assessment - // (if any) instantly while the worker re-warms; only fall to the empty - // preparing skeleton when none was ever produced. - return { - phase: "served", - result: { - hasProvider: true, - text: outcome.lastGood?.text ?? null, - cached: outcome.lastGood !== null, - updatedAt: outcome.lastGood?.updatedAt ?? null, - preparing: outcome.lastGood === null, - revalidating: outcome.revalidating, - }, - }; - } - +function readMoodEntriesForStatus(userId: string) { // v1.4.28 — cap at 90 entries (~3 months) for the mood // surface. The downstream `applyPayloadBudget` slims further. // Order desc + reverse so the per-row consumers downstream (which // expect oldest-first) see the same shape. - const entries = await prisma.moodEntry + return prisma.moodEntry .findMany({ where: { userId, @@ -318,50 +395,13 @@ export async function prepareMoodStatusForUser( })), })); }); +} - const now = new Date(); - - // v1.2.5 — every day-bucketing pass below keys on the user's own - // calendar day. `userTz` was already resolved above for the cache day-key. - - const moodPoints = entries.map((entry) => ({ - measuredAt: entry.moodLoggedAt, - value: entry.score, - })); - // `applyPayloadBudget` daily buckets drive the in-target %, latest, - // and correlations; the compact graded series reaches the prompt. - const moodSeries = applyPayloadBudget(moodPoints, { now, tz: userTz }); - const moodGraded = buildGradedSeriesFromPoints(moodPoints, now, userTz); - const moodSummary = summarizeSeries( - moodSeries.daily.map((bucket) => ({ value: bucket.value })), - ); - - const greenMin = MOOD_GREEN_MIN; - const greenMax = MOOD_GREEN_MAX; - const orangeMin = MOOD_ORANGE_MIN; - const orangeMax = MOOD_ORANGE_MAX; - - // "Last 30 daily points" = the newest 30 daily buckets (offsets 0..29). - const inTargetPctLast30DailyPoints = computeInTargetPct(moodSeries.daily); - - // daily[0] = newest bucket (lowest dayOffset). - const latestMood = moodSeries.daily[0] ?? null; - const previousMood = moodSeries.daily[1] ?? null; - - const oldestEntry = entries.length > 0 ? entries[0].moodLoggedAt : null; - const newestEntry = - entries.length > 0 ? entries[entries.length - 1].moodLoggedAt : null; - const totalSpanDays = - oldestEntry && newestEntry - ? Math.round( - (newestEntry.getTime() - oldestEntry.getTime()) / - (24 * 60 * 60 * 1000), - ) - : 0; - const newestEntryDaysAgo = newestEntry - ? Math.round((Date.now() - newestEntry.getTime()) / (24 * 60 * 60 * 1000)) - : null; - +/** + * The cross-metric measurements the correlations and the factor crosstab + * pair against, bounded to the ~365-day window they can pair in. + */ +async function readCrossMetricMeasurements(userId: string, now: Date) { // Fetch cross-metric context for enrichment. v1.4.28 FB-D2 — cap at // 1095 = 365 d × 3 channels so power users do not pull unbounded // rows. Order desc + reverse so the downstream filters see oldest- @@ -415,6 +455,33 @@ export async function prepareMoodStatusForUser( annotate({ meta: { cross_metric_truncated: true } }); } + return measurements; +} + +/** + * Mood against the other metrics and its own tags: the correlations, the + * tag summaries and influence, the "better days" board, the factor + * crosstab, the time-of-day pattern, stability and the narrative feed — the + * same aggregates the visible mood page shows. + */ +async function computeMoodRelations(args: { + userId: string; + entries: MoodStatusEntry[]; + measurements: CrossMetricMeasurement[]; + moodDaily: DailyBucket[]; + inTargetPctLast30DailyPoints: number | null; + now: Date; + userTz: string; +}) { + const { + userId, + entries, + measurements, + inTargetPctLast30DailyPoints, + now, + userTz, + } = args; + const moodSeries = { daily: args.moodDaily }; const weightSeries = crossMetricDailySeries( measurements, "WEIGHT", @@ -540,275 +607,75 @@ export async function prepareMoodStatusForUser( tz: userTz, }); - // v1.18.10 — hand the model the finished recent-vs-baseline - // comparison + normal-swing verdict instead of asking it to derive them. - // Mood reads higher-better against the green band. - const moodSignal = buildMetricSignal({ - metric: locale === "en" ? "your mood" : "deine Stimmung", - unit: "/5", - direction: "higher-better", - graded: moodGraded, - normalRange: { low: greenMin, high: greenMax }, - newestDaysAgo: newestEntryDaysAgo, - }); - - const snapshot = { - locale, - generatedForDay: todayKey, - focus: "mood", - dataCoverage: { - totalEntries: entries.length, - totalSpanDays, - newestEntryDaysAgo, - }, - mood: { - ...(moodSignal ? { signal: moodSignal } : {}), - summary: moodSummary, - series: moodGraded, - latestDayFocus: latestMood - ? { - dayOffset: latestMood.dayOffset, - value: latestMood.value, - deltaToPreviousDailyPoint: - previousMood == null - ? null - : round(latestMood.value - previousMood.value, 2), - } - : null, - target: { - greenMin, - greenMax, - orangeMin, - orangeMax, - inTargetPctLast30DailyPoints, - }, - tags: tagSummary.length > 0 ? tagSummary : null, - // v1.11.5 — with-vs-without tag influence (only the populated - // axes are emitted so the prompt stays compact). - tagInfluence: - tagInfluence.flat.length > 0 || tagInfluence.structured.length > 0 - ? { - flat: - tagInfluence.flat.length > 0 - ? tagInfluence.flat.map(stripRankingOnly) - : null, - structured: - tagInfluence.structured.length > 0 - ? tagInfluence.structured.map(stripRankingOnly) - : null, - } - : null, - // v1.11.5 — the ranked "better days" board the user sees, so the - // model's relations prose matches the on-screen list (shown == sent). - betterDays: betterDays.length > 0 ? betterDays : null, - // v1.14.0 — RATED-factor → vital deviation rows (low-vs-high-day Welch - // delta, FDR-gated). Lets the model say "your data shows sleep runs ~X - // shorter on days you rate work low (n=N, association not cause)". Only - // the surviving rows reach the prompt; an empty board emits null. - factorCrosstab: - factorCrosstab.length > 0 - ? factorCrosstab.map(stripFactorCrosstab) - : null, - narratives: narratives.length > 0 ? narratives : null, - // v1.9.0 — only emit the daypart pattern when it cleared its - // spread/sample floors, so the model never reasons over a once-a-day - // logger's single-bucket artefact. - timeOfDay: timeOfDay.reliable - ? { - buckets: timeOfDay.buckets, - best: timeOfDay.best, - worst: timeOfDay.worst, - } - : null, - stability, - }, - crossMetricContext: - weightSeries.daily.length >= 3 || - sysSeries.daily.length >= 3 || - pulseSeries.daily.length >= 3 - ? { - weight: - weightSeries.daily.length >= 3 - ? { - summary: summarizeSeries( - weightSeries.daily.map((bucket) => ({ - value: bucket.value, - })), - ), - correlation: moodVsWeightCorrelation, - } - : null, - bloodPressureSystolic: - sysSeries.daily.length >= 3 - ? { - summary: summarizeSeries( - sysSeries.daily.map((bucket) => ({ - value: bucket.value, - })), - ), - correlation: moodVsSysCorrelation, - } - : null, - pulse: - pulseSeries.daily.length >= 3 - ? { - summary: summarizeSeries( - pulseSeries.daily.map((bucket) => ({ - value: bucket.value, - })), - ), - correlation: moodVsPulseCorrelation, - } - : null, - } - : null, - }; - - const shed = degradeStatusSnapshotToBudget( - snapshot as unknown as Record, - { maxChars: await statusSnapshotCharsForRecord(userId) }, - ); - const snapshotJson = JSON.stringify(snapshot); - - annotate({ - action: { name: cacheAction }, - meta: { - payload_size_bytes: snapshotJson.length, - ...(shed.length > 0 ? { snapshot_shed: shed } : {}), - }, - }); - - // Content-hash gate (v1.16.8): when the snapshot is unchanged since the - // last real assessment, refresh the cache timestamp and skip the LLM. - const snapshotHash = hashInsightSnapshot(snapshot); - const unchanged = await refreshUnchangedStatusInsight({ - userId, - cacheAction, - todayKey, - snapshotHash, - }); - if (unchanged) { - return { - phase: "served", - result: { - hasProvider: true, - text: unchanged.text, - cached: true, - updatedAt: unchanged.updatedAt, - }, - }; - } - - const previousContext = await getPreviousInsightContext( - userId, - "mood-status", - locale, - 12, - ); - const previousContextBlock = formatPreviousContextForPrompt( - previousContext, - locale, - ); - - // v1.12.7 — diversity / anti-repetition block (see blood-pressure-status). - // MOOD is mood-entry backed (not a MeasurementType), so the cross-metric - // RELATIONS sub-block is sourced from the snapshot's own crossMetricContext - // rather than the measurement discovery engine; relations stays empty here - // and the variety / data-strength / repetition signals carry the rotation. - const varietyLead = pickVarietyLead(userId, "mood", todayKey); - const steadyRun = computeSteadyRun(moodGraded.weekly, moodGraded.monthly); - const assessmentContextBlock = buildAssessmentContextBlock( - { - varietyLead, - dataStrength: { - points: moodSeries.daily.length, - newestDaysAgo: newestEntryDaysAgo, - }, - repeatCount: steadyRun, - relations: [], - }, - locale, - ); - - // The provider call is deferred to the caller — the single-card path runs - // ONE completion, the batch path folds this prompt into one shared call. - // A timeout / error is a transient miss (fallback served, no assessment - // persisted); `none` (no provider / no consent) serves the no-key text. return { - phase: "pending", - metric: "mood", - userId, - cacheAction, - systemPrompt: getMoodSystemPrompt(locale), - userPrompt: getMoodUserPrompt( - snapshotJson, - todayKey, - locale, - previousContextBlock, - assessmentContextBlock, - // v1.28.40 — rotating opener hint, per (user, metric, day). - openerArchetypeHint(`${userId}:mood:${todayKey}`, locale as Locale), - ), - snapshotHash, - // v1.12.7 — match the archetype cards' 0.45. - temperature: 0.45, - noProvider: { - hasProvider: false, - text: getNoKeyMoodStatusText(locale, moodSignal), - cached: true, - updatedAt: null, - }, - timeout: (reason): StatusCardResult => - returnTimeoutFallback({ - cacheAction, - reason, - userId, - todayKey, - stubText: getNoKeyMoodStatusText(locale, moodSignal), - }), - finalize: async (outcome): Promise => { - // Outbound safety screen. The only transform here used to be - // whitespace-normalisation, so a dose-change imperative or a fabricated - // clinical risk score persisted as the day's cached assessment. Policy - // for a background-generated card is WITHHOLD: serve the deterministic - // stub and persist no model text (see `finalizeStatusSummary`). - const screened = finalizeStatusSummary(outcome.content, locale); - if (!screened.ok) { - annotate({ - action: { name: "insights.status.outbound_blocked" }, - meta: { cacheAction, reason: screened.reason }, - }); - return returnTimeoutFallback({ - cacheAction, - reason: "screened", - userId: userId, - todayKey: todayKey, - stubText: getNoKeyMoodStatusText(locale, moodSignal), - }); - } - const summary = screened.text; - if (!summary) { - throw new Error("Mood-status summary was empty after normalization"); - } - const updatedAt = await persistStatusInsight({ - userId, - cacheAction, - todayKey, - text: summary, - snapshotHash, - }); - return { - hasProvider: true, - text: summary, - cached: false, - updatedAt, - }; - }, + weightSeries, + sysSeries, + pulseSeries, + moodVsWeightCorrelation, + moodVsSysCorrelation, + moodVsPulseCorrelation, + tagSummary, + tagInfluence, + betterDays, + factorCrosstab, + timeOfDay, + stability, + narratives, }; } -export function resolveMoodStatusLocale( - locale: string | null | undefined, -): SupportedLocale { - return normalizeLocale(locale); +/** + * The cross-metric block: each of weight, systolic and pulse with at least + * three daily points, summarised with its mood correlation. + */ +function moodCrossMetricContext( + r: Awaited>, +) { + return r.weightSeries.daily.length >= 3 || + r.sysSeries.daily.length >= 3 || + r.pulseSeries.daily.length >= 3 + ? { + weight: + r.weightSeries.daily.length >= 3 + ? { + summary: summarizeSeries( + r.weightSeries.daily.map((bucket) => ({ + value: bucket.value, + })), + ), + correlation: r.moodVsWeightCorrelation, + } + : null, + bloodPressureSystolic: + r.sysSeries.daily.length >= 3 + ? { + summary: summarizeSeries( + r.sysSeries.daily.map((bucket) => ({ + value: bucket.value, + })), + ), + correlation: r.moodVsSysCorrelation, + } + : null, + pulse: + r.pulseSeries.daily.length >= 3 + ? { + summary: summarizeSeries( + r.pulseSeries.daily.map((bucket) => ({ + value: bucket.value, + })), + ), + correlation: r.moodVsPulseCorrelation, + } + : null, + } + : null; } + +const MOOD_STATUS_CARD: StatusCardPlugin = { + scope: "mood", + codec: textStatusCodec, + unavailableText: (locale) => getNoKeyMoodStatusText(locale), + emptyMessage: "Mood-status summary was empty after normalization", + build: buildMoodSnapshot, +}; diff --git a/src/lib/insights/pulse-status.ts b/src/lib/insights/pulse-status.ts index 3f7dfee82..31ee348d6 100644 --- a/src/lib/insights/pulse-status.ts +++ b/src/lib/insights/pulse-status.ts @@ -5,18 +5,7 @@ import { getPulseSystemPrompt, getPulseUserPrompt, } from "@/lib/ai/prompts/pulse"; -import { openerArchetypeHint } from "@/lib/ai/prompts/opener-archetype"; -import type { Locale } from "@/lib/i18n/config"; -import { - formatPreviousContextForPrompt, - getPreviousInsightContext, -} from "@/lib/insights/memory"; -import { - buildAssessmentContextBlock, - computeSteadyRun, - pickVarietyLead, -} from "@/lib/insights/assessment-context"; -import { getRelevantCorrelationsForMetric } from "@/lib/insights/metric-correlation-context"; +import { computeSteadyRun } from "@/lib/insights/assessment-context"; import { getAgeFromDateOfBirth, getPersonalizedPulseTarget, @@ -27,34 +16,25 @@ import { applyPayloadBudget } from "@/lib/insights/bucket-series"; import { buildGradedSeriesFromPoints, buildGradedSeriesWithRollups, - degradeStatusSnapshotToBudget, } from "@/lib/insights/graded-series"; -import { statusSnapshotCharsForRecord } from "@/lib/ai/capabilities/usage"; import { buildMetricSignal } from "@/lib/insights/metric-signal"; -import { - type SupportedLocale, - normalizeLocale, - finalizeStatusSummary, - persistStatusInsight, - round, - summarizeSeries, -} from "@/lib/insights/status-shared"; -import { - readFreshStatusText, - refreshUnchangedStatusInsight, - resolveReadOnlyStatusMiss, - statusCacheAction, -} from "@/lib/insights/status-cache"; -import { hashInsightSnapshot } from "@/lib/insights/snapshot-hash"; -import { returnTimeoutFallback } from "@/lib/insights/timeout-fallback"; +import { round, summarizeSeries } from "@/lib/insights/status-shared"; import { runPreparedStatusCard, type PreparedStatusCard, - type StatusCardResult, } from "@/lib/insights/status-card-generation"; -import { annotate } from "@/lib/logging/context"; +import { + coverageSpan, + prepareStatusCard, + textStatusCodec, + type StatusCardBuild, + type StatusCardContext, + type StatusCardOptions, + type StatusCardPlugin, + type StatusTextResult, +} from "@/lib/insights/status-pipeline"; import { binaryReferenceSex } from "@/lib/profile/sex"; -import { resolveUserTimezone, userDayKey } from "@/lib/tz/resolver"; +import { userDayKey } from "@/lib/tz/resolver"; /** * Public entry — unchanged signature. Prepares the card (cache-read, @@ -65,112 +45,31 @@ import { resolveUserTimezone, userDayKey } from "@/lib/tz/resolver"; */ export async function generatePulseStatusForUser( userId: string, - options?: { - locale?: string | null; - force?: boolean; - /** v1.8.3 — read-only navigation path; see weight-status for the rationale. */ - readOnly?: boolean; - }, -): Promise<{ - hasProvider: boolean; - text: string | null; - cached: boolean; - updatedAt: string | null; - preparing?: boolean; - /** v1.9.0 — last-good text served while a refresh is in flight; keep polling. */ - revalidating?: boolean; -}> { + options?: StatusCardOptions, +): Promise { const prepared = await preparePulseStatusForUser(userId, options); - const result = await runPreparedStatusCard(prepared); - return result as { - hasProvider: boolean; - text: string | null; - cached: boolean; - updatedAt: string | null; - preparing?: boolean; - revalidating?: boolean; - }; + return (await runPreparedStatusCard(prepared)) as StatusTextResult; } /** - * v1.18.7 — everything up to (not including) the provider call. - * Returns a finished `served` result for every path that never needed the - * LLM, or a `pending` descriptor the single-card path and the batch path - * both drive. See `status-card-generation.ts` for the contract. + * Everything up to (not including) the provider call, through the shared + * status pipeline (`status-pipeline.ts`); this card supplies its snapshot. */ -export async function preparePulseStatusForUser( +export function preparePulseStatusForUser( userId: string, - options?: { - locale?: string | null; - force?: boolean; - /** v1.8.3 — read-only navigation path; see weight-status for the rationale. */ - readOnly?: boolean; - }, + options?: StatusCardOptions, ): Promise { - const locale = normalizeLocale(options?.locale); - const force = options?.force === true; - const readOnly = options?.readOnly === true; - const cacheAction = statusCacheAction("pulse", locale); - // v1.30.3 — resolve the user's own tz BEFORE the day-key so the - // cache rolls over at the user's local midnight, not Berlin's. This has to - // happen ahead of the cache read below (the earliest possible return path), - // not just the later day-bucketing reads. - const userTz = await resolveUserTimezone(userId); - const todayKey = userDayKey(new Date(), userTz); - - const cached = await readFreshStatusText({ - userId, - cacheAction, - todayKey, - force, - }); - if (cached) { - return { - phase: "served", - result: { - hasProvider: true, - text: cached.text, - cached: true, - updatedAt: cached.updatedAt, - }, - }; - } - - if (readOnly) { - const outcome = await resolveReadOnlyStatusMiss({ - userId, - metric: "pulse", - locale, - }); - // Unavailable for any reason serves the deterministic line and enqueues - // nothing. `hasProvider` is provider presence only. - if (outcome.kind === "unavailable") { - return { - phase: "served", - result: { - hasProvider: outcome.hasProvider, - text: getNoKeyPulseStatusText(locale), - cached: true, - updatedAt: null, - }, - }; - } - // v1.8.7 — stale-while-revalidate: serve the last good assessment (if - // any) instantly while the worker re-warms the cache; only fall to the - // empty preparing skeleton when none was ever produced. - return { - phase: "served", - result: { - hasProvider: true, - text: outcome.lastGood?.text ?? null, - cached: outcome.lastGood !== null, - updatedAt: outcome.lastGood?.updatedAt ?? null, - preparing: outcome.lastGood === null, - revalidating: outcome.revalidating, - }, - }; - } + return prepareStatusCard(PULSE_STATUS_CARD, userId, options); +} +/** + * The resting-pulse snapshot: the PULSE stream graded for the prompt, the + * resting band judged against the RESTING series, and the mood context. + */ +async function buildPulseSnapshot( + ctx: StatusCardContext, +): Promise> { + const { userId, locale, userTz, todayKey } = ctx; const user = await prisma.user.findUnique({ where: { id: userId }, select: { @@ -257,62 +156,25 @@ export async function preparePulseStatusForUser( binaryReferenceSex(user?.gender), ); - // v1.15.12 — resting-target in-target % over the RESTING series - // (preferring RESTING_HEART_RATE, else the low-percentile PULSE proxy), - // NOT the raw PULSE daily buckets which mix in workout HR. Scoring the - // raw stream against a resting band counted a workout day as "over - // target" and tanked the %. - const restingResolved = resolveRestingPulseSeries({ - restingSamples: restingMeasurements.map((m) => ({ - measuredAt: m.measuredAt, - value: m.value, - })), - pulseSamples: measurements.map((m) => ({ - measuredAt: m.measuredAt, - value: m.value, - })), - dayKeyOf: (d: Date) => userDayKey(d, userTz), + const inTargetPctLast30DailyPoints = restingInTargetPct({ + restingMeasurements, + measurements, + target: pulseTarget, + now, + userTz, }); - const thirtyDaysAgoMs = now.getTime() - 30 * 24 * 60 * 60 * 1000; - const recentResting = restingResolved.series.filter( - (p) => p.measuredAt.getTime() >= thirtyDaysAgoMs, - ); - const inTargetPctLast30DailyPoints = - recentResting.length === 0 - ? null - : round( - (recentResting.filter( - (entry) => - entry.value >= pulseTarget.greenMin && - entry.value <= pulseTarget.greenMax, - ).length / - recentResting.length) * - 100, - 1, - ); // Daily buckets are sorted oldest-first by dayOffset (lower = newer). const latestPulse = pulseSeries.daily[0] ?? null; const previousPulse = pulseSeries.daily[1] ?? null; - const oldestMeasurement = - measurements.length > 0 ? measurements[0].measuredAt : null; - const newestMeasurement = - measurements.length > 0 - ? measurements[measurements.length - 1].measuredAt - : null; - const totalSpanDays = - oldestMeasurement && newestMeasurement - ? Math.round( - (newestMeasurement.getTime() - oldestMeasurement.getTime()) / - (24 * 60 * 60 * 1000), - ) - : 0; - const newestMeasurementDaysAgo = newestMeasurement - ? Math.round( - (Date.now() - newestMeasurement.getTime()) / (24 * 60 * 60 * 1000), - ) - : null; + const { totalSpanDays, newestDaysAgo: newestMeasurementDaysAgo } = + coverageSpan( + measurements.length > 0 ? measurements[0].measuredAt : null, + measurements.length > 0 + ? measurements[measurements.length - 1].measuredAt + : null, + ); // v1.18.10 — feed the model the finished recent-vs-baseline // comparison + normal-swing verdict instead of asking it to derive them from @@ -369,152 +231,78 @@ export async function preparePulseStatusForUser( : null, }; - const shed = degradeStatusSnapshotToBudget( - snapshot as unknown as Record, - { maxChars: await statusSnapshotCharsForRecord(userId) }, - ); - const snapshotJson = JSON.stringify(snapshot); - - annotate({ - action: { name: cacheAction }, - meta: { - payload_size_bytes: snapshotJson.length, - ...(shed.length > 0 ? { snapshot_shed: shed } : {}), - }, - }); - - // Content-hash gate (v1.16.8): when the snapshot is unchanged since the - // last real assessment, refresh the cache timestamp and skip the LLM. - const snapshotHash = hashInsightSnapshot(snapshot); - const unchanged = await refreshUnchangedStatusInsight({ - userId, - cacheAction, - todayKey, - snapshotHash, - }); - if (unchanged) { - return { - phase: "served", - result: { - hasProvider: true, - text: unchanged.text, - cached: true, - updatedAt: unchanged.updatedAt, - }, - }; - } - - const previousContext = await getPreviousInsightContext( - userId, - "pulse-status", - locale, - 12, - ); - const previousContextBlock = formatPreviousContextForPrompt( - previousContext, - locale, - ); - - // v1.12.7 — diversity / anti-repetition block (see blood-pressure-status). - const varietyLead = pickVarietyLead(userId, "pulse", todayKey); - const steadyRun = computeSteadyRun(pulseGraded.weekly, pulseGraded.monthly); - const relations = await getRelevantCorrelationsForMetric( - userId, - "PULSE", - locale, - ); - const assessmentContextBlock = buildAssessmentContextBlock( - { - varietyLead, - dataStrength: { + return { + snapshot, + finish: () => ({ + context: { + repeatCount: computeSteadyRun(pulseGraded.weekly, pulseGraded.monthly), points: pulseSeries.daily.length, newestDaysAgo: newestMeasurementDaysAgo, + relationsFor: "PULSE", }, - repeatCount: steadyRun, - relations, - }, - locale, - ); - - // The provider call is deferred to the caller — the single-card path runs - // ONE completion, the batch path folds this prompt into one shared call. - // A timeout / error is a transient miss (fallback served, no assessment - // persisted); `none` (no provider / no consent) serves the no-key text. - return { - phase: "pending", - metric: "pulse", - userId, - cacheAction, - systemPrompt: getPulseSystemPrompt(locale), - userPrompt: getPulseUserPrompt( - snapshotJson, - todayKey, - locale, - previousContextBlock, - assessmentContextBlock, - // v1.28.40 — rotating opener hint, per (user, metric, day). - openerArchetypeHint(`${userId}:pulse:${todayKey}`, locale as Locale), - ), - snapshotHash, - // v1.12.7 — match the archetype cards' 0.45. - temperature: 0.45, - noProvider: { - hasProvider: false, - text: getNoKeyPulseStatusText(locale, pulseSignal), - cached: true, - updatedAt: null, - }, - timeout: (reason): StatusCardResult => - returnTimeoutFallback({ - cacheAction, - reason, - userId, - todayKey, - stubText: getNoKeyPulseStatusText(locale, pulseSignal), + fallbackText: getNoKeyPulseStatusText(locale, pulseSignal), + prompts: (snapshotJson, context) => ({ + systemPrompt: getPulseSystemPrompt(locale), + userPrompt: getPulseUserPrompt( + snapshotJson, + todayKey, + locale, + context.previousContextBlock, + context.assessmentContextBlock, + context.openerHint, + ), }), - finalize: async (outcome): Promise => { - // Outbound safety screen. The only transform here used to be - // whitespace-normalisation, so a dose-change imperative or a fabricated - // clinical risk score persisted as the day's cached assessment. Policy - // for a background-generated card is WITHHOLD: serve the deterministic - // stub and persist no model text (see `finalizeStatusSummary`). - const screened = finalizeStatusSummary(outcome.content, locale); - if (!screened.ok) { - annotate({ - action: { name: "insights.status.outbound_blocked" }, - meta: { cacheAction, reason: screened.reason }, - }); - return returnTimeoutFallback({ - cacheAction, - reason: "screened", - userId: userId, - todayKey: todayKey, - stubText: getNoKeyPulseStatusText(locale, pulseSignal), - }); - } - const summary = screened.text; - if (!summary) { - throw new Error("Pulse-status summary was empty after normalization"); - } - const updatedAt = await persistStatusInsight({ - userId, - cacheAction, - todayKey, - text: summary, - snapshotHash, - }); - return { - hasProvider: true, - text: summary, - cached: false, - updatedAt, - }; - }, + detail: undefined, + }), }; } -export function resolvePulseStatusLocale( - locale: string | null | undefined, -): SupportedLocale { - return normalizeLocale(locale); +/** + * Resting-target in-target % over the RESTING series (preferring + * RESTING_HEART_RATE, else the low-percentile PULSE proxy), NOT the raw PULSE + * daily buckets which mix in workout HR. Scoring the raw stream against a + * resting band counted a workout day as "over target" and tanked the %. + */ +function restingInTargetPct(args: { + restingMeasurements: ReadonlyArray<{ measuredAt: Date; value: number }>; + measurements: ReadonlyArray<{ measuredAt: Date; value: number }>; + target: { greenMin: number; greenMax: number }; + now: Date; + userTz: string; +}): number | null { + const { target, now, userTz } = args; + const restingResolved = resolveRestingPulseSeries({ + restingSamples: args.restingMeasurements.map((m) => ({ + measuredAt: m.measuredAt, + value: m.value, + })), + pulseSamples: args.measurements.map((m) => ({ + measuredAt: m.measuredAt, + value: m.value, + })), + dayKeyOf: (d: Date) => userDayKey(d, userTz), + }); + const thirtyDaysAgoMs = now.getTime() - 30 * 24 * 60 * 60 * 1000; + const recentResting = restingResolved.series.filter( + (p) => p.measuredAt.getTime() >= thirtyDaysAgoMs, + ); + return recentResting.length === 0 + ? null + : round( + (recentResting.filter( + (entry) => + entry.value >= target.greenMin && entry.value <= target.greenMax, + ).length / + recentResting.length) * + 100, + 1, + ); } + +const PULSE_STATUS_CARD: StatusCardPlugin = { + scope: "pulse", + codec: textStatusCodec, + unavailableText: (locale) => getNoKeyPulseStatusText(locale), + emptyMessage: "Pulse-status summary was empty after normalization", + build: buildPulseSnapshot, +}; diff --git a/src/lib/insights/signals-of-day.ts b/src/lib/insights/signals-of-day.ts index 3be2c7e0c..b4a52bd23 100644 --- a/src/lib/insights/signals-of-day.ts +++ b/src/lib/insights/signals-of-day.ts @@ -13,7 +13,7 @@ import { avgInWindow, roundedSpread, toDataPoints, -} from "@/lib/insights/features"; +} from "@/lib/insights/feature-window"; import { SAME_HOURS_MIN_DAYS, SAME_HOURS_TYPES, diff --git a/src/lib/insights/status-card-generation.ts b/src/lib/insights/status-card-generation.ts index 84c73ffec..ddab24292 100644 --- a/src/lib/insights/status-card-generation.ts +++ b/src/lib/insights/status-card-generation.ts @@ -75,9 +75,15 @@ export interface PendingStatusCard { systemPrompt: string; userPrompt: string; /** Snapshot fingerprint — persisted so the next run's hash gate can skip. */ - snapshotHash: string; + snapshotHash?: string; /** Per-card sampling temperature (the cards run a touch warmer at 0.45). */ temperature: number; + /** + * The answer budget for the single-card call; `AI_BUDGETS.status` when + * omitted. The archetype cards (generic metric, biomarker) compose a longer + * prompt and pass `AI_BUDGETS.statusArchetype`. + */ + maxTokens?: number; /** * The deterministic no-key / no-consent result. The provider call returns * `{ kind: "none" }` for both, and the prepare step already knows the @@ -129,7 +135,7 @@ export async function runPreparedStatusCard( systemPrompt: prepared.systemPrompt, userPrompt: prepared.userPrompt, temperature: prepared.temperature, - maxTokens: AI_BUDGETS.status.maxTokens, + maxTokens: prepared.maxTokens ?? AI_BUDGETS.status.maxTokens, }); if (outcome.kind === "none") { diff --git a/src/lib/insights/status-pipeline.ts b/src/lib/insights/status-pipeline.ts new file mode 100644 index 000000000..8a51a0e13 --- /dev/null +++ b/src/lib/insights/status-pipeline.ts @@ -0,0 +1,613 @@ +/** + * The pipeline every per-card status generator runs, in stages. + * + * A card is a plug-in: what it reads and how it shapes that into a snapshot, + * the prompts it sends, and, for the one card that stores more than a note, + * how its note is stored and what its result looks like. Everything around + * that is the same for every card and lives here: + * + * openStatusCard locale, note name, the reader's day + * readCachedStatus today's note, when there is one + * resolveReadOnlyStatus a navigation read never waits on a provider + * gateUnchangedInputs slow-moving inputs unchanged: re-date the note + * sealStatusSnapshot budget, serialise, fingerprint + * refreshUnchangedStatus snapshot unchanged: re-date the note + * buildStatusPromptContext previous note, variety lead, data strength, + * relations, opener hint + * pendingStatusCard the descriptor the single-card path + * (`runPreparedStatusCard`) and the batch drive + * + * `prepareStatusCard` runs them in order for a plug-in. The generic metric + * card and the biomarker card compose the same stages themselves, because + * each puts an empty-data gate between the cache read and the read-only path. + */ +import type { MeasurementType } from "@/generated/prisma/client"; +import { openerArchetypeHint } from "@/lib/ai/prompts/opener-archetype"; +import { statusSnapshotCharsForRecord } from "@/lib/ai/capabilities/usage"; +import type { Locale } from "@/lib/i18n/config"; +import { + buildAssessmentContextBlock, + pickVarietyLead, +} from "@/lib/insights/assessment-context"; +import { degradeStatusSnapshotToBudget } from "@/lib/insights/graded-series"; +import { + formatPreviousContextForPrompt, + getPreviousInsightContext, +} from "@/lib/insights/memory"; +import { getRelevantCorrelationsForMetric } from "@/lib/insights/metric-correlation-context"; +import { hashInsightSnapshot } from "@/lib/insights/snapshot-hash"; +import { + gateUnchangedStatusInput, + readFreshStatusText, + refreshUnchangedStatusInsight, + resolveReadOnlyStatusMiss, + statusCacheAction, + type ReadOnlyMissOutcome, +} from "@/lib/insights/status-cache"; +import type { + PendingStatusCard, + PreparedStatusCard, + StatusCardResult, +} from "@/lib/insights/status-card-generation"; +import { + finalizeStatusSummary, + normalizeLocale, + persistStatusInsight, + type SupportedLocale, +} from "@/lib/insights/status-shared"; +import { + returnTimeoutFallback, + type StatusFallbackReason, +} from "@/lib/insights/timeout-fallback"; +import type { + InsightStatusMetric, + InsightStatusScope, +} from "@/lib/jobs/insight-status-generate-shared"; +import { annotate } from "@/lib/logging/context"; +import { resolveUserTimezone, userDayKey } from "@/lib/tz/resolver"; + +/** What every public generator entry accepts. */ +export interface StatusCardOptions { + locale?: string | null; + force?: boolean; + /** + * Read-only mode for the navigation path. On a cache miss the generator + * does NOT run the SQL gather + blocking LLM inline; it enqueues an + * out-of-band generation and returns a `preparing` shape so the request + * never awaits the provider. The worker (and the nightly pre-generate cron) + * call with `readOnly: false` to actually generate. + */ + readOnly?: boolean; +} + +/** The note-only result every card but medication compliance returns. */ +export type StatusTextResult = { + hasProvider: boolean; + text: string | null; + cached: boolean; + updatedAt: string | null; + preparing?: boolean; + /** Last-good text served while a refresh is in flight; keep polling. */ + revalidating?: boolean; +}; + +/** One card's run: who, which note, and the reader's day. */ +export interface StatusCardContext< + S extends InsightStatusScope = InsightStatusScope, +> { + userId: string; + scope: S; + locale: SupportedLocale; + force: boolean; + readOnly: boolean; + /** `insights..` — the note name and the consent-gate key. */ + cacheAction: string; + userTz: string; + todayKey: string; +} + +/** The fingerprints a written note carries for the next run's gates. */ +export interface StatusNoteHashes { + snapshotHash?: string; + inputHash?: string; +} + +/** + * How a card stores its note and shapes its result. Every card stores one + * note and answers `{ hasProvider, text, cached, updatedAt }` + * (`textStatusCodec`); medication compliance stores per-medication lines + * beside it and answers with them, so it brings its own codec. `I` is the + * card's per-run detail the codec needs on the way out (those lines). + */ +export interface StatusNoteCodec { + readFresh(ctx: StatusCardContext): Promise; + refreshUnchanged( + ctx: StatusCardContext, + snapshotHash: string, + ): Promise; + readOnly(outcome: ReadOnlyMissOutcome, unavailableText: string): R; + noProvider(text: string): R; + fallback( + ctx: StatusCardContext, + reason: StatusFallbackReason, + stubText: string, + detail: I, + ): R; + persist( + ctx: StatusCardContext, + text: string, + hashes: StatusNoteHashes, + detail: I, + ): Promise; +} + +/** The standard codec: one encrypted note, a text-shaped result. */ +export const textStatusCodec: StatusNoteCodec = { + async readFresh(ctx) { + // Serve today's real assessment when present. The shared reader rejects + // timeout stubs, so a single stall no longer pins the fallback for the + // day. + const cached = await readFreshStatusText({ + userId: ctx.userId, + cacheAction: ctx.cacheAction, + todayKey: ctx.todayKey, + force: ctx.force, + }); + if (!cached) return null; + return { + hasProvider: true, + text: cached.text, + cached: true, + updatedAt: cached.updatedAt, + }; + }, + async refreshUnchanged(ctx, snapshotHash) { + const unchanged = await refreshUnchangedStatusInsight({ + userId: ctx.userId, + cacheAction: ctx.cacheAction, + todayKey: ctx.todayKey, + snapshotHash, + }); + if (!unchanged) return null; + return { + hasProvider: true, + text: unchanged.text, + cached: true, + updatedAt: unchanged.updatedAt, + }; + }, + readOnly(outcome, unavailableText) { + // Unavailable for any reason serves the deterministic line and enqueues + // nothing. `hasProvider` is provider presence only. + if (outcome.kind === "unavailable") { + return { + hasProvider: outcome.hasProvider, + text: unavailableText, + cached: true, + updatedAt: null, + }; + } + // Stale-while-revalidate: serve the last good assessment (if any) + // instantly while the worker re-warms the cache; only fall to the empty + // preparing skeleton when none was ever produced. + return { + hasProvider: true, + text: outcome.lastGood?.text ?? null, + cached: outcome.lastGood !== null, + updatedAt: outcome.lastGood?.updatedAt ?? null, + preparing: outcome.lastGood === null, + revalidating: outcome.revalidating, + }; + }, + noProvider(text) { + return { hasProvider: false, text, cached: true, updatedAt: null }; + }, + fallback(ctx, reason, stubText) { + return returnTimeoutFallback({ + cacheAction: ctx.cacheAction, + reason, + userId: ctx.userId, + todayKey: ctx.todayKey, + stubText, + }); + }, + async persist(ctx, text, hashes) { + const updatedAt = await persistStatusInsight({ + userId: ctx.userId, + cacheAction: ctx.cacheAction, + todayKey: ctx.todayKey, + text, + snapshotHash: hashes.snapshotHash, + inputHash: hashes.inputHash, + }); + return { hasProvider: true, text, cached: false, updatedAt }; + }, +}; + +/** + * Resolve the run: locale, the note name, and the reader's day. The user's + * own time zone is resolved BEFORE the day key so the cache rolls over at the + * user's local midnight, not Berlin's; it has to happen ahead of the cache + * read (the earliest possible return path), not just the later day-bucketing + * reads. + */ +export async function openStatusCard( + userId: string, + scope: S, + options: StatusCardOptions | undefined, +): Promise> { + const locale = normalizeLocale(options?.locale); + const cacheAction = statusCacheAction(scope, locale); + const userTz = await resolveUserTimezone(userId); + return { + userId, + scope, + locale, + force: options?.force === true, + readOnly: options?.readOnly === true, + cacheAction, + userTz, + todayKey: userDayKey(new Date(), userTz), + }; +} + +/** Today's stored note as the card's result, or `null` on a miss. */ +export function readCachedStatus( + ctx: StatusCardContext, + codec: StatusNoteCodec, +): Promise { + return codec.readFresh(ctx); +} + +/** + * The read-only navigation path: never block on the provider. Enqueue the + * generation out of band and serve the last good note, the preparing + * skeleton, or (no provider, no consent) the deterministic line. + */ +export async function resolveReadOnlyStatus( + ctx: StatusCardContext, + codec: StatusNoteCodec, + unavailableText: string, +): Promise { + const outcome = await resolveReadOnlyStatusMiss({ + userId: ctx.userId, + metric: ctx.scope, + locale: ctx.locale, + }); + return codec.readOnly(outcome, unavailableText); +} + +/** + * The input gate for a slow-moving card: when the cheap fingerprint of its + * salient inputs matches the stored note's, re-date that note under today's + * key and skip the build and the provider. `null` when the build must run. + */ +export async function gateUnchangedInputs( + ctx: StatusCardContext, + inputHash: string, + force: boolean, +): Promise { + const unchanged = await gateUnchangedStatusInput({ + userId: ctx.userId, + cacheAction: ctx.cacheAction, + todayKey: ctx.todayKey, + inputHash, + force, + }); + if (!unchanged) return null; + return { + hasProvider: true, + text: unchanged.text, + cached: true, + updatedAt: unchanged.updatedAt, + }; +} + +/** + * Fit the snapshot to the record's prompt budget (shedding the coarsest + * series first), serialise it, record its size, and fingerprint it. + */ +export async function sealStatusSnapshot( + ctx: StatusCardContext, + snapshot: object, +): Promise<{ snapshotJson: string; snapshotHash: string }> { + const shed = degradeStatusSnapshotToBudget( + snapshot as unknown as Record, + { maxChars: await statusSnapshotCharsForRecord(ctx.userId) }, + ); + const snapshotJson = JSON.stringify(snapshot); + + annotate({ + action: { name: ctx.cacheAction }, + meta: { + payload_size_bytes: snapshotJson.length, + ...(shed.length > 0 ? { snapshot_shed: shed } : {}), + }, + }); + + return { snapshotJson, snapshotHash: hashInsightSnapshot(snapshot) }; +} + +/** + * Content-hash gate: when the snapshot is unchanged since the last real + * assessment, re-date the stored note and skip the LLM. + */ +export function refreshUnchangedStatus( + ctx: StatusCardContext, + codec: StatusNoteCodec, + snapshotHash: string, +): Promise { + return codec.refreshUnchanged(ctx, snapshotHash); +} + +/** What the anti-repetition block is built from, per card. */ +export interface StatusContextInputs { + /** The key the variety lead rotates on; the card's scope unless it differs. */ + varietyKey?: string; + /** Consecutive steady weeks/months, or 0 where a card has no single mean. */ + repeatCount: number; + points: number; + newestDaysAgo: number | null; + /** + * The discovery channel whose FDR-surviving relations the block lists, or + * `null` for a card with no measurement channel of its own (no fetch). + */ + relationsFor: MeasurementType | null; +} + +/** The prompt blocks every assessment card carries beside its snapshot. */ +export interface StatusPromptContext { + previousContextBlock: string; + assessmentContextBlock: string; + openerHint: string; +} + +/** + * The previous-assessment comparison, the diversity / anti-repetition block + * and the rotating opener hint. All formatting of already-computed data; the + * correlation fetch is best-effort and resolves to no block on failure, never + * a generation failure. The opener hint is keyed per (user, card, day) and + * activates the base prompt's verdict-first opener branch. + */ +export async function buildStatusPromptContext( + ctx: StatusCardContext, + inputs: StatusContextInputs, +): Promise { + const previousContext = await getPreviousInsightContext( + ctx.userId, + `${ctx.scope}-status`, + ctx.locale, + 12, + ); + const previousContextBlock = formatPreviousContextForPrompt( + previousContext, + ctx.locale, + ); + + const varietyLead = pickVarietyLead( + ctx.userId, + inputs.varietyKey ?? ctx.scope, + ctx.todayKey, + ); + const relations = + inputs.relationsFor === null + ? [] + : await getRelevantCorrelationsForMetric( + ctx.userId, + inputs.relationsFor, + ctx.locale, + ); + const assessmentContextBlock = buildAssessmentContextBlock( + { + varietyLead, + dataStrength: { + points: inputs.points, + newestDaysAgo: inputs.newestDaysAgo, + }, + repeatCount: inputs.repeatCount, + relations, + }, + ctx.locale, + ); + + return { + previousContextBlock, + assessmentContextBlock, + openerHint: openerArchetypeHint( + `${ctx.userId}:${ctx.scope}:${ctx.todayKey}`, + ctx.locale as Locale, + ), + }; +} + +/** Everything a pending card needs beyond the run itself. */ +export interface PendingStatusSpec { + systemPrompt: string; + userPrompt: string; + hashes: StatusNoteHashes; + temperature: number; + /** The answer budget; the status default when omitted. */ + maxTokens?: number; + /** + * The deterministic line served with no provider, after a timeout or error, + * and when the reply is withheld. + */ + fallbackText: string; + /** Thrown if a reply survives the screen with no text at all. */ + emptyMessage: string; + detail: I; +} + +/** + * The descriptor the single-card path and the batch both drive. A timeout or + * error is a transient miss (fallback served, short negative window, no + * assessment persisted); `none` (no provider / no consent) serves the no-key + * line. + * + * The reply passes the outbound safety screen before anything is stored. + * The only transform here used to be whitespace normalisation, so a + * dose-change imperative or a fabricated clinical risk score persisted as the + * day's cached assessment. Policy for a background-generated card is + * WITHHOLD: serve the deterministic stub and persist no model text (see + * `finalizeStatusSummary`). + */ +export function pendingStatusCard( + ctx: StatusCardContext, + codec: StatusNoteCodec, + spec: PendingStatusSpec, +): PendingStatusCard { + return { + phase: "pending", + metric: ctx.scope, + userId: ctx.userId, + cacheAction: ctx.cacheAction, + systemPrompt: spec.systemPrompt, + userPrompt: spec.userPrompt, + ...(spec.hashes.snapshotHash !== undefined + ? { snapshotHash: spec.hashes.snapshotHash } + : {}), + temperature: spec.temperature, + ...(spec.maxTokens !== undefined ? { maxTokens: spec.maxTokens } : {}), + noProvider: codec.noProvider(spec.fallbackText), + timeout: (reason) => + codec.fallback(ctx, reason, spec.fallbackText, spec.detail), + finalize: async (outcome) => { + const screened = finalizeStatusSummary(outcome.content, ctx.locale); + if (!screened.ok) { + annotate({ + action: { name: "insights.status.outbound_blocked" }, + meta: { cacheAction: ctx.cacheAction, reason: screened.reason }, + }); + return codec.fallback(ctx, "screened", spec.fallbackText, spec.detail); + } + if (!screened.text) throw new Error(spec.emptyMessage); + return codec.persist(ctx, screened.text, spec.hashes, spec.detail); + }, + }; +} + +const DAY_MS = 24 * 60 * 60 * 1000; + +/** + * How far a card's data reaches: whole days between its oldest and newest + * reading, and how many days ago the newest one was (`null` with no data). + */ +export function coverageSpan( + oldest: Date | null, + newest: Date | null, +): { totalSpanDays: number; newestDaysAgo: number | null } { + return { + totalSpanDays: + oldest && newest + ? Math.round((newest.getTime() - oldest.getTime()) / DAY_MS) + : 0, + newestDaysAgo: newest + ? Math.round((Date.now() - newest.getTime()) / DAY_MS) + : null, + }; +} + +/** The temperature every specialised card runs at, matching the archetype cards. */ +const STATUS_CARD_TEMPERATURE = 0.45; + +/** What a card's snapshot stage hands the pipeline. */ +export interface StatusCardBuild { + snapshot: object; + /** The input fingerprint to store, for a card with an input gate. */ + inputHash?: string; + /** + * The rest of the card, read once the snapshot is sealed and the hash gate + * missed. Sealing sheds graded slices from the snapshot IN PLACE when it is + * over budget, and the series objects the snapshot carries are the card's + * own, so whatever reads them (the steady-run count) reads them here, after + * the shed, never before it. + */ + finish(): StatusCardTail; +} + +/** The part of a card that is read after its snapshot is sealed. */ +export interface StatusCardTail { + context: StatusContextInputs; + /** The deterministic line, grounded in the card's signal when it has one. */ + fallbackText: string; + prompts( + snapshotJson: string, + context: StatusPromptContext, + ): { systemPrompt: string; userPrompt: string }; + detail: I; +} + +/** A served early return from a card's snapshot stage. */ +export interface ServedBuild { + phase: "served"; + result: R; +} + +/** One specialised card, as a plug-in to `prepareStatusCard`. */ +export interface StatusCardPlugin { + scope: InsightStatusMetric; + codec: StatusNoteCodec; + /** The deterministic line a read-only miss serves when notes are unavailable. */ + unavailableText(locale: SupportedLocale): string; + /** Thrown if a reply survives the screen with no text at all. */ + emptyMessage: string; + build(ctx: StatusCardContext): Promise | ServedBuild>; +} + +/** + * Everything up to (not including) the provider call, for one specialised + * card. Returns a finished `served` result for every path that never needed + * the LLM, or a `pending` descriptor the single-card path and the batch path + * both drive. See `status-card-generation.ts` for the contract. + */ +export async function prepareStatusCard( + plugin: StatusCardPlugin, + userId: string, + options: StatusCardOptions | undefined, +): Promise { + const ctx = await openStatusCard(userId, plugin.scope, options); + + const cached = await readCachedStatus(ctx, plugin.codec); + if (cached) return { phase: "served", result: cached }; + + if (ctx.readOnly) { + return { + phase: "served", + result: await resolveReadOnlyStatus( + ctx, + plugin.codec, + plugin.unavailableText(ctx.locale), + ), + }; + } + + const build = await plugin.build(ctx); + if ("phase" in build) return build; + + const { snapshotJson, snapshotHash } = await sealStatusSnapshot( + ctx, + build.snapshot, + ); + const unchanged = await refreshUnchangedStatus( + ctx, + plugin.codec, + snapshotHash, + ); + if (unchanged) return { phase: "served", result: unchanged }; + + const tail = build.finish(); + const context = await buildStatusPromptContext(ctx, tail.context); + const { systemPrompt, userPrompt } = tail.prompts(snapshotJson, context); + return pendingStatusCard(ctx, plugin.codec, { + systemPrompt, + userPrompt, + hashes: { + snapshotHash, + ...(build.inputHash !== undefined ? { inputHash: build.inputHash } : {}), + }, + temperature: STATUS_CARD_TEMPERATURE, + fallbackText: tail.fallbackText, + emptyMessage: plugin.emptyMessage, + detail: tail.detail, + }); +} diff --git a/src/lib/insights/weight-status.ts b/src/lib/insights/weight-status.ts index c5ec126e2..c6f699472 100644 --- a/src/lib/insights/weight-status.ts +++ b/src/lib/insights/weight-status.ts @@ -4,8 +4,6 @@ import { getWeightSystemPrompt, getWeightUserPrompt, } from "@/lib/ai/prompts/weight"; -import { openerArchetypeHint } from "@/lib/ai/prompts/opener-archetype"; -import type { Locale } from "@/lib/i18n/config"; import { significantPearsonCorrelation, type PairedPoint, @@ -13,16 +11,7 @@ import { import { getNoKeyWeightStatusText } from "@/lib/insights/no-key-fallbacks"; import { resolveWeightTargetOverride } from "@/lib/analytics/effective-range"; import { buildWeightTargetFeature } from "@/lib/targets/weight-trend"; -import { - formatPreviousContextForPrompt, - getPreviousInsightContext, -} from "@/lib/insights/memory"; -import { - buildAssessmentContextBlock, - computeSteadyRun, - pickVarietyLead, -} from "@/lib/insights/assessment-context"; -import { getRelevantCorrelationsForMetric } from "@/lib/insights/metric-correlation-context"; +import { computeSteadyRun } from "@/lib/insights/assessment-context"; import { applyPayloadBudget, dayOffsetToDayKey, @@ -32,42 +21,40 @@ import { buildGradedSeriesFromPoints, buildGradedSeriesWithRollups, convertGradedSeries, - degradeStatusSnapshotToBudget, + type GradedSeries, } from "@/lib/insights/graded-series"; -import { statusSnapshotCharsForRecord } from "@/lib/ai/capabilities/usage"; import { applyDisplayTransform, applyDisplayTransformDelta, getReadingTransform, resolveUnitPreferences, transformRescales, + type DisplayTransform, + type UnitPreferences, } from "@/lib/measurements/display-transform"; import { buildMetricSignal } from "@/lib/insights/metric-signal"; import { - type SupportedLocale, - normalizeLocale, - finalizeStatusSummary, - persistStatusInsight, round, summarizeSeries, + type SupportedLocale, } from "@/lib/insights/status-shared"; -import { - computeStatusInputFingerprint, - gateUnchangedStatusInput, - readFreshStatusText, - refreshUnchangedStatusInsight, - resolveReadOnlyStatusMiss, - statusCacheAction, -} from "@/lib/insights/status-cache"; -import { hashInsightSnapshot } from "@/lib/insights/snapshot-hash"; -import { returnTimeoutFallback } from "@/lib/insights/timeout-fallback"; +import { computeStatusInputFingerprint } from "@/lib/insights/status-cache"; import { runPreparedStatusCard, type PreparedStatusCard, - type StatusCardResult, } from "@/lib/insights/status-card-generation"; -import { annotate } from "@/lib/logging/context"; -import { resolveUserTimezone, userDayKey } from "@/lib/tz/resolver"; +import { + coverageSpan, + gateUnchangedInputs, + prepareStatusCard, + textStatusCodec, + type ServedBuild, + type StatusCardBuild, + type StatusCardContext, + type StatusCardOptions, + type StatusCardPlugin, + type StatusTextResult, +} from "@/lib/insights/status-pipeline"; import { DEFAULT_TIMEZONE } from "@/lib/tz/format"; import { canonicalDailyTimestamp } from "@/lib/measurements/consolidation-tz"; @@ -120,129 +107,45 @@ function pairDailyBuckets( */ export async function generateWeightStatusForUser( userId: string, - options?: { - locale?: string | null; - force?: boolean; - readOnly?: boolean; - }, -): Promise<{ - hasProvider: boolean; - text: string | null; - cached: boolean; - updatedAt: string | null; - preparing?: boolean; - revalidating?: boolean; -}> { + options?: StatusCardOptions, +): Promise { const prepared = await prepareWeightStatusForUser(userId, options); - const result = await runPreparedStatusCard(prepared); - return result as { - hasProvider: boolean; - text: string | null; - cached: boolean; - updatedAt: string | null; - preparing?: boolean; - revalidating?: boolean; - }; + return (await runPreparedStatusCard(prepared)) as StatusTextResult; } /** - * v1.18.7 — everything up to (not including) the provider call. - * Returns a finished `served` result for every path that never needed the - * LLM, or a `pending` descriptor the single-card path and the batch path - * both drive. See `status-card-generation.ts` for the contract. + * Everything up to (not including) the provider call, through the shared + * status pipeline (`status-pipeline.ts`); this card supplies its input gate + * and its snapshot. */ -export async function prepareWeightStatusForUser( +export function prepareWeightStatusForUser( userId: string, - options?: { - locale?: string | null; - force?: boolean; - /** - * v1.8.3 — read-only mode for the navigation path. On a cache miss the - * generator does NOT run the SQL gather + blocking LLM inline; it - * enqueues an out-of-band generation and returns a `preparing` shape so - * the request never awaits the provider. The worker (and the nightly - * pre-generate cron) call with `readOnly: false` to actually generate. - */ - readOnly?: boolean; - }, + options?: StatusCardOptions, ): Promise { - const locale = normalizeLocale(options?.locale); - const force = options?.force === true; - const readOnly = options?.readOnly === true; - const cacheAction = statusCacheAction("weight", locale); - // v1.30.3 — resolve the user's own tz BEFORE the day-key so the - // cache rolls over at the user's local midnight, not Berlin's. Has to - // happen ahead of the cache read below (the earliest possible return - // path), not just the later day-bucketing reads. - const userTz = await resolveUserTimezone(userId); - const todayKey = userDayKey(new Date(), userTz); + return prepareStatusCard(WEIGHT_STATUS_CARD, userId, options); +} - // Serve today's real assessment when present. The shared reader - // rejects timeout stubs, so a single stall no longer pins the - // fallback for the day. - const cached = await readFreshStatusText({ - userId, - cacheAction, - todayKey, - force, - }); - if (cached) { - return { - phase: "served", - result: { - hasProvider: true, - text: cached.text, - cached: true, - updatedAt: cached.updatedAt, - }, - }; - } +interface WeightInputs { + weightTarget: ReturnType; + units: UnitPreferences; + mass: DisplayTransform; + inputHash: string; +} - // v1.8.3 — read-only navigation path: never block on the provider. - // Enqueue generation out of band and return preparing / no-provider. - if (readOnly) { - const outcome = await resolveReadOnlyStatusMiss({ - userId, - metric: "weight", - locale, - }); - // Unavailable for any reason serves the deterministic line and enqueues - // nothing. `hasProvider` is provider presence only. - if (outcome.kind === "unavailable") { - return { - phase: "served", - result: { - hasProvider: outcome.hasProvider, - text: getNoKeyWeightStatusText(locale), - cached: true, - updatedAt: null, - }, - }; - } - // v1.8.7 — stale-while-revalidate: serve the last good assessment - // (if any) instantly while the worker re-warms; only fall to the empty - // preparing skeleton when none was ever produced. - return { - phase: "served", - result: { - hasProvider: true, - text: outcome.lastGood?.text ?? null, - cached: outcome.lastGood !== null, - updatedAt: outcome.lastGood?.updatedAt ?? null, - preparing: outcome.lastGood === null, - revalidating: outcome.revalidating, - }, - }; - } - - // v1.18.11 — input gate for this slow-moving metric. A cheap grouped - // probe over the salient inputs (weight + BP channels + mood) fingerprints - // what the snapshot would read. On a non-forced run with an unchanged - // fingerprint the cached assessment is re-stamped under today's key and the - // whole heavy build below (bounded findMany + per-series rollup reads + - // correlation math + the provider call) is skipped. A forced run never - // gates — but still computes the fingerprint so the persisted row carries a - // current one for the next day's gate. +/** + * Input gate for this slow-moving metric. A cheap grouped probe over the + * salient inputs (weight + BP channels + mood) fingerprints what the snapshot + * would read. On a non-forced run with an unchanged fingerprint the cached + * assessment is re-stamped under today's key and the whole heavy build + * (bounded findMany + per-series rollup reads + correlation math + the + * provider call) is skipped. A forced run never gates — but still computes + * the fingerprint so the persisted row carries a current one for the next + * day's gate. + */ +async function readWeightInputs( + ctx: StatusCardContext, +): Promise> { + const { userId } = ctx; // v1.39 (#1006) — the person's own weight target decides which way is // progress. Folded into the input fingerprint so editing the target flips // the gate even when no weight row moved. @@ -258,7 +161,6 @@ export async function prepareWeightStatusForUser( unitPreference: userRow?.unitPreference, }); const mass = getReadingTransform("WEIGHT", units); - const toMass = (kg: number) => applyDisplayTransform(kg, mass); const inputHash = await computeStatusInputFingerprint({ userId, types: ["WEIGHT", "BLOOD_PRESSURE_SYS", "BLOOD_PRESSURE_DIA"], @@ -276,176 +178,48 @@ export async function prepareWeightStatusForUser( // the discovery channels + the prior-analysis anchor into the fingerprint. includeCorrelationChannels: true, }); - if (!force) { - const unchangedInput = await gateUnchangedStatusInput({ - userId, - cacheAction, - todayKey, - inputHash, - force, - }); - if (unchangedInput) { - return { - phase: "served", - result: { - hasProvider: true, - text: unchangedInput.text, - cached: true, - updatedAt: unchangedInput.updatedAt, - }, - }; - } + if (!ctx.force) { + const unchangedInput = await gateUnchangedInputs(ctx, inputHash, ctx.force); + if (unchangedInput) return { phase: "served", result: unchangedInput }; } + return { weightTarget, units, mass, inputHash }; +} - // v1.4.28 — cap the snapshot input. BP captures three types - // per day so 1095 = 365 d × 3 channels. Order desc + reverse so we - // keep the most recent year of rows. - const measurements = await prisma.measurement - .findMany({ - where: { - userId, - type: { - in: ["WEIGHT", "BLOOD_PRESSURE_SYS", "BLOOD_PRESSURE_DIA"], - }, - // Soft-deleted rows must never reach the prompt snapshot. - deletedAt: null, - }, - take: 1095, - orderBy: { measuredAt: "desc" }, - select: { - type: true, - value: true, - measuredAt: true, - }, - }) - .then((rows) => rows.reverse()); +/** + * The weight snapshot: the weight series in the reader's mass unit, the two + * BP channels as context, the weight-vs-BP correlations, and mood context. + */ +async function buildWeightSnapshot( + ctx: StatusCardContext, +): Promise | ServedBuild> { + const { userId, locale, userTz, todayKey } = ctx; + const inputs = await readWeightInputs(ctx); + if ("phase" in inputs) return inputs; + const { weightTarget, units, mass, inputHash } = inputs; + const toMass = (kg: number) => applyDisplayTransform(kg, mass); - const now = new Date(); - - // v1.2.5 — every day-bucketing pass below keys on the user's own - // calendar day. `userTz` was already resolved above for the cache day-key. - - const weightSeries = applyPayloadBudget( - measurements - .filter((measurement) => measurement.type === "WEIGHT") - .map((measurement) => ({ - measuredAt: measurement.measuredAt, - value: measurement.value, - })), - { now, tz: userTz }, - ); - - const sysSeries = applyPayloadBudget( - measurements - .filter((measurement) => measurement.type === "BLOOD_PRESSURE_SYS") - .map((measurement) => ({ - measuredAt: measurement.measuredAt, - value: measurement.value, - })), - { now, tz: userTz }, - ); - - const diaSeries = applyPayloadBudget( - measurements - .filter((measurement) => measurement.type === "BLOOD_PRESSURE_DIA") - .map((measurement) => ({ - measuredAt: measurement.measuredAt, - value: measurement.value, - })), - { now, tz: userTz }, - ); - - // Fetch mood context (optional — for enrichment only). v1.4.28 - // Cap at 90 entries. - const moodEntries = await readRecentMoodScores(userId, 90); - - const moodSeries = applyPayloadBudget( - moodEntries.map((entry) => ({ - measuredAt: entry.moodLoggedAt, - value: entry.score, - })), - { now, tz: userTz }, - ); - const moodSummary = summarizeSeries( - moodSeries.daily.map((bucket) => ({ value: bucket.value })), - ); - const moodMean = moodSummary?.mean ?? null; - - // Compact graded series for the prompt. The `*Series.daily` buckets - // above stay for the correlation pairing + latest-day focus; only the - // graded shape is embedded so the full daily arrays never ship. - // - // Weight + the two BP context channels source their recent / weekly - // slices from a bounded raw read and their monthly / yearly tail from - // the MONTH / YEAR rollup tier (with a full-history in-memory fallback - // on a cold-tier coverage miss). Mood has no rollup tier, so it stays - // an in-memory fold. - const [canonicalWeightGraded, sysGraded, diaGraded] = await Promise.all([ - buildGradedSeriesWithRollups(userId, "WEIGHT", now, userTz), - buildGradedSeriesWithRollups(userId, "BLOOD_PRESSURE_SYS", now, userTz), - buildGradedSeriesWithRollups(userId, "BLOOD_PRESSURE_DIA", now, userTz), - ]); - const weightGraded = transformRescales(mass) - ? convertGradedSeries(canonicalWeightGraded, mass, mass.decimals) - : canonicalWeightGraded; - const moodGraded = buildGradedSeriesFromPoints( - moodSeries.daily.map((b) => ({ - measuredAt: canonicalDailyTimestamp( - dayOffsetToDayKey(now, b.dayOffset, userTz), - userTz, - ), - value: b.value, - })), + const { now, - userTz, - ); + measurements, + weightSeries, + sysSeries, + diaSeries, + moodSeries, + moodMean, + canonicalWeightGraded, + weightGraded, + sysGraded, + diaGraded, + moodGraded, + } = await readWeightSeries(userId, mass, userTz); - const weightVsSystolicPairs = pairDailyBuckets( + const pairs = pairWeightWithBloodPressure( weightSeries.daily, - sysSeries.daily, - now, - userTz, - ); - const weightVsSystolicCorrelation = significantPearsonCorrelation( - weightVsSystolicPairs, - ); - - const pairedSystolicDiastolic = pairDailyBuckets( sysSeries.daily, diaSeries.daily, now, userTz, - ).map((entry) => ({ - day: entry.dayKey, - sys: entry.a, - dia: entry.b, - mean: round((entry.a + entry.b) / 2, 2), - })); - - // Synthesise a daily-bucket-shaped BP-mean series so we can reuse - // pairDailyBuckets — derive dayOffset from the offsets in sysSeries. - const sysOffsetByDay = new Map( - sysSeries.daily.map((bucket) => { - const dayKey = dayOffsetToDayKey(now, bucket.dayOffset, userTz); - return [dayKey, bucket.dayOffset]; - }), ); - const bpMeanDaily: DailyBucket[] = pairedSystolicDiastolic - .map((entry) => { - const dayOffset = sysOffsetByDay.get(entry.day); - if (dayOffset == null) return null; - return { dayOffset, value: entry.mean, n: 1 }; - }) - .filter((entry): entry is DailyBucket => entry !== null); - - const weightVsMeanBpPairs = pairDailyBuckets( - weightSeries.daily, - bpMeanDaily, - now, - userTz, - ); - const weightVsMeanBpCorrelation = - significantPearsonCorrelation(weightVsMeanBpPairs); // daily[0] = newest bucket (lowest dayOffset). const latestWeight = weightSeries.daily[0] ?? null; @@ -454,61 +228,27 @@ export async function prepareWeightStatusForUser( ? dayOffsetToDayKey(now, latestWeight.dayOffset, userTz) : null; const sameDayBp = latestWeightDay - ? (pairedSystolicDiastolic.find((entry) => entry.day === latestWeightDay) ?? + ? (pairs.systolicDiastolic.find((entry) => entry.day === latestWeightDay) ?? null) : null; - const oldestMeasurement = - measurements.length > 0 ? measurements[0].measuredAt : null; - const newestMeasurement = - measurements.length > 0 - ? measurements[measurements.length - 1].measuredAt - : null; - const totalSpanDays = - oldestMeasurement && newestMeasurement - ? Math.round( - (newestMeasurement.getTime() - oldestMeasurement.getTime()) / - (24 * 60 * 60 * 1000), - ) - : 0; - const newestMeasurementDaysAgo = newestMeasurement - ? Math.round( - (Date.now() - newestMeasurement.getTime()) / (24 * 60 * 60 * 1000), - ) - : null; + const { totalSpanDays, newestDaysAgo: newestMeasurementDaysAgo } = + coverageSpan( + measurements.length > 0 ? measurements[0].measuredAt : null, + measurements.length > 0 + ? measurements[measurements.length - 1].measuredAt + : null, + ); - // v1.18.10 — hand the model the finished recent-vs-baseline - // comparison + normal-swing verdict instead of asking it to derive them. - // Weight has no universal favourable direction or population band, so - // without a stored target the signal carries the neutral "target-band" - // framing and no normalRange. v1.39 (#1006) — with one, the direction is - // the person's own: below the band a rise is favourable, above it a fall. - const recentWeights = weightSeries.daily - .filter((bucket) => bucket.dayOffset < 7) - .map((bucket) => bucket.value); - const weightTargetFeature = buildWeightTargetFeature( + const { weightTargetFeature, weightSignal } = weightTargetAndSignal({ + weightDaily: weightSeries.daily, + latestWeight, weightTarget, - { - avg7: - recentWeights.length > 0 - ? recentWeights.reduce((sum, value) => sum + value, 0) / - recentWeights.length - : null, - latest: latestWeight?.value ?? null, - }, - units.system, - ); - const weightSignal = buildMetricSignal({ - metric: locale === "en" ? "your weight" : "dein Gewicht", - unit: mass.displayUnit, - direction: - weightTargetFeature?.position === "below" - ? "higher-better" - : weightTargetFeature?.position === "above" - ? "lower-better" - : "target-band", - graded: weightGraded, + units, + mass, + weightGraded, newestDaysAgo: newestMeasurementDaysAgo, + locale: ctx.locale, }); const snapshot = { @@ -560,11 +300,11 @@ export async function prepareWeightStatusForUser( // Keep the correlation coefficients (computed over the full // overlap) but cap the embedded pair arrays to the most recent // paired days — the raw pair lists were a major payload driver. - pairedDaily: pairedSystolicDiastolic.slice(-CORRELATION_PAIR_CAP), + pairedDaily: pairs.systolicDiastolic.slice(-CORRELATION_PAIR_CAP), }, weightVsSystolic: { - correlation: weightVsSystolicCorrelation, - pairs: weightVsSystolicPairs + correlation: pairs.weightVsSystolicCorrelation, + pairs: pairs.weightVsSystolic .slice(-CORRELATION_PAIR_CAP) .map((entry) => ({ day: entry.dayKey, @@ -573,8 +313,8 @@ export async function prepareWeightStatusForUser( })), }, weightVsMeanBloodPressure: { - correlation: weightVsMeanBpCorrelation, - pairs: weightVsMeanBpPairs.slice(-CORRELATION_PAIR_CAP).map((entry) => ({ + correlation: pairs.weightVsMeanBpCorrelation, + pairs: pairs.weightVsMeanBp.slice(-CORRELATION_PAIR_CAP).map((entry) => ({ day: entry.dayKey, weight: toMass(round(entry.a, 2)), meanBloodPressure: round(entry.b, 2), @@ -600,158 +340,266 @@ export async function prepareWeightStatusForUser( : null, }; - const shed = degradeStatusSnapshotToBudget( - snapshot as unknown as Record, - { maxChars: await statusSnapshotCharsForRecord(userId) }, - ); - const snapshotJson = JSON.stringify(snapshot); - - annotate({ - action: { name: cacheAction }, - meta: { - payload_size_bytes: snapshotJson.length, - ...(shed.length > 0 ? { snapshot_shed: shed } : {}), - }, - }); - - // Content-hash gate (v1.16.8): when the snapshot is unchanged since the - // last real assessment, refresh the cache timestamp and skip the LLM. - const snapshotHash = hashInsightSnapshot(snapshot); - const unchanged = await refreshUnchangedStatusInsight({ - userId, - cacheAction, - todayKey, - snapshotHash, - }); - if (unchanged) { - return { - phase: "served", - result: { - hasProvider: true, - text: unchanged.text, - cached: true, - updatedAt: unchanged.updatedAt, - }, - }; - } - - const previousContext = await getPreviousInsightContext( - userId, - "weight-status", - locale, - 12, - ); - const previousContextBlock = formatPreviousContextForPrompt( - previousContext, - locale, - ); - - // v1.12.7 — diversity / anti-repetition block (see blood-pressure-status). - const varietyLead = pickVarietyLead(userId, "weight", todayKey); - const steadyRun = computeSteadyRun( - canonicalWeightGraded.weekly, - canonicalWeightGraded.monthly, - ); - const relations = await getRelevantCorrelationsForMetric( - userId, - "WEIGHT", - locale, - ); - const assessmentContextBlock = buildAssessmentContextBlock( - { - varietyLead, - dataStrength: { + return { + snapshot, + inputHash, + finish: () => ({ + context: { + repeatCount: computeSteadyRun( + canonicalWeightGraded.weekly, + canonicalWeightGraded.monthly, + ), points: weightSeries.daily.length, newestDaysAgo: newestMeasurementDaysAgo, + relationsFor: "WEIGHT", }, - repeatCount: steadyRun, - relations, - }, - locale, - ); - - // The provider call is deferred to the caller — the single-card path runs - // ONE completion, the batch path folds this prompt into one shared call. - // A timeout / error is a transient miss (fallback served, no assessment - // persisted); `none` (no provider / no consent) serves the no-key text. - return { - phase: "pending", - metric: "weight", - userId, - cacheAction, - systemPrompt: getWeightSystemPrompt(locale, units.system), - userPrompt: getWeightUserPrompt( - snapshotJson, - todayKey, - locale, - previousContextBlock, - assessmentContextBlock, - // v1.28.40 — rotating opener hint, per (user, metric, day). - openerArchetypeHint(`${userId}:weight:${todayKey}`, locale as Locale), - ), - snapshotHash, - // v1.12.7 — match the archetype cards' 0.45. - temperature: 0.45, - noProvider: { - hasProvider: false, - text: getNoKeyWeightStatusText(locale, weightSignal), - cached: true, - updatedAt: null, - }, - timeout: (reason): StatusCardResult => - returnTimeoutFallback({ - cacheAction, - reason, - userId, - todayKey, - stubText: getNoKeyWeightStatusText(locale, weightSignal), + fallbackText: getNoKeyWeightStatusText(locale, weightSignal), + prompts: (snapshotJson, context) => ({ + systemPrompt: getWeightSystemPrompt(locale, units.system), + userPrompt: getWeightUserPrompt( + snapshotJson, + todayKey, + locale, + context.previousContextBlock, + context.assessmentContextBlock, + context.openerHint, + ), }), - finalize: async (outcome): Promise => { - // Outbound safety screen. The only transform here used to be - // whitespace-normalisation, so a dose-change imperative or a fabricated - // clinical risk score persisted as the day's cached assessment. Policy - // for a background-generated card is WITHHOLD: serve the deterministic - // stub and persist no model text (see `finalizeStatusSummary`). - const screened = finalizeStatusSummary(outcome.content, locale); - if (!screened.ok) { - annotate({ - action: { name: "insights.status.outbound_blocked" }, - meta: { cacheAction, reason: screened.reason }, - }); - return returnTimeoutFallback({ - cacheAction, - reason: "screened", - userId: userId, - todayKey: todayKey, - stubText: getNoKeyWeightStatusText(locale, weightSignal), - }); - } - const summary = screened.text; - if (!summary) { - throw new Error("Weight-status summary was empty after normalization"); - } - const updatedAt = await persistStatusInsight({ - userId, - cacheAction, - todayKey, - text: summary, - snapshotHash, - // v1.18.11 — persist the input fingerprint so tomorrow's input - // gate can skip the rebuild when nothing salient changed. - inputHash, - }); - return { - hasProvider: true, - text: summary, - cached: false, - updatedAt, - }; - }, + detail: undefined, + }), }; } -export function resolveWeightStatusLocale( - locale: string | null | undefined, -): SupportedLocale { - return normalizeLocale(locale); +/** + * The weight target feature and the finished recent-vs-baseline signal. + */ +function weightTargetAndSignal(args: { + weightDaily: DailyBucket[]; + latestWeight: DailyBucket | null; + weightTarget: WeightInputs["weightTarget"]; + units: UnitPreferences; + mass: DisplayTransform; + weightGraded: GradedSeries; + newestDaysAgo: number | null; + locale: SupportedLocale; +}) { + const { weightTarget, units, mass, weightGraded, locale } = args; + const newestMeasurementDaysAgo = args.newestDaysAgo; + const latestWeight = args.latestWeight; + // v1.18.10 — hand the model the finished recent-vs-baseline + // comparison + normal-swing verdict instead of asking it to derive them. + // Weight has no universal favourable direction or population band, so + // without a stored target the signal carries the neutral "target-band" + // framing and no normalRange. v1.39 (#1006) — with one, the direction is + // the person's own: below the band a rise is favourable, above it a fall. + const recentWeights = args.weightDaily + .filter((bucket) => bucket.dayOffset < 7) + .map((bucket) => bucket.value); + const weightTargetFeature = buildWeightTargetFeature( + weightTarget, + { + avg7: + recentWeights.length > 0 + ? recentWeights.reduce((sum, value) => sum + value, 0) / + recentWeights.length + : null, + latest: latestWeight?.value ?? null, + }, + units.system, + ); + const weightSignal = buildMetricSignal({ + metric: locale === "en" ? "your weight" : "dein Gewicht", + unit: mass.displayUnit, + direction: + weightTargetFeature?.position === "below" + ? "higher-better" + : weightTargetFeature?.position === "above" + ? "lower-better" + : "target-band", + graded: weightGraded, + newestDaysAgo: newestMeasurementDaysAgo, + }); + + return { weightTargetFeature, weightSignal }; } + +/** + * The weight snapshot's reads: weight and the two BP channels as daily + * buckets and graded series, and the mood context. + */ +async function readWeightSeries( + userId: string, + mass: DisplayTransform, + userTz: string, +) { + // v1.4.28 — cap the snapshot input. BP captures three types + // per day so 1095 = 365 d × 3 channels. Order desc + reverse so we + // keep the most recent year of rows. + const measurements = await prisma.measurement + .findMany({ + where: { + userId, + type: { + in: ["WEIGHT", "BLOOD_PRESSURE_SYS", "BLOOD_PRESSURE_DIA"], + }, + // Soft-deleted rows must never reach the prompt snapshot. + deletedAt: null, + }, + take: 1095, + orderBy: { measuredAt: "desc" }, + select: { + type: true, + value: true, + measuredAt: true, + }, + }) + .then((rows) => rows.reverse()); + + const now = new Date(); + + // v1.2.5 — every day-bucketing pass below keys on the user's own + // calendar day. `userTz` was already resolved above for the cache day-key. + const dailyOf = (type: string) => + applyPayloadBudget( + measurements + .filter((measurement) => measurement.type === type) + .map((measurement) => ({ + measuredAt: measurement.measuredAt, + value: measurement.value, + })), + { now, tz: userTz }, + ); + const weightSeries = dailyOf("WEIGHT"); + const sysSeries = dailyOf("BLOOD_PRESSURE_SYS"); + const diaSeries = dailyOf("BLOOD_PRESSURE_DIA"); + + // Fetch mood context (optional — for enrichment only). v1.4.28 + // Cap at 90 entries. + const moodEntries = await readRecentMoodScores(userId, 90); + + const moodSeries = applyPayloadBudget( + moodEntries.map((entry) => ({ + measuredAt: entry.moodLoggedAt, + value: entry.score, + })), + { now, tz: userTz }, + ); + const moodSummary = summarizeSeries( + moodSeries.daily.map((bucket) => ({ value: bucket.value })), + ); + const moodMean = moodSummary?.mean ?? null; + + // Compact graded series for the prompt. The `*Series.daily` buckets + // above stay for the correlation pairing + latest-day focus; only the + // graded shape is embedded so the full daily arrays never ship. + // + // Weight + the two BP context channels source their recent / weekly + // slices from a bounded raw read and their monthly / yearly tail from + // the MONTH / YEAR rollup tier (with a full-history in-memory fallback + // on a cold-tier coverage miss). Mood has no rollup tier, so it stays + // an in-memory fold. + const [canonicalWeightGraded, sysGraded, diaGraded] = await Promise.all([ + buildGradedSeriesWithRollups(userId, "WEIGHT", now, userTz), + buildGradedSeriesWithRollups(userId, "BLOOD_PRESSURE_SYS", now, userTz), + buildGradedSeriesWithRollups(userId, "BLOOD_PRESSURE_DIA", now, userTz), + ]); + const weightGraded = transformRescales(mass) + ? convertGradedSeries(canonicalWeightGraded, mass, mass.decimals) + : canonicalWeightGraded; + const moodGraded = buildGradedSeriesFromPoints( + moodSeries.daily.map((b) => ({ + measuredAt: canonicalDailyTimestamp( + dayOffsetToDayKey(now, b.dayOffset, userTz), + userTz, + ), + value: b.value, + })), + now, + userTz, + ); + + return { + now, + measurements, + weightSeries, + sysSeries, + diaSeries, + moodSeries, + moodMean, + canonicalWeightGraded, + weightGraded, + sysGraded, + diaGraded, + moodGraded, + }; +} + +/** + * Weight against the two BP channels: the paired systolic / diastolic days, + * weight against systolic, and weight against the daily BP mean, each with + * its significance-gated correlation. + */ +function pairWeightWithBloodPressure( + weightDaily: DailyBucket[], + sysDaily: DailyBucket[], + diaDaily: DailyBucket[], + now: Date, + userTz: string, +) { + const weightVsSystolic = pairDailyBuckets(weightDaily, sysDaily, now, userTz); + const weightVsSystolicCorrelation = + significantPearsonCorrelation(weightVsSystolic); + + const systolicDiastolic = pairDailyBuckets( + sysDaily, + diaDaily, + now, + userTz, + ).map((entry) => ({ + day: entry.dayKey, + sys: entry.a, + dia: entry.b, + mean: round((entry.a + entry.b) / 2, 2), + })); + + // Synthesise a daily-bucket-shaped BP-mean series so we can reuse + // pairDailyBuckets — derive dayOffset from the offsets in sysSeries. + const sysOffsetByDay = new Map( + sysDaily.map((bucket) => { + const dayKey = dayOffsetToDayKey(now, bucket.dayOffset, userTz); + return [dayKey, bucket.dayOffset]; + }), + ); + const bpMeanDaily: DailyBucket[] = systolicDiastolic + .map((entry) => { + const dayOffset = sysOffsetByDay.get(entry.day); + if (dayOffset == null) return null; + return { dayOffset, value: entry.mean, n: 1 }; + }) + .filter((entry): entry is DailyBucket => entry !== null); + + const weightVsMeanBp = pairDailyBuckets( + weightDaily, + bpMeanDaily, + now, + userTz, + ); + const weightVsMeanBpCorrelation = + significantPearsonCorrelation(weightVsMeanBp); + + return { + weightVsSystolic, + weightVsSystolicCorrelation, + systolicDiastolic, + weightVsMeanBp, + weightVsMeanBpCorrelation, + }; +} + +const WEIGHT_STATUS_CARD: StatusCardPlugin = { + scope: "weight", + codec: textStatusCodec, + unavailableText: (locale) => getNoKeyWeightStatusText(locale), + emptyMessage: "Weight-status summary was empty after normalization", + build: buildWeightSnapshot, +}; diff --git a/tests/integration/__golden__/status-parity/batch.json b/tests/integration/__golden__/status-parity/batch.json new file mode 100644 index 000000000..538a0ae4c --- /dev/null +++ b/tests/integration/__golden__/status-parity/batch.json @@ -0,0 +1,1930 @@ +{ + "batch-en": { + "calls": [ + { + "system": [ + "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", + "", + "YOUR DATA (graded snapshot):", + "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", + "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", + "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", + "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", + "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", + "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", + "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", + "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", + "", + "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", + "Moves available (choose the few that genuinely apply):", + "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", + "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", + "(c) name a genuine, earned win or a steady streak when the data shows one;", + "(d) one watch-item worth keeping an eye on;", + "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", + "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", + "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", + "", + "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", + "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", + "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", + "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", + "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", + "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", + "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", + "", + "OPENING — lead with meaning, not the metric value", + "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", + "", + "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", + "", + "JUDGE THE DATA HONESTLY (never invent a trend):", + "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", + "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", + "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", + "", + "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", + "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", + "", + "METRIC IDENTIFIERS STAY OUT OF PROSE", + "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", + "", + "GLP-1 DOSE SAFETY (contract, not style)", + "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", + "", + "ACUTE RED FLAGS (contract, not style)", + "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", + "", + "GROUNDING — ZERO HALLUCINATIONS", + "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", + "", + "TONE — a warm, motivating advisor, never clinical or alarming", + "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", + "", + "NOT AN INSIGHT — never recite mechanics", + "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", + "", + "INTERPRET, DON'T ENUMERATE", + "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", + "", + "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", + "", + "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", + "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", + "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", + "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", + "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", + "", + "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in English: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", + "{ \"summary\": \"...\" }", + "", + "BATCHED OUTPUT — you are assessing SEVERAL of this user's metrics in one pass. Each metric below carries its OWN graded snapshot under a \"### \" heading; assess each one STRICTLY from its own snapshot block, applying every rule above per metric. Do NOT let one metric's finding leak into another. Do NOT invent a metric: assess ONLY the keys present below.", + "", + "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. \"perMetric\" carries one short assessment (2-4 sentences each, the same contract as a single card) per present key:", + "{ \"perMetric\": { \"bp\": \"...\", \"pulse\": \"...\", \"weight\": \"...\", \"bmi\": \"...\", \"mood\": \"...\", \"compliance\": \"...\", \"general\": \"...\" } }", + "Include a key ONLY if its snapshot block is present below (keys present: bp, pulse, weight, bmi, mood, compliance, general). Omit any metric whose block is absent — never fabricate one." + ], + "messages": [ + { + "role": "user", + "content": [ + "### bp", + "", + "Date: 2026-06-15", + "OPENER HINT: Open with the overall read in plain words, then bring in the number as support — not number-first.", + "Write one short assessment of this person's blood pressure. Open with what the reading MEANS in plain words — the overall read, not the number (e.g. \"calm and right where you want it\", \"running a touch higher than your usual this week\") — then bring in the systolic/diastolic figures right after as support, judged as ONE pair and placed against their own weekly/monthly baseline; never lead with the numbers. Close with one doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the measurement count and recency.", + "", + "PREVIOUS ANALYSIS: none on file. Treat this as the user's first analysis for this domain — no improvement/regression delta to surface.", + "", + "", + "VARIETY: to keep your cards from reading the same, you may open this one by leading with the single most recent value and where it sits — only if the data genuinely supports that angle; never force it, never invent a trend.", + "", + "DATA STRENGTH: 94 day-buckets in the window, latest reading today.", + "", + "REPETITION: you have already given a similar \"no material change\" assessment for this metric 11 time(s) in a row. Do NOT restate the same level again. Acknowledge the continuity in one short clause (\"still holding steady, 12 checks running\") and then pivot to a DIFFERENT facet — consistency, time-of-day pattern, or a correlated metric — or, if nothing is genuinely actionable, say so plainly and skip the manufactured step.", + "", + "RELATIONS (statistically-screened associations in YOUR data involving this metric — descriptive, NEVER causal):", + "- Higher blood glucose looks like it goes with higher next-day systolic blood pressure in your data, on the evidence so far. It is a pattern to watch, not a cause. (n=23, r=0.91)", + "- Higher systolic blood pressure looks like it goes with higher next-day weight in your data, on the evidence so far. It is a pattern to watch, not a cause. (n=47, r=0.88)", + "You MAY weave at most ONE of these in if it adds genuine insight, phrased as an \"association\" / \"tends to go with\" — never as a cause, and never alter the numbers. Skip them if the metric's own finding is the stronger story.", + "", + "", + "{\"locale\":\"en\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"blood_pressure\",\"dataCoverage\":{\"totalMeasurements\":258,\"totalSpanDays\":139,\"newestMeasurementDaysAgo\":0},\"bloodPressure\":{\"systolic\":{\"signal\":{\"metric\":\"your systolic\",\"unit\":\"mmHg\",\"current\":127.29,\"currentWindowDays\":14,\"baseline\":126.97,\"baselineLabel\":\"your monthly average\",\"delta\":0.32,\"deltaPct\":0.3,\"spread\":2.38,\"outsideNormalSwing\":false,\"direction\":\"lower-better\",\"n\":14,\"newestDaysAgo\":0,\"normalRange\":{\"low\":120,\"high\":129},\"placement\":\"in band\"},\"summary\":{\"points\":94,\"start\":128,\"end\":125,\"delta\":-3,\"mean\":125.39,\"min\":116,\"max\":134},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":119,\"max\":119,\"mean\":119,\"n\":1},{\"date\":\"2026-05-28\",\"min\":121,\"max\":121,\"mean\":121,\"n\":1},{\"date\":\"2026-05-30\",\"min\":126,\"max\":126,\"mean\":126,\"n\":1},{\"date\":\"2026-05-31\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-02\",\"min\":125,\"max\":125,\"mean\":125,\"n\":1},{\"date\":\"2026-06-03\",\"min\":127,\"max\":127,\"mean\":127,\"n\":1},{\"date\":\"2026-06-05\",\"min\":130,\"max\":130,\"mean\":130,\"n\":1},{\"date\":\"2026-06-06\",\"min\":132,\"max\":132,\"mean\":132,\"n\":1},{\"date\":\"2026-06-08\",\"min\":134,\"max\":134,\"mean\":134,\"n\":1},{\"date\":\"2026-06-09\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-11\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-12\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-14\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-15\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":117,\"max\":121,\"mean\":118.75,\"n\":4},{\"weekISO\":\"2026-W13\",\"min\":119,\"max\":128,\"mean\":123.2,\"n\":5},{\"weekISO\":\"2026-W14\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"weekISO\":\"2026-W15\",\"min\":128,\"max\":134,\"mean\":129.4,\"n\":5},{\"weekISO\":\"2026-W16\",\"min\":121,\"max\":128,\"mean\":122.4,\"n\":5},{\"weekISO\":\"2026-W17\",\"min\":116,\"max\":122,\"mean\":118.75,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":121,\"max\":131,\"mean\":126,\"n\":5},{\"weekISO\":\"2026-W19\",\"min\":129,\"max\":133,\"mean\":131,\"n\":5},{\"weekISO\":\"2026-W20\",\"min\":124,\"max\":125,\"mean\":124.75,\"n\":4},{\"weekISO\":\"2026-W21\",\"min\":117,\"max\":124,\"mean\":119.8,\"n\":5},{\"weekISO\":\"2026-W22\",\"min\":123,\"max\":123,\"mean\":123,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"month\":\"2026-02\",\"min\":116,\"max\":134,\"mean\":124.22,\"n\":18},{\"month\":\"2026-03\",\"min\":124,\"max\":133,\"mean\":128.18,\"n\":11}],\"yearly\":[]}},\"diastolic\":{\"signal\":{\"metric\":\"your diastolic\",\"unit\":\"mmHg\",\"current\":81.71,\"currentWindowDays\":14,\"baseline\":81.43,\"baselineLabel\":\"your monthly average\",\"delta\":0.29,\"deltaPct\":0.4,\"spread\":1.93,\"outsideNormalSwing\":false,\"direction\":\"lower-better\",\"n\":14,\"newestDaysAgo\":0,\"normalRange\":{\"low\":70,\"high\":79},\"placement\":\"above band\"},\"summary\":{\"points\":94,\"start\":82,\"end\":80,\"delta\":-2,\"mean\":80.31,\"min\":74,\"max\":86},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":76,\"max\":76,\"mean\":76,\"n\":1},{\"date\":\"2026-05-28\",\"min\":77,\"max\":77,\"mean\":77,\"n\":1},{\"date\":\"2026-05-30\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-05-31\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-02\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-03\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-05\",\"min\":86,\"max\":86,\"mean\":86,\"n\":1},{\"date\":\"2026-06-06\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-08\",\"min\":84,\"max\":84,\"mean\":84,\"n\":1},{\"date\":\"2026-06-09\",\"min\":85,\"max\":85,\"mean\":85,\"n\":1},{\"date\":\"2026-06-11\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-12\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-14\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-15\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":75,\"max\":79,\"mean\":77,\"n\":4},{\"weekISO\":\"2026-W13\",\"min\":74,\"max\":79,\"mean\":77.4,\"n\":5},{\"weekISO\":\"2026-W14\",\"min\":83,\"max\":85,\"mean\":83.75,\"n\":4},{\"weekISO\":\"2026-W15\",\"min\":79,\"max\":86,\"mean\":82.6,\"n\":5},{\"weekISO\":\"2026-W16\",\"min\":76,\"max\":80,\"mean\":78.2,\"n\":5},{\"weekISO\":\"2026-W17\",\"min\":74,\"max\":78,\"mean\":76.75,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":77,\"max\":84,\"mean\":80.6,\"n\":5},{\"weekISO\":\"2026-W19\",\"min\":82,\"max\":86,\"mean\":83.8,\"n\":5},{\"weekISO\":\"2026-W20\",\"min\":80,\"max\":81,\"mean\":80.5,\"n\":4},{\"weekISO\":\"2026-W21\",\"min\":74,\"max\":78,\"mean\":76.2,\"n\":5},{\"weekISO\":\"2026-W22\",\"min\":78,\"max\":78,\"mean\":78,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":80,\"max\":86,\"mean\":83,\"n\":4},{\"month\":\"2026-02\",\"min\":75,\"max\":85,\"mean\":79.28,\"n\":18},{\"month\":\"2026-03\",\"min\":78,\"max\":86,\"mean\":82,\"n\":11}],\"yearly\":[]}},\"paired\":{\"summary\":{\"points\":94,\"start\":105,\"end\":102.5,\"delta\":-2.5,\"mean\":102.85,\"min\":95.5,\"max\":110},\"series\":[{\"day\":\"2026-03-11\",\"sys\":125,\"dia\":82,\"inTarget\":false},{\"day\":\"2026-03-10\",\"sys\":125,\"dia\":82,\"inTarget\":false},{\"day\":\"2026-03-08\",\"sys\":132,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-03-07\",\"sys\":131,\"dia\":86,\"inTarget\":false},{\"day\":\"2026-03-05\",\"sys\":130,\"dia\":84,\"inTarget\":false},{\"day\":\"2026-03-04\",\"sys\":129,\"dia\":83,\"inTarget\":false},{\"day\":\"2026-03-02\",\"sys\":133,\"dia\":85,\"inTarget\":false},{\"day\":\"2026-03-01\",\"sys\":131,\"dia\":84,\"inTarget\":false},{\"day\":\"2026-02-27\",\"sys\":127,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-02-26\",\"sys\":125,\"dia\":79,\"inTarget\":true},{\"day\":\"2026-02-24\",\"sys\":121,\"dia\":80,\"inTarget\":false},{\"day\":\"2026-02-23\",\"sys\":126,\"dia\":78,\"inTarget\":true},{\"day\":\"2026-02-21\",\"sys\":122,\"dia\":75,\"inTarget\":true},{\"day\":\"2026-02-20\",\"sys\":120,\"dia\":79,\"inTarget\":true},{\"day\":\"2026-02-18\",\"sys\":117,\"dia\":76,\"inTarget\":true},{\"day\":\"2026-02-17\",\"sys\":116,\"dia\":75,\"inTarget\":true},{\"day\":\"2026-02-15\",\"sys\":122,\"dia\":78,\"inTarget\":true},{\"day\":\"2026-02-14\",\"sys\":121,\"dia\":78,\"inTarget\":true},{\"day\":\"2026-02-12\",\"sys\":121,\"dia\":77,\"inTarget\":true},{\"day\":\"2026-02-11\",\"sys\":121,\"dia\":77,\"inTarget\":true},{\"day\":\"2026-02-09\",\"sys\":128,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-02-08\",\"sys\":129,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-02-06\",\"sys\":129,\"dia\":80,\"inTarget\":false},{\"day\":\"2026-02-05\",\"sys\":129,\"dia\":85,\"inTarget\":false},{\"day\":\"2026-02-03\",\"sys\":128,\"dia\":84,\"inTarget\":false},{\"day\":\"2026-02-02\",\"sys\":134,\"dia\":83,\"inTarget\":false},{\"day\":\"2026-01-31\",\"sys\":132,\"dia\":86,\"inTarget\":false},{\"day\":\"2026-01-30\",\"sys\":130,\"dia\":85,\"inTarget\":false},{\"day\":\"2026-01-28\",\"sys\":127,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-01-27\",\"sys\":125,\"dia\":80,\"inTarget\":false}]},\"targets\":{\"systolic\":{\"min\":120,\"max\":129},\"diastolic\":{\"min\":70,\"max\":79},\"inTargetPctLast30DailyPoints\":42.6}},\"weightVsSystolic\":{\"correlation\":{\"r\":0.385,\"strength\":\"schwach\",\"n\":47},\"pairs\":[{\"day\":\"2026-04-24\",\"weight\":83.09,\"systolic\":120},{\"day\":\"2026-04-22\",\"weight\":83,\"systolic\":117},{\"day\":\"2026-04-18\",\"weight\":83,\"systolic\":121},{\"day\":\"2026-04-16\",\"weight\":83.09,\"systolic\":121},{\"day\":\"2026-04-12\",\"weight\":83.33,\"systolic\":128},{\"day\":\"2026-04-10\",\"weight\":83.45,\"systolic\":129},{\"day\":\"2026-04-06\",\"weight\":83.55,\"systolic\":134},{\"day\":\"2026-04-04\",\"weight\":83.51,\"systolic\":132},{\"day\":\"2026-03-31\",\"weight\":83.25,\"systolic\":125},{\"day\":\"2026-03-29\",\"weight\":83.07,\"systolic\":128},{\"day\":\"2026-03-25\",\"weight\":82.71,\"systolic\":119},{\"day\":\"2026-03-23\",\"weight\":82.58,\"systolic\":123},{\"day\":\"2026-03-19\",\"weight\":82.5,\"systolic\":118},{\"day\":\"2026-03-17\",\"weight\":82.55,\"systolic\":117},{\"day\":\"2026-03-13\",\"weight\":82.77,\"systolic\":125},{\"day\":\"2026-03-11\",\"weight\":82.9,\"systolic\":125},{\"day\":\"2026-03-07\",\"weight\":83.07,\"systolic\":131},{\"day\":\"2026-03-05\",\"weight\":83.07,\"systolic\":130},{\"day\":\"2026-03-01\",\"weight\":82.89,\"systolic\":131},{\"day\":\"2026-02-27\",\"weight\":82.73,\"systolic\":127},{\"day\":\"2026-02-23\",\"weight\":82.36,\"systolic\":126},{\"day\":\"2026-02-21\",\"weight\":82.2,\"systolic\":122},{\"day\":\"2026-02-17\",\"weight\":82.03,\"systolic\":116},{\"day\":\"2026-02-15\",\"weight\":82.04,\"systolic\":122},{\"day\":\"2026-02-11\",\"weight\":82.21,\"systolic\":121},{\"day\":\"2026-02-09\",\"weight\":82.34,\"systolic\":128},{\"day\":\"2026-02-05\",\"weight\":82.56,\"systolic\":129},{\"day\":\"2026-02-03\",\"weight\":82.6,\"systolic\":128},{\"day\":\"2026-01-30\",\"weight\":82.51,\"systolic\":130},{\"day\":\"2026-01-28\",\"weight\":82.38,\"systolic\":127}]},\"bpMedicationContinuityVsSystolic\":{\"expectedIntakesPerDay\":1,\"medicationCount\":1,\"correlation\":null,\"series\":[{\"day\":\"2026-03-11\",\"dayOffset\":96,\"sys\":125,\"continuityPct\":100},{\"day\":\"2026-03-10\",\"dayOffset\":97,\"sys\":125,\"continuityPct\":100},{\"day\":\"2026-03-08\",\"dayOffset\":99,\"sys\":132,\"continuityPct\":100},{\"day\":\"2026-03-07\",\"dayOffset\":100,\"sys\":131,\"continuityPct\":0},{\"day\":\"2026-03-05\",\"dayOffset\":102,\"sys\":130,\"continuityPct\":0},{\"day\":\"2026-03-04\",\"dayOffset\":103,\"sys\":129,\"continuityPct\":0},{\"day\":\"2026-03-02\",\"dayOffset\":105,\"sys\":133,\"continuityPct\":0},{\"day\":\"2026-03-01\",\"dayOffset\":106,\"sys\":131,\"continuityPct\":0},{\"day\":\"2026-02-27\",\"dayOffset\":108,\"sys\":127,\"continuityPct\":0},{\"day\":\"2026-02-26\",\"dayOffset\":109,\"sys\":125,\"continuityPct\":0},{\"day\":\"2026-02-24\",\"dayOffset\":111,\"sys\":121,\"continuityPct\":0},{\"day\":\"2026-02-23\",\"dayOffset\":112,\"sys\":126,\"continuityPct\":0},{\"day\":\"2026-02-21\",\"dayOffset\":114,\"sys\":122,\"continuityPct\":0},{\"day\":\"2026-02-20\",\"dayOffset\":115,\"sys\":120,\"continuityPct\":0},{\"day\":\"2026-02-18\",\"dayOffset\":117,\"sys\":117,\"continuityPct\":0},{\"day\":\"2026-02-17\",\"dayOffset\":118,\"sys\":116,\"continuityPct\":0},{\"day\":\"2026-02-15\",\"dayOffset\":120,\"sys\":122,\"continuityPct\":0},{\"day\":\"2026-02-14\",\"dayOffset\":121,\"sys\":121,\"continuityPct\":0},{\"day\":\"2026-02-12\",\"dayOffset\":123,\"sys\":121,\"continuityPct\":0},{\"day\":\"2026-02-11\",\"dayOffset\":124,\"sys\":121,\"continuityPct\":0},{\"day\":\"2026-02-09\",\"dayOffset\":126,\"sys\":128,\"continuityPct\":0},{\"day\":\"2026-02-08\",\"dayOffset\":127,\"sys\":129,\"continuityPct\":0},{\"day\":\"2026-02-06\",\"dayOffset\":129,\"sys\":129,\"continuityPct\":0},{\"day\":\"2026-02-05\",\"dayOffset\":130,\"sys\":129,\"continuityPct\":0},{\"day\":\"2026-02-03\",\"dayOffset\":132,\"sys\":128,\"continuityPct\":0},{\"day\":\"2026-02-02\",\"dayOffset\":133,\"sys\":134,\"continuityPct\":0},{\"day\":\"2026-01-31\",\"dayOffset\":135,\"sys\":132,\"continuityPct\":0},{\"day\":\"2026-01-30\",\"dayOffset\":136,\"sys\":130,\"continuityPct\":0},{\"day\":\"2026-01-28\",\"dayOffset\":138,\"sys\":127,\"continuityPct\":0},{\"day\":\"2026-01-27\",\"dayOffset\":139,\"sys\":125,\"continuityPct\":0}]},\"bpMedications\":[{\"name\":\"Ramipril\",\"dose\":\"5mg\",\"schedulesPerDay\":1,\"compliance7\":67,\"compliance30\":79}],\"moodContext\":{\"points\":70,\"mean\":3.03,\"latest\":1,\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-15\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W15\",\"min\":2,\"max\":5,\"mean\":3.33,\"n\":6},{\"weekISO\":\"2026-W16\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":1,\"max\":5,\"mean\":3,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":1,\"max\":4,\"mean\":2.57,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1}],\"monthly\":[],\"yearly\":[]},\"moodVsSystolicCorrelation\":null}}", + "", + "---", + "", + "### pulse", + "", + "Date: 2026-06-15", + "OPENER HINT: Open by acknowledging steadiness or a streak, then the current value.", + "Write one short assessment of this person's resting pulse. Open with what it MEANS in plain words — the read, not the number (e.g. \"running a little calmer than usual\", \"steady, right where it's been\") — then bring in ONE concrete number from the snapshot right after as support, placed against their own weekly/monthly baseline; never lead with the value. Close with one doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the measurement count and recency.", + "", + "PREVIOUS ANALYSIS: none on file. Treat this as the user's first analysis for this domain — no improvement/regression delta to surface.", + "", + "", + "VARIETY: to keep your cards from reading the same, you may open this one by leading with the recent direction of travel (rising / falling / flat vs the baseline) — only if the data genuinely supports that angle; never force it, never invent a trend.", + "", + "DATA STRENGTH: 122 day-buckets in the window, latest reading today.", + "", + "", + "{\"locale\":\"en\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"pulse\",\"dataCoverage\":{\"totalMeasurements\":365,\"totalSpanDays\":121,\"newestMeasurementDaysAgo\":0},\"pulse\":{\"signal\":{\"metric\":\"your resting pulse\",\"unit\":\"bpm\",\"current\":71.64,\"currentWindowDays\":22,\"baseline\":71.88,\"baselineLabel\":\"your monthly average\",\"delta\":-0.24,\"deltaPct\":-0.3,\"spread\":2.21,\"outsideNormalSwing\":false,\"direction\":\"lower-better\",\"n\":63,\"newestDaysAgo\":0,\"normalRange\":{\"low\":61,\"high\":77},\"placement\":\"in band\"},\"summary\":{\"points\":122,\"start\":70,\"end\":68,\"delta\":-2,\"mean\":70.13,\"min\":65,\"max\":75},\"series\":{\"recent\":[{\"date\":\"2026-05-25\",\"min\":62,\"max\":74,\"mean\":68,\"n\":2},{\"date\":\"2026-05-26\",\"min\":62,\"max\":74,\"mean\":66,\"n\":3},{\"date\":\"2026-05-27\",\"min\":63,\"max\":75,\"mean\":67,\"n\":3},{\"date\":\"2026-05-28\",\"min\":64,\"max\":76,\"mean\":68,\"n\":3},{\"date\":\"2026-05-29\",\"min\":65,\"max\":77,\"mean\":69,\"n\":3},{\"date\":\"2026-05-30\",\"min\":66,\"max\":78,\"mean\":70,\"n\":3},{\"date\":\"2026-05-31\",\"min\":67,\"max\":79,\"mean\":71,\"n\":3},{\"date\":\"2026-06-01\",\"min\":68,\"max\":80,\"mean\":72,\"n\":3},{\"date\":\"2026-06-02\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-03\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-04\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-05\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-06\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-07\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-08\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-09\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-10\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-11\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-12\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-13\",\"min\":68,\"max\":80,\"mean\":72,\"n\":3},{\"date\":\"2026-06-14\",\"min\":67,\"max\":79,\"mean\":71,\"n\":3},{\"date\":\"2026-06-15\",\"min\":66,\"max\":66,\"mean\":66,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":61,\"max\":75,\"mean\":65.86,\"n\":20},{\"weekISO\":\"2026-W13\",\"min\":62,\"max\":79,\"mean\":68.14,\"n\":21},{\"weekISO\":\"2026-W14\",\"min\":68,\"max\":83,\"mean\":73.57,\"n\":21},{\"weekISO\":\"2026-W15\",\"min\":67,\"max\":83,\"mean\":73.43,\"n\":21},{\"weekISO\":\"2026-W16\",\"min\":61,\"max\":78,\"mean\":67.43,\"n\":21},{\"weekISO\":\"2026-W17\",\"min\":61,\"max\":75,\"mean\":65.71,\"n\":21},{\"weekISO\":\"2026-W18\",\"min\":64,\"max\":82,\"mean\":71,\"n\":21},{\"weekISO\":\"2026-W19\",\"min\":70,\"max\":83,\"mean\":74.57,\"n\":21},{\"weekISO\":\"2026-W20\",\"min\":64,\"max\":81,\"mean\":70.71,\"n\":21},{\"weekISO\":\"2026-W21\",\"min\":61,\"max\":75,\"mean\":65.57,\"n\":21},{\"weekISO\":\"2026-W22\",\"min\":62,\"max\":62,\"mean\":62,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":68,\"max\":83,\"mean\":73.6,\"n\":15},{\"month\":\"2026-02\",\"min\":61,\"max\":83,\"mean\":69.39,\"n\":84},{\"month\":\"2026-03\",\"min\":63,\"max\":83,\"mean\":72.65,\"n\":46}],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":0,\"value\":70,\"deltaToPreviousDailyPoint\":-1},\"target\":{\"greenMin\":61,\"greenMax\":77,\"orangeMin\":55,\"orangeMax\":85,\"inTargetPctLast30DailyPoints\":40}},\"moodContext\":{\"points\":70,\"mean\":3.03,\"latest\":1,\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W15\",\"min\":2,\"max\":5,\"mean\":3.33,\"n\":6},{\"weekISO\":\"2026-W16\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":1,\"max\":5,\"mean\":3,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":1,\"max\":4,\"mean\":2.57,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1}],\"monthly\":[],\"yearly\":[]}}}", + "", + "---", + "", + "### weight", + "", + "Date: 2026-06-15", + "OPENER HINT: Open by acknowledging steadiness or a streak, then the current value.", + "Write one short assessment of this person's weight. Open with what the trend MEANS in plain words — the direction and momentum, not the number (e.g. \"easing down steadily\", \"holding right where it's settled\") — then bring in ONE concrete number from the snapshot right after as support, read as a continuous trend and pace against their own weekly/monthly baseline (pace per week, plateau, milestone — not the single value, and not the WHO band, which the BMI card covers); never lead with the value. Close with one doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the measurement count and recency.", + "", + "PREVIOUS ANALYSIS: none on file. Treat this as the user's first analysis for this domain — no improvement/regression delta to surface.", + "", + "", + "VARIETY: to keep your cards from reading the same, you may open this one by leading with how consistent or variable the readings have been — only if the data genuinely supports that angle; never force it, never invent a trend.", + "", + "DATA STRENGTH: 70 day-buckets in the window, latest reading today.", + "", + "REPETITION: you have already given a similar \"no material change\" assessment for this metric 10 time(s) in a row. Do NOT restate the same level again. Acknowledge the continuity in one short clause (\"still holding steady, 11 checks running\") and then pivot to a DIFFERENT facet — consistency, time-of-day pattern, or a correlated metric — or, if nothing is genuinely actionable, say so plainly and skip the manufactured step.", + "", + "RELATIONS (statistically-screened associations in YOUR data involving this metric — descriptive, NEVER causal):", + "- Higher systolic blood pressure looks like it goes with higher next-day weight in your data, on the evidence so far. It is a pattern to watch, not a cause. (n=47, r=0.88)", + "- Higher sleep duration looks like it goes with higher next-day weight in your data, on the evidence so far. It is a pattern to watch, not a cause. (n=45, r=0.98)", + "You MAY weave at most ONE of these in if it adds genuine insight, phrased as an \"association\" / \"tends to go with\" — never as a cause, and never alter the numbers. Skip them if the metric's own finding is the stronger story.", + "", + "", + "{\"locale\":\"en\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"weight\",\"dataCoverage\":{\"totalMeasurements\":258,\"totalSpanDays\":139,\"newestMeasurementDaysAgo\":0},\"weight\":{\"unit\":\"lb\",\"signal\":{\"metric\":\"your weight\",\"unit\":\"lb\",\"current\":185.58,\"currentWindowDays\":11,\"baseline\":182.07,\"baselineLabel\":\"your monthly average\",\"delta\":3.52,\"deltaPct\":1.9,\"spread\":0.64,\"outsideNormalSwing\":true,\"direction\":\"target-band\",\"n\":11,\"newestDaysAgo\":0},\"summary\":{\"points\":70,\"start\":185.6,\"end\":181.6,\"delta\":-4,\"mean\":183.4,\"min\":180.8,\"max\":186.2},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":184.3,\"max\":184.3,\"mean\":184.3,\"n\":1},{\"date\":\"2026-05-28\",\"min\":184.6,\"max\":184.6,\"mean\":184.6,\"n\":1},{\"date\":\"2026-05-30\",\"min\":185.1,\"max\":185.1,\"mean\":185.1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":185.5,\"max\":185.5,\"mean\":185.5,\"n\":1},{\"date\":\"2026-06-03\",\"min\":185.8,\"max\":185.8,\"mean\":185.8,\"n\":1},{\"date\":\"2026-06-05\",\"min\":186.1,\"max\":186.1,\"mean\":186.1,\"n\":1},{\"date\":\"2026-06-07\",\"min\":186.2,\"max\":186.2,\"mean\":186.2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":186.2,\"max\":186.2,\"mean\":186.2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":186.1,\"max\":186.1,\"mean\":186.1,\"n\":1},{\"date\":\"2026-06-13\",\"min\":185.9,\"max\":185.9,\"mean\":185.9,\"n\":1},{\"date\":\"2026-06-15\",\"min\":185.6,\"max\":185.6,\"mean\":185.6,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":181.9,\"max\":182,\"mean\":181.9,\"n\":3},{\"weekISO\":\"2026-W13\",\"min\":182.1,\"max\":183.1,\"mean\":182.6,\"n\":4},{\"weekISO\":\"2026-W14\",\"min\":183.5,\"max\":184.1,\"mean\":183.8,\"n\":3},{\"weekISO\":\"2026-W15\",\"min\":183.7,\"max\":184.2,\"mean\":184,\"n\":4},{\"weekISO\":\"2026-W16\",\"min\":183,\"max\":183.4,\"mean\":183.2,\"n\":3},{\"weekISO\":\"2026-W17\",\"min\":182.9,\"max\":183.5,\"mean\":183.1,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":183.9,\"max\":184.7,\"mean\":184.3,\"n\":3},{\"weekISO\":\"2026-W19\",\"min\":185,\"max\":185.2,\"mean\":185.1,\"n\":4},{\"weekISO\":\"2026-W20\",\"min\":184.4,\"max\":184.9,\"mean\":184.7,\"n\":3},{\"weekISO\":\"2026-W21\",\"min\":184,\"max\":184.2,\"mean\":184,\"n\":4}],\"monthly\":[{\"month\":\"2026-01\",\"min\":181.6,\"max\":181.9,\"mean\":181.8,\"n\":2},{\"month\":\"2026-02\",\"min\":180.8,\"max\":182.4,\"mean\":181.6,\"n\":14},{\"month\":\"2026-03\",\"min\":182.2,\"max\":183.1,\"mean\":182.8,\"n\":8}],\"yearly\":[]},\"latestDayFocus\":{\"day\":\"2026-06-15\",\"dayOffset\":0,\"value\":185.6,\"deltaToPreviousDailyPoint\":-0.3,\"sameDayBloodPressure\":{\"day\":\"2026-06-15\",\"sys\":128,\"dia\":82,\"mean\":105}}},\"bloodPressureContext\":{\"systolic\":{\"summary\":{\"points\":94,\"start\":128,\"end\":125,\"delta\":-3,\"mean\":125.39,\"min\":116,\"max\":134},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":119,\"max\":119,\"mean\":119,\"n\":1},{\"date\":\"2026-05-28\",\"min\":121,\"max\":121,\"mean\":121,\"n\":1},{\"date\":\"2026-05-30\",\"min\":126,\"max\":126,\"mean\":126,\"n\":1},{\"date\":\"2026-05-31\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-02\",\"min\":125,\"max\":125,\"mean\":125,\"n\":1},{\"date\":\"2026-06-03\",\"min\":127,\"max\":127,\"mean\":127,\"n\":1},{\"date\":\"2026-06-05\",\"min\":130,\"max\":130,\"mean\":130,\"n\":1},{\"date\":\"2026-06-06\",\"min\":132,\"max\":132,\"mean\":132,\"n\":1},{\"date\":\"2026-06-08\",\"min\":134,\"max\":134,\"mean\":134,\"n\":1},{\"date\":\"2026-06-09\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-11\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-12\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-14\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-15\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":117,\"max\":121,\"mean\":118.75,\"n\":4},{\"weekISO\":\"2026-W13\",\"min\":119,\"max\":128,\"mean\":123.2,\"n\":5},{\"weekISO\":\"2026-W14\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"weekISO\":\"2026-W15\",\"min\":128,\"max\":134,\"mean\":129.4,\"n\":5},{\"weekISO\":\"2026-W16\",\"min\":121,\"max\":128,\"mean\":122.4,\"n\":5},{\"weekISO\":\"2026-W17\",\"min\":116,\"max\":122,\"mean\":118.75,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":121,\"max\":131,\"mean\":126,\"n\":5},{\"weekISO\":\"2026-W19\",\"min\":129,\"max\":133,\"mean\":131,\"n\":5},{\"weekISO\":\"2026-W20\",\"min\":124,\"max\":125,\"mean\":124.75,\"n\":4},{\"weekISO\":\"2026-W21\",\"min\":117,\"max\":124,\"mean\":119.8,\"n\":5},{\"weekISO\":\"2026-W22\",\"min\":123,\"max\":123,\"mean\":123,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"month\":\"2026-02\",\"min\":116,\"max\":134,\"mean\":124.22,\"n\":18},{\"month\":\"2026-03\",\"min\":124,\"max\":133,\"mean\":128.18,\"n\":11}],\"yearly\":[]}},\"diastolic\":{\"summary\":{\"points\":94,\"start\":82,\"end\":80,\"delta\":-2,\"mean\":80.31,\"min\":74,\"max\":86},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":76,\"max\":76,\"mean\":76,\"n\":1},{\"date\":\"2026-05-28\",\"min\":77,\"max\":77,\"mean\":77,\"n\":1},{\"date\":\"2026-05-30\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-05-31\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-02\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-03\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-05\",\"min\":86,\"max\":86,\"mean\":86,\"n\":1},{\"date\":\"2026-06-06\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-08\",\"min\":84,\"max\":84,\"mean\":84,\"n\":1},{\"date\":\"2026-06-09\",\"min\":85,\"max\":85,\"mean\":85,\"n\":1},{\"date\":\"2026-06-11\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-12\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-14\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-15\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":75,\"max\":79,\"mean\":77,\"n\":4},{\"weekISO\":\"2026-W13\",\"min\":74,\"max\":79,\"mean\":77.4,\"n\":5},{\"weekISO\":\"2026-W14\",\"min\":83,\"max\":85,\"mean\":83.75,\"n\":4},{\"weekISO\":\"2026-W15\",\"min\":79,\"max\":86,\"mean\":82.6,\"n\":5},{\"weekISO\":\"2026-W16\",\"min\":76,\"max\":80,\"mean\":78.2,\"n\":5},{\"weekISO\":\"2026-W17\",\"min\":74,\"max\":78,\"mean\":76.75,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":77,\"max\":84,\"mean\":80.6,\"n\":5},{\"weekISO\":\"2026-W19\",\"min\":82,\"max\":86,\"mean\":83.8,\"n\":5},{\"weekISO\":\"2026-W20\",\"min\":80,\"max\":81,\"mean\":80.5,\"n\":4},{\"weekISO\":\"2026-W21\",\"min\":74,\"max\":78,\"mean\":76.2,\"n\":5},{\"weekISO\":\"2026-W22\",\"min\":78,\"max\":78,\"mean\":78,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":80,\"max\":86,\"mean\":83,\"n\":4},{\"month\":\"2026-02\",\"min\":75,\"max\":85,\"mean\":79.28,\"n\":18},{\"month\":\"2026-03\",\"min\":78,\"max\":86,\"mean\":82,\"n\":11}],\"yearly\":[]}},\"pairedDaily\":[{\"day\":\"2026-03-11\",\"sys\":125,\"dia\":82,\"mean\":103.5},{\"day\":\"2026-03-10\",\"sys\":125,\"dia\":82,\"mean\":103.5},{\"day\":\"2026-03-08\",\"sys\":132,\"dia\":81,\"mean\":106.5},{\"day\":\"2026-03-07\",\"sys\":131,\"dia\":86,\"mean\":108.5},{\"day\":\"2026-03-05\",\"sys\":130,\"dia\":84,\"mean\":107},{\"day\":\"2026-03-04\",\"sys\":129,\"dia\":83,\"mean\":106},{\"day\":\"2026-03-02\",\"sys\":133,\"dia\":85,\"mean\":109},{\"day\":\"2026-03-01\",\"sys\":131,\"dia\":84,\"mean\":107.5},{\"day\":\"2026-02-27\",\"sys\":127,\"dia\":81,\"mean\":104},{\"day\":\"2026-02-26\",\"sys\":125,\"dia\":79,\"mean\":102},{\"day\":\"2026-02-24\",\"sys\":121,\"dia\":80,\"mean\":100.5},{\"day\":\"2026-02-23\",\"sys\":126,\"dia\":78,\"mean\":102},{\"day\":\"2026-02-21\",\"sys\":122,\"dia\":75,\"mean\":98.5},{\"day\":\"2026-02-20\",\"sys\":120,\"dia\":79,\"mean\":99.5},{\"day\":\"2026-02-18\",\"sys\":117,\"dia\":76,\"mean\":96.5},{\"day\":\"2026-02-17\",\"sys\":116,\"dia\":75,\"mean\":95.5},{\"day\":\"2026-02-15\",\"sys\":122,\"dia\":78,\"mean\":100},{\"day\":\"2026-02-14\",\"sys\":121,\"dia\":78,\"mean\":99.5},{\"day\":\"2026-02-12\",\"sys\":121,\"dia\":77,\"mean\":99},{\"day\":\"2026-02-11\",\"sys\":121,\"dia\":77,\"mean\":99},{\"day\":\"2026-02-09\",\"sys\":128,\"dia\":81,\"mean\":104.5},{\"day\":\"2026-02-08\",\"sys\":129,\"dia\":81,\"mean\":105},{\"day\":\"2026-02-06\",\"sys\":129,\"dia\":80,\"mean\":104.5},{\"day\":\"2026-02-05\",\"sys\":129,\"dia\":85,\"mean\":107},{\"day\":\"2026-02-03\",\"sys\":128,\"dia\":84,\"mean\":106},{\"day\":\"2026-02-02\",\"sys\":134,\"dia\":83,\"mean\":108.5},{\"day\":\"2026-01-31\",\"sys\":132,\"dia\":86,\"mean\":109},{\"day\":\"2026-01-30\",\"sys\":130,\"dia\":85,\"mean\":107.5},{\"day\":\"2026-01-28\",\"sys\":127,\"dia\":81,\"mean\":104},{\"day\":\"2026-01-27\",\"sys\":125,\"dia\":80,\"mean\":102.5}]},\"weightVsSystolic\":{\"correlation\":{\"r\":0.385,\"strength\":\"schwach\",\"n\":47},\"pairs\":[{\"day\":\"2026-04-24\",\"weight\":183.2,\"systolic\":120},{\"day\":\"2026-04-22\",\"weight\":183,\"systolic\":117},{\"day\":\"2026-04-18\",\"weight\":183,\"systolic\":121},{\"day\":\"2026-04-16\",\"weight\":183.2,\"systolic\":121},{\"day\":\"2026-04-12\",\"weight\":183.7,\"systolic\":128},{\"day\":\"2026-04-10\",\"weight\":184,\"systolic\":129},{\"day\":\"2026-04-06\",\"weight\":184.2,\"systolic\":134},{\"day\":\"2026-04-04\",\"weight\":184.1,\"systolic\":132},{\"day\":\"2026-03-31\",\"weight\":183.5,\"systolic\":125},{\"day\":\"2026-03-29\",\"weight\":183.1,\"systolic\":128},{\"day\":\"2026-03-25\",\"weight\":182.3,\"systolic\":119},{\"day\":\"2026-03-23\",\"weight\":182.1,\"systolic\":123},{\"day\":\"2026-03-19\",\"weight\":181.9,\"systolic\":118},{\"day\":\"2026-03-17\",\"weight\":182,\"systolic\":117},{\"day\":\"2026-03-13\",\"weight\":182.5,\"systolic\":125},{\"day\":\"2026-03-11\",\"weight\":182.8,\"systolic\":125},{\"day\":\"2026-03-07\",\"weight\":183.1,\"systolic\":131},{\"day\":\"2026-03-05\",\"weight\":183.1,\"systolic\":130},{\"day\":\"2026-03-01\",\"weight\":182.7,\"systolic\":131},{\"day\":\"2026-02-27\",\"weight\":182.4,\"systolic\":127},{\"day\":\"2026-02-23\",\"weight\":181.6,\"systolic\":126},{\"day\":\"2026-02-21\",\"weight\":181.2,\"systolic\":122},{\"day\":\"2026-02-17\",\"weight\":180.8,\"systolic\":116},{\"day\":\"2026-02-15\",\"weight\":180.9,\"systolic\":122},{\"day\":\"2026-02-11\",\"weight\":181.2,\"systolic\":121},{\"day\":\"2026-02-09\",\"weight\":181.5,\"systolic\":128},{\"day\":\"2026-02-05\",\"weight\":182,\"systolic\":129},{\"day\":\"2026-02-03\",\"weight\":182.1,\"systolic\":128},{\"day\":\"2026-01-30\",\"weight\":181.9,\"systolic\":130},{\"day\":\"2026-01-28\",\"weight\":181.6,\"systolic\":127}]},\"weightVsMeanBloodPressure\":{\"correlation\":{\"r\":0.409,\"strength\":\"moderat\",\"n\":47},\"pairs\":[{\"day\":\"2026-04-24\",\"weight\":183.2,\"meanBloodPressure\":98.5},{\"day\":\"2026-04-22\",\"weight\":183,\"meanBloodPressure\":95.5},{\"day\":\"2026-04-18\",\"weight\":183,\"meanBloodPressure\":98.5},{\"day\":\"2026-04-16\",\"weight\":183.2,\"meanBloodPressure\":100.5},{\"day\":\"2026-04-12\",\"weight\":183.7,\"meanBloodPressure\":103.5},{\"day\":\"2026-04-10\",\"weight\":184,\"meanBloodPressure\":106},{\"day\":\"2026-04-06\",\"weight\":184.2,\"meanBloodPressure\":110},{\"day\":\"2026-04-04\",\"weight\":184.1,\"meanBloodPressure\":108},{\"day\":\"2026-03-31\",\"weight\":183.5,\"meanBloodPressure\":104},{\"day\":\"2026-03-29\",\"weight\":183.1,\"meanBloodPressure\":103.5},{\"day\":\"2026-03-25\",\"weight\":182.3,\"meanBloodPressure\":98},{\"day\":\"2026-03-23\",\"weight\":182.1,\"meanBloodPressure\":98.5},{\"day\":\"2026-03-19\",\"weight\":181.9,\"meanBloodPressure\":96.5},{\"day\":\"2026-03-17\",\"weight\":182,\"meanBloodPressure\":98},{\"day\":\"2026-03-13\",\"weight\":182.5,\"meanBloodPressure\":101.5},{\"day\":\"2026-03-11\",\"weight\":182.8,\"meanBloodPressure\":103.5},{\"day\":\"2026-03-07\",\"weight\":183.1,\"meanBloodPressure\":108.5},{\"day\":\"2026-03-05\",\"weight\":183.1,\"meanBloodPressure\":107},{\"day\":\"2026-03-01\",\"weight\":182.7,\"meanBloodPressure\":107.5},{\"day\":\"2026-02-27\",\"weight\":182.4,\"meanBloodPressure\":104},{\"day\":\"2026-02-23\",\"weight\":181.6,\"meanBloodPressure\":102},{\"day\":\"2026-02-21\",\"weight\":181.2,\"meanBloodPressure\":98.5},{\"day\":\"2026-02-17\",\"weight\":180.8,\"meanBloodPressure\":95.5},{\"day\":\"2026-02-15\",\"weight\":180.9,\"meanBloodPressure\":100},{\"day\":\"2026-02-11\",\"weight\":181.2,\"meanBloodPressure\":99},{\"day\":\"2026-02-09\",\"weight\":181.5,\"meanBloodPressure\":104.5},{\"day\":\"2026-02-05\",\"weight\":182,\"meanBloodPressure\":107},{\"day\":\"2026-02-03\",\"weight\":182.1,\"meanBloodPressure\":106},{\"day\":\"2026-01-30\",\"weight\":181.9,\"meanBloodPressure\":107.5},{\"day\":\"2026-01-28\",\"weight\":181.6,\"meanBloodPressure\":104}]},\"moodContext\":{\"points\":70,\"mean\":3.03,\"latest\":1,\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-15\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W15\",\"min\":2,\"max\":5,\"mean\":3.33,\"n\":6},{\"weekISO\":\"2026-W16\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":1,\"max\":5,\"mean\":3,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":1,\"max\":4,\"mean\":2.57,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1}],\"monthly\":[],\"yearly\":[]},\"moodVsWeightCorrelation\":{\"r\":0.523,\"strength\":\"moderat\",\"n\":35}}}", + "", + "---", + "", + "### bmi", + "", + "Date: 2026-06-15", + "OPENER HINT: Open with what this means for the user today, then the number behind it.", + "Write one short assessment of this person's BMI. Open with what its band placement MEANS in plain words — where it sits and whether that's holding or shifting, not the number (e.g. \"sitting comfortably in the healthy band\", \"edging toward the next band up\") — then bring in the current value and WHO band right after as support, saying against their own weekly/monthly baseline whether it has crossed a band or is nearing a boundary; never lead with the value. Close with one doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Leave the weight pace to the weight card. Judge confidence from the measurement count and recency.", + "", + "PREVIOUS ANALYSIS: none on file. Treat this as the user's first analysis for this domain — no improvement/regression delta to surface.", + "", + "", + "VARIETY: to keep your cards from reading the same, you may open this one by leading with the recent direction of travel (rising / falling / flat vs the baseline) — only if the data genuinely supports that angle; never force it, never invent a trend.", + "", + "DATA STRENGTH: 70 day-buckets in the window, latest reading today.", + "", + "REPETITION: you have already given a similar \"no material change\" assessment for this metric 10 time(s) in a row. Do NOT restate the same level again. Acknowledge the continuity in one short clause (\"still holding steady, 11 checks running\") and then pivot to a DIFFERENT facet — consistency, time-of-day pattern, or a correlated metric — or, if nothing is genuinely actionable, say so plainly and skip the manufactured step.", + "", + "", + "{\"locale\":\"en\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"bmi\",\"dataCoverage\":{\"totalMeasurements\":70,\"totalSpanDays\":138,\"newestMeasurementDaysAgo\":0},\"bmi\":{\"signal\":{\"metric\":\"your BMI\",\"current\":26.57,\"currentWindowDays\":11,\"baseline\":26.06,\"baselineLabel\":\"your monthly average\",\"delta\":0.51,\"deltaPct\":2,\"spread\":0.1,\"outsideNormalSwing\":true,\"direction\":\"target-band\",\"n\":11,\"newestDaysAgo\":0,\"normalRange\":{\"low\":18.5,\"high\":24.9},\"placement\":\"above band\"},\"summary\":{\"points\":70,\"start\":26.57,\"end\":26,\"delta\":-0.57,\"mean\":26.26,\"min\":25.89,\"max\":26.66},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":26.39,\"max\":26.39,\"mean\":26.39,\"n\":1},{\"date\":\"2026-05-28\",\"min\":26.43,\"max\":26.43,\"mean\":26.43,\"n\":1},{\"date\":\"2026-05-30\",\"min\":26.49,\"max\":26.49,\"mean\":26.49,\"n\":1},{\"date\":\"2026-06-01\",\"min\":26.55,\"max\":26.55,\"mean\":26.55,\"n\":1},{\"date\":\"2026-06-03\",\"min\":26.6,\"max\":26.6,\"mean\":26.6,\"n\":1},{\"date\":\"2026-06-05\",\"min\":26.64,\"max\":26.64,\"mean\":26.64,\"n\":1},{\"date\":\"2026-06-07\",\"min\":26.66,\"max\":26.66,\"mean\":26.66,\"n\":1},{\"date\":\"2026-06-09\",\"min\":26.66,\"max\":26.66,\"mean\":26.66,\"n\":1},{\"date\":\"2026-06-11\",\"min\":26.65,\"max\":26.65,\"mean\":26.65,\"n\":1},{\"date\":\"2026-06-13\",\"min\":26.62,\"max\":26.62,\"mean\":26.62,\"n\":1},{\"date\":\"2026-06-15\",\"min\":26.57,\"max\":26.57,\"mean\":26.57,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":26.04,\"max\":26.05,\"mean\":26.04,\"n\":3},{\"weekISO\":\"2026-W13\",\"min\":26.06,\"max\":26.22,\"mean\":26.14,\"n\":4},{\"weekISO\":\"2026-W14\",\"min\":26.28,\"max\":26.36,\"mean\":26.32,\"n\":3},{\"weekISO\":\"2026-W15\",\"min\":26.3,\"max\":26.37,\"mean\":26.34,\"n\":4},{\"weekISO\":\"2026-W16\",\"min\":26.2,\"max\":26.26,\"mean\":26.23,\"n\":3},{\"weekISO\":\"2026-W17\",\"min\":26.19,\"max\":26.27,\"mean\":26.22,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":26.33,\"max\":26.44,\"mean\":26.38,\"n\":3},{\"weekISO\":\"2026-W19\",\"min\":26.48,\"max\":26.52,\"mean\":26.51,\"n\":4},{\"weekISO\":\"2026-W20\",\"min\":26.4,\"max\":26.48,\"mean\":26.44,\"n\":3},{\"weekISO\":\"2026-W21\",\"min\":26.34,\"max\":26.36,\"mean\":26.35,\"n\":4}],\"monthly\":[{\"month\":\"2026-01\",\"min\":26,\"max\":26.04,\"mean\":26.02,\"n\":2},{\"month\":\"2026-02\",\"min\":25.89,\"max\":26.11,\"mean\":25.99,\"n\":14},{\"month\":\"2026-03\",\"min\":26.09,\"max\":26.22,\"mean\":26.17,\"n\":8}],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":0,\"value\":26.57,\"classification\":{\"category\":\"Overweight\",\"color\":\"var(--warning)\",\"severity\":\"warning\"},\"deltaToPreviousDailyPoint\":-0.04},\"target\":{\"greenMin\":18.5,\"greenMax\":24.9}}}", + "", + "---", + "", + "### mood", + "", + "Date: 2026-06-15", + "OPENER HINT: Open with what this means for the user today, then the number behind it.", + "Write one short assessment of this person's mood. Open with what the recent pattern MEANS in plain words — the read, not the number (e.g. \"a brighter stretch than usual\", \"steady, holding where it's been\") — then bring in the recent level right after as support, placed against their own weekly/monthly baseline; never lead with the value. Close with one kind, doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the entry count and recency.", + "", + "PREVIOUS ANALYSIS: none on file. Treat this as the user's first analysis for this domain — no improvement/regression delta to surface.", + "", + "", + "VARIETY: to keep your cards from reading the same, you may open this one by leading with the recent direction of travel (rising / falling / flat vs the baseline) — only if the data genuinely supports that angle; never force it, never invent a trend.", + "", + "DATA STRENGTH: 70 day-buckets in the window, latest reading today.", + "", + "", + "{\"locale\":\"en\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"mood\",\"dataCoverage\":{\"totalEntries\":70,\"totalSpanDays\":69,\"newestEntryDaysAgo\":0},\"mood\":{\"signal\":{\"metric\":\"your mood\",\"unit\":\"/5\",\"current\":3.15,\"currentWindowDays\":20,\"baseline\":3.24,\"baselineLabel\":\"your recent-weeks average\",\"delta\":-0.09,\"deltaPct\":-2.7,\"spread\":0.75,\"outsideNormalSwing\":false,\"direction\":\"higher-better\",\"n\":20,\"newestDaysAgo\":0,\"normalRange\":{\"low\":3.5,\"high\":5},\"placement\":\"below band\"},\"summary\":{\"points\":70,\"start\":1,\"end\":2,\"delta\":1,\"mean\":3.03,\"min\":1,\"max\":5},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W15\",\"min\":2,\"max\":5,\"mean\":3.33,\"n\":6},{\"weekISO\":\"2026-W16\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":1,\"max\":5,\"mean\":3,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":1,\"max\":4,\"mean\":2.57,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1}],\"monthly\":[],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":0,\"value\":1,\"deltaToPreviousDailyPoint\":-2},\"target\":{\"greenMin\":3.5,\"greenMax\":5,\"orangeMin\":2,\"orangeMax\":3.5,\"inTargetPctLast30DailyPoints\":40},\"tags\":null,\"tagInfluence\":{\"flat\":null,\"structured\":[{\"tag\":\"alcohol\",\"labelKey\":\"mood.tag.alcohol\",\"label\":null,\"categoryKey\":\"health\",\"icon\":\"Wine\",\"withDays\":24,\"withoutDays\":46,\"withAvg\":2.96,\"withoutAvg\":3.07,\"delta\":-0.11,\"pValue\":0.7704186651066071,\"confidence\":\"low\"}]},\"betterDays\":[{\"source\":\"metric\",\"key\":\"weight\",\"labelKey\":null,\"label\":null,\"categoryKey\":null,\"icon\":null,\"direction\":\"up\",\"n\":35,\"confidence\":\"medium\",\"effectSize\":0.523,\"delta\":null,\"r\":0.523},{\"source\":\"tag\",\"key\":\"alcohol\",\"labelKey\":\"mood.tag.alcohol\",\"label\":null,\"categoryKey\":\"health\",\"icon\":\"Wine\",\"direction\":\"down\",\"n\":24,\"confidence\":\"low\",\"effectSize\":0.07670850767085077,\"delta\":-0.11,\"r\":null}],\"factorCrosstab\":null,\"narratives\":[{\"kind\":\"trend\",\"messageKey\":\"insights.mood.narrative.trendUp\",\"vars\":{\"direction\":\"up\",\"delta\":\"0.9\",\"days\":\"7\"},\"strength\":0.86},{\"kind\":\"weekday-dip\",\"messageKey\":\"insights.mood.narrative.weekdayDip\",\"vars\":{\"weekdayKey\":\"charts.weekdaysFull.mon\",\"delta\":\"0.3\"},\"strength\":0.33},{\"kind\":\"in-target\",\"messageKey\":\"insights.mood.narrative.inTarget\",\"vars\":{\"pct\":\"40\"},\"strength\":0.2},{\"kind\":\"streak\",\"messageKey\":\"insights.mood.narrative.streak\",\"vars\":{\"days\":\"70\"},\"strength\":0.15}],\"timeOfDay\":{\"buckets\":[{\"bucket\":\"morning\",\"avgScore\":2.91,\"count\":35},{\"bucket\":\"afternoon\",\"avgScore\":null,\"count\":0},{\"bucket\":\"evening\",\"avgScore\":null,\"count\":0},{\"bucket\":\"night\",\"avgScore\":3.14,\"count\":35}],\"best\":\"night\",\"worst\":\"morning\"},\"stability\":{\"score\":6,\"stdDev\":1.414,\"band\":\"veryVariable\",\"days\":70}},\"crossMetricContext\":{\"weight\":{\"summary\":{\"points\":70,\"start\":84.2,\"end\":82.38,\"delta\":-1.82,\"mean\":83.19,\"min\":82.03,\"max\":84.48},\"correlation\":{\"r\":0.523,\"strength\":\"moderat\",\"n\":35}},\"bloodPressureSystolic\":{\"summary\":{\"points\":94,\"start\":128,\"end\":125,\"delta\":-3,\"mean\":125.39,\"min\":116,\"max\":134},\"correlation\":null},\"pulse\":{\"summary\":{\"points\":140,\"start\":70,\"end\":72,\"delta\":2,\"mean\":70.37,\"min\":65,\"max\":75},\"correlation\":null}}}", + "", + "---", + "", + "### compliance", + "", + "Date: 2026-06-15", + "OPENER HINT: Open with the direction of change over time, then say where it stands now.", + "Write one short assessment of this person's medication adherence. Open with what it MEANS in plain words — how reliable the routine is looking, not the number (e.g. \"your routine's been rock-solid\", \"a few doses have slipped lately\") — then bring in the recent rate right after as support, placed against their own baseline (compliance7 vs compliance30); never lead with the value. Close with one doable, blame-free step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the event count and recency.", + "", + "PREVIOUS ANALYSIS: none on file. Treat this as the user's first analysis for this domain — no improvement/regression delta to surface.", + "", + "", + "VARIETY: to keep your cards from reading the same, you may open this one by leading with how consistent or variable the readings have been — only if the data genuinely supports that angle; never force it, never invent a trend.", + "", + "DATA STRENGTH: 176 day-buckets in the window, latest reading 1 day ago.", + "", + "", + "{\"locale\":\"en\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"medication_compliance\",\"dataCoverage\":{\"totalMeasurements\":176,\"totalSpanDays\":98,\"newestMeasurementDaysAgo\":1},\"overall\":{\"medicationCount\":2,\"averageCompliance30\":79,\"averageCompliance7\":67},\"medications\":[{\"medicationId\":\"parity-rich-med-bp\",\"name\":\"Ramipril\",\"dose\":\"5mg\",\"category\":\"BLOOD_PRESSURE\",\"schedulesPerDay\":1,\"compliance7\":67,\"compliance30\":79,\"streak\":0,\"taken7\":4,\"skipped7\":0,\"missed7\":2,\"signal\":{\"metric\":\"adherence for Ramipril\",\"unit\":\"%\",\"current\":80,\"currentWindowDays\":20,\"baseline\":21.88,\"baselineLabel\":\"your monthly average\",\"delta\":58.13,\"deltaPct\":265.7,\"spread\":30.94,\"outsideNormalSwing\":true,\"direction\":\"higher-better\",\"n\":20,\"newestDaysAgo\":null},\"dailySeries\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-27\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-28\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-29\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-30\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-31\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-01\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-02\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-03\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-04\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-05\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-06\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-07\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-08\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-09\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-10\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-11\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-12\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-13\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-14\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":0,\"max\":100,\"mean\":83.33,\"n\":6},{\"weekISO\":\"2026-W13\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W14\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W15\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W16\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":100,\"max\":100,\"mean\":100,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1}],\"monthly\":[{\"month\":\"2026-02\",\"min\":0,\"max\":0,\"mean\":0,\"n\":13},{\"month\":\"2026-03\",\"min\":0,\"max\":100,\"mean\":43.75,\"n\":16}],\"yearly\":[]},\"latestDay\":{\"dayOffset\":1,\"value\":100,\"n\":1}},{\"medicationId\":\"parity-rich-med-d\",\"name\":\"Vitamin D\",\"dose\":\"1000 IE\",\"category\":\"OTHER\",\"schedulesPerDay\":1,\"compliance7\":67,\"compliance30\":79,\"streak\":0,\"taken7\":4,\"skipped7\":0,\"missed7\":2,\"signal\":{\"metric\":\"adherence for Vitamin D\",\"unit\":\"%\",\"current\":80,\"currentWindowDays\":20,\"baseline\":21.88,\"baselineLabel\":\"your monthly average\",\"delta\":58.13,\"deltaPct\":265.7,\"spread\":30.94,\"outsideNormalSwing\":true,\"direction\":\"higher-better\",\"n\":20,\"newestDaysAgo\":null},\"dailySeries\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-27\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-28\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-29\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-30\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-31\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-01\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-02\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-03\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-04\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-05\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-06\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-07\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-08\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-09\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-10\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-11\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-12\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-13\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-14\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":0,\"max\":100,\"mean\":83.33,\"n\":6},{\"weekISO\":\"2026-W13\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W14\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W15\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W16\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":100,\"max\":100,\"mean\":100,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1}],\"monthly\":[{\"month\":\"2026-02\",\"min\":0,\"max\":0,\"mean\":0,\"n\":13},{\"month\":\"2026-03\",\"min\":0,\"max\":100,\"mean\":43.75,\"n\":16}],\"yearly\":[]},\"latestDay\":{\"dayOffset\":1,\"value\":100,\"n\":1}}]}", + "", + "---", + "", + "### general", + "", + "Date: 2026-06-15", + "OPENER HINT: Open with what this means for the user today, then the number behind it.", + "Write one short overall assessment across the available health metrics. Open with the overall read in plain words — how things are looking taken together, not a number (e.g. \"a steady stretch across the board\", \"one thing standing out this week\") — then bring in the one or two metrics that stand out as support, each placed against the person's own weekly/monthly baseline; never lead with a value. Close with the single most important doable step only when the overall picture genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the measurement count, density and recency.", + "", + "PREVIOUS ANALYSIS: none on file. Treat this as the user's first analysis for this domain — no improvement/regression delta to surface.", + "", + "", + "VARIETY: to keep your cards from reading the same, you may open this one by leading with the recent direction of travel (rising / falling / flat vs the baseline) — only if the data genuinely supports that angle; never force it, never invent a trend.", + "", + "DATA STRENGTH: 887 day-buckets in the window, latest reading today.", + "", + "", + "{\"locale\":\"en\",\"generatedForDay\":\"2026-06-15\",\"interpretationHint\":\"Use trend direction and deltas. Prioritize the newest data if trends conflict. Consider dataCoverage for reliability assessment.\",\"dataCoverage\":{\"totalMeasurements\":887,\"totalSpanDays\":140,\"newestMeasurementDaysAgo\":0,\"avgDaysBetweenMeasurements\":0.2},\"measurementSeries\":{\"WEIGHT\":{\"summary\":{\"points\":70,\"start\":84.2,\"end\":82.38,\"delta\":-1.82,\"mean\":83.19,\"min\":82.03,\"max\":84.48},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":83.6,\"max\":83.6,\"mean\":83.6,\"n\":1},{\"date\":\"2026-05-28\",\"min\":83.75,\"max\":83.75,\"mean\":83.75,\"n\":1},{\"date\":\"2026-05-30\",\"min\":83.94,\"max\":83.94,\"mean\":83.94,\"n\":1},{\"date\":\"2026-06-01\",\"min\":84.12,\"max\":84.12,\"mean\":84.12,\"n\":1},{\"date\":\"2026-06-03\",\"min\":84.29,\"max\":84.29,\"mean\":84.29,\"n\":1},{\"date\":\"2026-06-05\",\"min\":84.41,\"max\":84.41,\"mean\":84.41,\"n\":1},{\"date\":\"2026-06-07\",\"min\":84.48,\"max\":84.48,\"mean\":84.48,\"n\":1},{\"date\":\"2026-06-09\",\"min\":84.48,\"max\":84.48,\"mean\":84.48,\"n\":1},{\"date\":\"2026-06-11\",\"min\":84.43,\"max\":84.43,\"mean\":84.43,\"n\":1},{\"date\":\"2026-06-13\",\"min\":84.33,\"max\":84.33,\"mean\":84.33,\"n\":1},{\"date\":\"2026-06-15\",\"min\":84.2,\"max\":84.2,\"mean\":84.2,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":82.38,\"max\":82.51,\"mean\":82.45,\"n\":2},{\"month\":\"2026-02\",\"min\":82.03,\"max\":82.73,\"mean\":82.35,\"n\":14},{\"month\":\"2026-03\",\"min\":82.65,\"max\":83.07,\"mean\":82.92,\"n\":8}]}},\"BLOOD_PRESSURE_SYS\":{\"summary\":{\"points\":94,\"start\":128,\"end\":125,\"delta\":-3,\"mean\":125.39,\"min\":116,\"max\":134},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":119,\"max\":119,\"mean\":119,\"n\":1},{\"date\":\"2026-05-28\",\"min\":121,\"max\":121,\"mean\":121,\"n\":1},{\"date\":\"2026-05-30\",\"min\":126,\"max\":126,\"mean\":126,\"n\":1},{\"date\":\"2026-05-31\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-02\",\"min\":125,\"max\":125,\"mean\":125,\"n\":1},{\"date\":\"2026-06-03\",\"min\":127,\"max\":127,\"mean\":127,\"n\":1},{\"date\":\"2026-06-05\",\"min\":130,\"max\":130,\"mean\":130,\"n\":1},{\"date\":\"2026-06-06\",\"min\":132,\"max\":132,\"mean\":132,\"n\":1},{\"date\":\"2026-06-08\",\"min\":134,\"max\":134,\"mean\":134,\"n\":1},{\"date\":\"2026-06-09\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-11\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-12\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-14\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-15\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"month\":\"2026-02\",\"min\":116,\"max\":134,\"mean\":124.22,\"n\":18},{\"month\":\"2026-03\",\"min\":124,\"max\":133,\"mean\":128.18,\"n\":11}]}},\"BLOOD_PRESSURE_DIA\":{\"summary\":{\"points\":94,\"start\":82,\"end\":80,\"delta\":-2,\"mean\":80.31,\"min\":74,\"max\":86},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":76,\"max\":76,\"mean\":76,\"n\":1},{\"date\":\"2026-05-28\",\"min\":77,\"max\":77,\"mean\":77,\"n\":1},{\"date\":\"2026-05-30\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-05-31\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-02\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-03\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-05\",\"min\":86,\"max\":86,\"mean\":86,\"n\":1},{\"date\":\"2026-06-06\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-08\",\"min\":84,\"max\":84,\"mean\":84,\"n\":1},{\"date\":\"2026-06-09\",\"min\":85,\"max\":85,\"mean\":85,\"n\":1},{\"date\":\"2026-06-11\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-12\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-14\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-15\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":80,\"max\":86,\"mean\":83,\"n\":4},{\"month\":\"2026-02\",\"min\":75,\"max\":85,\"mean\":79.28,\"n\":18},{\"month\":\"2026-03\",\"min\":78,\"max\":86,\"mean\":82,\"n\":11}]}},\"PULSE\":{\"summary\":{\"points\":122,\"start\":70,\"end\":68,\"delta\":-2,\"mean\":70.13,\"min\":65,\"max\":75},\"series\":{\"recent\":[{\"date\":\"2026-05-25\",\"min\":62,\"max\":74,\"mean\":68,\"n\":2},{\"date\":\"2026-05-26\",\"min\":62,\"max\":74,\"mean\":66,\"n\":3},{\"date\":\"2026-05-27\",\"min\":63,\"max\":75,\"mean\":67,\"n\":3},{\"date\":\"2026-05-28\",\"min\":64,\"max\":76,\"mean\":68,\"n\":3},{\"date\":\"2026-05-29\",\"min\":65,\"max\":77,\"mean\":69,\"n\":3},{\"date\":\"2026-05-30\",\"min\":66,\"max\":78,\"mean\":70,\"n\":3},{\"date\":\"2026-05-31\",\"min\":67,\"max\":79,\"mean\":71,\"n\":3},{\"date\":\"2026-06-01\",\"min\":68,\"max\":80,\"mean\":72,\"n\":3},{\"date\":\"2026-06-02\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-03\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-04\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-05\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-06\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-07\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-08\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-09\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-10\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-11\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-12\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-13\",\"min\":68,\"max\":80,\"mean\":72,\"n\":3},{\"date\":\"2026-06-14\",\"min\":67,\"max\":79,\"mean\":71,\"n\":3},{\"date\":\"2026-06-15\",\"min\":66,\"max\":66,\"mean\":66,\"n\":1}],\"monthly\":[{\"month\":\"2026-02\",\"min\":61,\"max\":81,\"mean\":67.67,\"n\":44},{\"month\":\"2026-03\",\"min\":63,\"max\":83,\"mean\":72.25,\"n\":46}]}},\"SLEEP_DURATION\":{\"summary\":{\"points\":89,\"start\":417,\"end\":380,\"delta\":-37,\"mean\":410.85,\"min\":375,\"max\":445},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":384,\"max\":384,\"mean\":384,\"n\":1},{\"date\":\"2026-05-27\",\"min\":389,\"max\":389,\"mean\":389,\"n\":1},{\"date\":\"2026-05-28\",\"min\":395,\"max\":395,\"mean\":395,\"n\":1},{\"date\":\"2026-05-29\",\"min\":401,\"max\":401,\"mean\":401,\"n\":1},{\"date\":\"2026-05-30\",\"min\":408,\"max\":408,\"mean\":408,\"n\":1},{\"date\":\"2026-05-31\",\"min\":415,\"max\":415,\"mean\":415,\"n\":1},{\"date\":\"2026-06-01\",\"min\":422,\"max\":422,\"mean\":422,\"n\":1},{\"date\":\"2026-06-02\",\"min\":428,\"max\":428,\"mean\":428,\"n\":1},{\"date\":\"2026-06-03\",\"min\":434,\"max\":434,\"mean\":434,\"n\":1},{\"date\":\"2026-06-04\",\"min\":438,\"max\":438,\"mean\":438,\"n\":1},{\"date\":\"2026-06-05\",\"min\":442,\"max\":442,\"mean\":442,\"n\":1},{\"date\":\"2026-06-06\",\"min\":444,\"max\":444,\"mean\":444,\"n\":1},{\"date\":\"2026-06-07\",\"min\":445,\"max\":445,\"mean\":445,\"n\":1},{\"date\":\"2026-06-08\",\"min\":444,\"max\":444,\"mean\":444,\"n\":1},{\"date\":\"2026-06-09\",\"min\":443,\"max\":443,\"mean\":443,\"n\":1},{\"date\":\"2026-06-10\",\"min\":439,\"max\":439,\"mean\":439,\"n\":1},{\"date\":\"2026-06-11\",\"min\":435,\"max\":435,\"mean\":435,\"n\":1},{\"date\":\"2026-06-12\",\"min\":430,\"max\":430,\"mean\":430,\"n\":1},{\"date\":\"2026-06-13\",\"min\":424,\"max\":424,\"mean\":424,\"n\":1},{\"date\":\"2026-06-14\",\"min\":417,\"max\":417,\"mean\":417,\"n\":1}],\"monthly\":[]}},\"BLOOD_GLUCOSE\":{\"summary\":{\"points\":35,\"start\":96,\"end\":108,\"delta\":12,\"mean\":98.46,\"min\":87,\"max\":109},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":91,\"max\":91,\"mean\":91,\"n\":1},{\"date\":\"2026-05-30\",\"min\":99,\"max\":99,\"mean\":99,\"n\":1},{\"date\":\"2026-06-03\",\"min\":102,\"max\":102,\"mean\":102,\"n\":1},{\"date\":\"2026-06-07\",\"min\":107,\"max\":107,\"mean\":107,\"n\":1},{\"date\":\"2026-06-11\",\"min\":106,\"max\":106,\"mean\":106,\"n\":1},{\"date\":\"2026-06-15\",\"min\":96,\"max\":96,\"mean\":96,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":108,\"max\":108,\"mean\":108,\"n\":1},{\"month\":\"2026-02\",\"min\":88,\"max\":105,\"mean\":96.71,\"n\":7},{\"month\":\"2026-03\",\"min\":94,\"max\":108,\"mean\":101.75,\"n\":4}]}},\"RESTING_HEART_RATE\":{\"summary\":{\"points\":140,\"start\":58,\"end\":62,\"delta\":4,\"mean\":59.71,\"min\":55,\"max\":64},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":56,\"max\":56,\"mean\":56,\"n\":1},{\"date\":\"2026-05-27\",\"min\":59,\"max\":59,\"mean\":59,\"n\":1},{\"date\":\"2026-05-28\",\"min\":59,\"max\":59,\"mean\":59,\"n\":1},{\"date\":\"2026-05-29\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1},{\"date\":\"2026-05-30\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1},{\"date\":\"2026-05-31\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-01\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-02\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-03\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-04\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-05\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-06\",\"min\":62,\"max\":62,\"mean\":62,\"n\":1},{\"date\":\"2026-06-07\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-08\",\"min\":64,\"max\":64,\"mean\":64,\"n\":1},{\"date\":\"2026-06-09\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-10\",\"min\":62,\"max\":62,\"mean\":62,\"n\":1},{\"date\":\"2026-06-11\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-12\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-13\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-14\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-15\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":61,\"max\":64,\"mean\":62,\"n\":5},{\"month\":\"2026-02\",\"min\":55,\"max\":64,\"mean\":59.18,\"n\":28},{\"month\":\"2026-03\",\"min\":57,\"max\":64,\"mean\":60.94,\"n\":16}]}}},\"medicationAdherence\":{\"summary\":{\"points\":88,\"start\":100,\"end\":100,\"delta\":0,\"mean\":90.91,\"min\":0,\"max\":100},\"series\":{\"recent\":[{\"date\":\"2026-05-25\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-26\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-27\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-28\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-29\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-30\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-31\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-01\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-03\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-04\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-05\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-06\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-07\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-08\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-09\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-10\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-12\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-13\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-14\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1}],\"monthly\":[{\"month\":\"2026-03\",\"min\":100,\"max\":100,\"mean\":100,\"n\":7}]}},\"bloodPressureTargets\":{\"systolic\":{\"min\":120,\"max\":129},\"diastolic\":{\"min\":70,\"max\":79},\"inTargetPctLast30DailyPoints\":40},\"moodContext\":{\"points\":70,\"mean\":3.03,\"latest\":1,\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1}],\"monthly\":[]}}}" + ] + } + ], + "temperature": 0.45, + "maxTokens": 1400, + "seed": 1618033, + "responseFormat": "json", + "timeoutMs": 60000 + }, + { + "system": [ + "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", + "", + "YOUR DATA (graded snapshot):", + "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", + "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", + "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", + "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", + "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", + "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", + "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", + "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", + "", + "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", + "Moves available (choose the few that genuinely apply):", + "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", + "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", + "(c) name a genuine, earned win or a steady streak when the data shows one;", + "(d) one watch-item worth keeping an eye on;", + "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", + "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", + "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", + "", + "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", + "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", + "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", + "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", + "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", + "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", + "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", + "", + "OPENING — lead with meaning, not the metric value", + "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", + "", + "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", + "", + "JUDGE THE DATA HONESTLY (never invent a trend):", + "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", + "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", + "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", + "", + "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", + "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", + "", + "METRIC IDENTIFIERS STAY OUT OF PROSE", + "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", + "", + "GLP-1 DOSE SAFETY (contract, not style)", + "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", + "", + "ACUTE RED FLAGS (contract, not style)", + "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", + "", + "GROUNDING — ZERO HALLUCINATIONS", + "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", + "", + "TONE — a warm, motivating advisor, never clinical or alarming", + "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", + "", + "NOT AN INSIGHT — never recite mechanics", + "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", + "", + "INTERPRET, DON'T ENUMERATE", + "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", + "", + "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", + "", + "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", + "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", + "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", + "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", + "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", + "", + "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in English: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", + "{ \"summary\": \"...\" }", + "", + "METRIC — MOOD / WELL-BEING:", + "- Scale 1 (very bad) to 5 (very good). The snapshot carries mood.signal (the finished comparison) + mood.summary + mood.series (graded daily means), mood.target (green/orange band), mood.latestDayFocus and, where present, mood.tags (most frequent mood tags).", + "- The weekly pattern matters: lead from mood.signal — current, delta vs baselineLabel and outsideNormalSwing are already computed. STATE them, do NOT recompute. Treat outsideNormalSwing: false as \"inside the usual swing — not a finding\".", + "- Name persistent phases seriously but calmly: several days below ~2.5 or above ~4.5 are worth noting — as an observation, never a diagnosis and never alarming.", + "- When mood stays very low for several days (clearly below ~2.5), offer a support pathway calmly and in an autonomy-preserving way — e.g. \"if the strain persists, talking it through with someone you trust or a professional can help\". Never alarming, never a diagnosis, no emergency tone; a gentle option, not an imperative.", + "- Tags: if mood.tags is present, you may name a recurring tag as possible context, without claiming a link.", + "- Cross-metric: crossMetricContext carries correlations to weight, blood pressure and pulse; mention only when present and |r| > 0.4 — as an association, never a cause. Do not force a cross-link without a clear pattern.", + "- One message: close with ONE doable, kind step ONLY when the finding implies one (e.g. on good days, briefly note what helped). When mood is steadily good, acknowledge that honestly and name one thing worth keeping an eye on instead of manufacturing a step." + ], + "messages": [ + { + "role": "user", + "content": [ + "Date: 2026-06-15", + "OPENER HINT: Open with what this means for the user today, then the number behind it.", + "Write one short assessment of this person's mood. Open with what the recent pattern MEANS in plain words — the read, not the number (e.g. \"a brighter stretch than usual\", \"steady, holding where it's been\") — then bring in the recent level right after as support, placed against their own weekly/monthly baseline; never lead with the value. Close with one kind, doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the entry count and recency.", + "", + "PREVIOUS ANALYSIS: none on file. Treat this as the user's first analysis for this domain — no improvement/regression delta to surface.", + "", + "", + "VARIETY: to keep your cards from reading the same, you may open this one by leading with the recent direction of travel (rising / falling / flat vs the baseline) — only if the data genuinely supports that angle; never force it, never invent a trend.", + "", + "DATA STRENGTH: 70 day-buckets in the window, latest reading today.", + "", + "", + "{\"locale\":\"en\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"mood\",\"dataCoverage\":{\"totalEntries\":70,\"totalSpanDays\":69,\"newestEntryDaysAgo\":0},\"mood\":{\"signal\":{\"metric\":\"your mood\",\"unit\":\"/5\",\"current\":3.15,\"currentWindowDays\":20,\"baseline\":3.24,\"baselineLabel\":\"your recent-weeks average\",\"delta\":-0.09,\"deltaPct\":-2.7,\"spread\":0.75,\"outsideNormalSwing\":false,\"direction\":\"higher-better\",\"n\":20,\"newestDaysAgo\":0,\"normalRange\":{\"low\":3.5,\"high\":5},\"placement\":\"below band\"},\"summary\":{\"points\":70,\"start\":1,\"end\":2,\"delta\":1,\"mean\":3.03,\"min\":1,\"max\":5},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W15\",\"min\":2,\"max\":5,\"mean\":3.33,\"n\":6},{\"weekISO\":\"2026-W16\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":1,\"max\":5,\"mean\":3,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":1,\"max\":4,\"mean\":2.57,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1}],\"monthly\":[],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":0,\"value\":1,\"deltaToPreviousDailyPoint\":-2},\"target\":{\"greenMin\":3.5,\"greenMax\":5,\"orangeMin\":2,\"orangeMax\":3.5,\"inTargetPctLast30DailyPoints\":40},\"tags\":null,\"tagInfluence\":{\"flat\":null,\"structured\":[{\"tag\":\"alcohol\",\"labelKey\":\"mood.tag.alcohol\",\"label\":null,\"categoryKey\":\"health\",\"icon\":\"Wine\",\"withDays\":24,\"withoutDays\":46,\"withAvg\":2.96,\"withoutAvg\":3.07,\"delta\":-0.11,\"pValue\":0.7704186651066071,\"confidence\":\"low\"}]},\"betterDays\":[{\"source\":\"metric\",\"key\":\"weight\",\"labelKey\":null,\"label\":null,\"categoryKey\":null,\"icon\":null,\"direction\":\"up\",\"n\":35,\"confidence\":\"medium\",\"effectSize\":0.523,\"delta\":null,\"r\":0.523},{\"source\":\"tag\",\"key\":\"alcohol\",\"labelKey\":\"mood.tag.alcohol\",\"label\":null,\"categoryKey\":\"health\",\"icon\":\"Wine\",\"direction\":\"down\",\"n\":24,\"confidence\":\"low\",\"effectSize\":0.07670850767085077,\"delta\":-0.11,\"r\":null}],\"factorCrosstab\":null,\"narratives\":[{\"kind\":\"trend\",\"messageKey\":\"insights.mood.narrative.trendUp\",\"vars\":{\"direction\":\"up\",\"delta\":\"0.9\",\"days\":\"7\"},\"strength\":0.86},{\"kind\":\"weekday-dip\",\"messageKey\":\"insights.mood.narrative.weekdayDip\",\"vars\":{\"weekdayKey\":\"charts.weekdaysFull.mon\",\"delta\":\"0.3\"},\"strength\":0.33},{\"kind\":\"in-target\",\"messageKey\":\"insights.mood.narrative.inTarget\",\"vars\":{\"pct\":\"40\"},\"strength\":0.2},{\"kind\":\"streak\",\"messageKey\":\"insights.mood.narrative.streak\",\"vars\":{\"days\":\"70\"},\"strength\":0.15}],\"timeOfDay\":{\"buckets\":[{\"bucket\":\"morning\",\"avgScore\":2.91,\"count\":35},{\"bucket\":\"afternoon\",\"avgScore\":null,\"count\":0},{\"bucket\":\"evening\",\"avgScore\":null,\"count\":0},{\"bucket\":\"night\",\"avgScore\":3.14,\"count\":35}],\"best\":\"night\",\"worst\":\"morning\"},\"stability\":{\"score\":6,\"stdDev\":1.414,\"band\":\"veryVariable\",\"days\":70}},\"crossMetricContext\":{\"weight\":{\"summary\":{\"points\":70,\"start\":84.2,\"end\":82.38,\"delta\":-1.82,\"mean\":83.19,\"min\":82.03,\"max\":84.48},\"correlation\":{\"r\":0.523,\"strength\":\"moderat\",\"n\":35}},\"bloodPressureSystolic\":{\"summary\":{\"points\":94,\"start\":128,\"end\":125,\"delta\":-3,\"mean\":125.39,\"min\":116,\"max\":134},\"correlation\":null},\"pulse\":{\"summary\":{\"points\":140,\"start\":70,\"end\":72,\"delta\":2,\"mean\":70.37,\"min\":65,\"max\":75},\"correlation\":null}}}" + ] + } + ], + "temperature": 0.45, + "maxTokens": 250, + "seed": 1618033, + "responseFormat": "json", + "timeoutMs": 60000 + } + ], + "enqueued": [], + "result": { + "served": 0, + "batched": 6, + "fellBack": 1, + "batchCallMade": true + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "blood-pressure", + "locale": "en", + "text": "Batched parity note for bp.", + "items": null, + "inputHash": null, + "snapshotHash": "07d02bb70e6afb9e8c4d9a8e8bc34e5c4ac8ffefc52501fde23f86d3a43c3ced", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "bmi", + "locale": "en", + "text": "Batched parity note for bmi.", + "items": null, + "inputHash": "c23e6b9fee3631b156b1bfbbb0d1668204963e887a91596cddeed398b3846bdb", + "snapshotHash": "aa09825f5e5cda0d0e0ffdb1c75004ea0b298a0773d065ddef3a682bda8a6918", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "general", + "locale": "en", + "text": "Batched parity note for general.", + "items": null, + "inputHash": null, + "snapshotHash": "6594f84d7983214b4c01ca79b2135e403689e025b342093c02d14642d2a122ee", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "medication-compliance", + "locale": "en", + "text": "Batched parity note for compliance.", + "items": [ + { + "medicationId": "parity-rich-med-bp", + "text": "Ramipril: There is currently not enough consistent detail data for a robust short assessment." + }, + { + "medicationId": "parity-rich-med-d", + "text": "Vitamin D: There is currently not enough consistent detail data for a robust short assessment." + } + ], + "inputHash": null, + "snapshotHash": "f1e6c1fbfdbb7063c9584f1bb223ddd3227557119a1d5bba58432622d22441f3", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "mood", + "locale": "en", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "pulse", + "locale": "en", + "text": "Batched parity note for pulse.", + "items": null, + "inputHash": null, + "snapshotHash": "8c0436633818c14ccc29c868aa50ae3127d0143ff5dc977720845dac1b3180e0", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "weight", + "locale": "en", + "text": "Batched parity note for weight.", + "items": null, + "inputHash": "4cc01c285b2f92494bb578350040930a348bdbd86863a98fd23c3dec38680143", + "snapshotHash": "773348667d5c62724cc6803444fe5518c074e3f54eeb3d69a764b26c33e38255", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + } + ] + }, + "batch-unchanged-en": { + "calls": [ + { + "system": [ + "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", + "", + "YOUR DATA (graded snapshot):", + "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", + "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", + "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", + "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", + "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", + "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", + "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", + "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", + "", + "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", + "Moves available (choose the few that genuinely apply):", + "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", + "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", + "(c) name a genuine, earned win or a steady streak when the data shows one;", + "(d) one watch-item worth keeping an eye on;", + "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", + "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", + "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", + "", + "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", + "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", + "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", + "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", + "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", + "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", + "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", + "", + "OPENING — lead with meaning, not the metric value", + "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", + "", + "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", + "", + "JUDGE THE DATA HONESTLY (never invent a trend):", + "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", + "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", + "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", + "", + "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", + "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", + "", + "METRIC IDENTIFIERS STAY OUT OF PROSE", + "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", + "", + "GLP-1 DOSE SAFETY (contract, not style)", + "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", + "", + "ACUTE RED FLAGS (contract, not style)", + "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", + "", + "GROUNDING — ZERO HALLUCINATIONS", + "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", + "", + "TONE — a warm, motivating advisor, never clinical or alarming", + "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", + "", + "NOT AN INSIGHT — never recite mechanics", + "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", + "", + "INTERPRET, DON'T ENUMERATE", + "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", + "", + "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", + "", + "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", + "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", + "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", + "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", + "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", + "", + "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in English: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", + "{ \"summary\": \"...\" }", + "", + "METRIC — MOOD / WELL-BEING:", + "- Scale 1 (very bad) to 5 (very good). The snapshot carries mood.signal (the finished comparison) + mood.summary + mood.series (graded daily means), mood.target (green/orange band), mood.latestDayFocus and, where present, mood.tags (most frequent mood tags).", + "- The weekly pattern matters: lead from mood.signal — current, delta vs baselineLabel and outsideNormalSwing are already computed. STATE them, do NOT recompute. Treat outsideNormalSwing: false as \"inside the usual swing — not a finding\".", + "- Name persistent phases seriously but calmly: several days below ~2.5 or above ~4.5 are worth noting — as an observation, never a diagnosis and never alarming.", + "- When mood stays very low for several days (clearly below ~2.5), offer a support pathway calmly and in an autonomy-preserving way — e.g. \"if the strain persists, talking it through with someone you trust or a professional can help\". Never alarming, never a diagnosis, no emergency tone; a gentle option, not an imperative.", + "- Tags: if mood.tags is present, you may name a recurring tag as possible context, without claiming a link.", + "- Cross-metric: crossMetricContext carries correlations to weight, blood pressure and pulse; mention only when present and |r| > 0.4 — as an association, never a cause. Do not force a cross-link without a clear pattern.", + "- One message: close with ONE doable, kind step ONLY when the finding implies one (e.g. on good days, briefly note what helped). When mood is steadily good, acknowledge that honestly and name one thing worth keeping an eye on instead of manufacturing a step." + ], + "messages": [ + { + "role": "user", + "content": [ + "Date: 2026-06-15", + "OPENER HINT: Open with what this means for the user today, then the number behind it.", + "Write one short assessment of this person's mood. Open with what the recent pattern MEANS in plain words — the read, not the number (e.g. \"a brighter stretch than usual\", \"steady, holding where it's been\") — then bring in the recent level right after as support, placed against their own weekly/monthly baseline; never lead with the value. Close with one kind, doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the entry count and recency.", + "", + "PREVIOUS ANALYSIS: none on file. Treat this as the user's first analysis for this domain — no improvement/regression delta to surface.", + "", + "", + "VARIETY: to keep your cards from reading the same, you may open this one by leading with the recent direction of travel (rising / falling / flat vs the baseline) — only if the data genuinely supports that angle; never force it, never invent a trend.", + "", + "DATA STRENGTH: 70 day-buckets in the window, latest reading today.", + "", + "", + "{\"locale\":\"en\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"mood\",\"dataCoverage\":{\"totalEntries\":70,\"totalSpanDays\":69,\"newestEntryDaysAgo\":0},\"mood\":{\"signal\":{\"metric\":\"your mood\",\"unit\":\"/5\",\"current\":3.15,\"currentWindowDays\":20,\"baseline\":3.24,\"baselineLabel\":\"your recent-weeks average\",\"delta\":-0.09,\"deltaPct\":-2.7,\"spread\":0.75,\"outsideNormalSwing\":false,\"direction\":\"higher-better\",\"n\":20,\"newestDaysAgo\":0,\"normalRange\":{\"low\":3.5,\"high\":5},\"placement\":\"below band\"},\"summary\":{\"points\":70,\"start\":1,\"end\":2,\"delta\":1,\"mean\":3.03,\"min\":1,\"max\":5},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W15\",\"min\":2,\"max\":5,\"mean\":3.33,\"n\":6},{\"weekISO\":\"2026-W16\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":1,\"max\":5,\"mean\":3,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":1,\"max\":4,\"mean\":2.57,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1}],\"monthly\":[],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":0,\"value\":1,\"deltaToPreviousDailyPoint\":-2},\"target\":{\"greenMin\":3.5,\"greenMax\":5,\"orangeMin\":2,\"orangeMax\":3.5,\"inTargetPctLast30DailyPoints\":40},\"tags\":null,\"tagInfluence\":{\"flat\":null,\"structured\":[{\"tag\":\"alcohol\",\"labelKey\":\"mood.tag.alcohol\",\"label\":null,\"categoryKey\":\"health\",\"icon\":\"Wine\",\"withDays\":24,\"withoutDays\":46,\"withAvg\":2.96,\"withoutAvg\":3.07,\"delta\":-0.11,\"pValue\":0.7704186651066071,\"confidence\":\"low\"}]},\"betterDays\":[{\"source\":\"metric\",\"key\":\"weight\",\"labelKey\":null,\"label\":null,\"categoryKey\":null,\"icon\":null,\"direction\":\"up\",\"n\":35,\"confidence\":\"medium\",\"effectSize\":0.523,\"delta\":null,\"r\":0.523},{\"source\":\"tag\",\"key\":\"alcohol\",\"labelKey\":\"mood.tag.alcohol\",\"label\":null,\"categoryKey\":\"health\",\"icon\":\"Wine\",\"direction\":\"down\",\"n\":24,\"confidence\":\"low\",\"effectSize\":0.07670850767085077,\"delta\":-0.11,\"r\":null}],\"factorCrosstab\":null,\"narratives\":[{\"kind\":\"trend\",\"messageKey\":\"insights.mood.narrative.trendUp\",\"vars\":{\"direction\":\"up\",\"delta\":\"0.9\",\"days\":\"7\"},\"strength\":0.86},{\"kind\":\"weekday-dip\",\"messageKey\":\"insights.mood.narrative.weekdayDip\",\"vars\":{\"weekdayKey\":\"charts.weekdaysFull.mon\",\"delta\":\"0.3\"},\"strength\":0.33},{\"kind\":\"in-target\",\"messageKey\":\"insights.mood.narrative.inTarget\",\"vars\":{\"pct\":\"40\"},\"strength\":0.2},{\"kind\":\"streak\",\"messageKey\":\"insights.mood.narrative.streak\",\"vars\":{\"days\":\"70\"},\"strength\":0.15}],\"timeOfDay\":{\"buckets\":[{\"bucket\":\"morning\",\"avgScore\":2.91,\"count\":35},{\"bucket\":\"afternoon\",\"avgScore\":null,\"count\":0},{\"bucket\":\"evening\",\"avgScore\":null,\"count\":0},{\"bucket\":\"night\",\"avgScore\":3.14,\"count\":35}],\"best\":\"night\",\"worst\":\"morning\"},\"stability\":{\"score\":6,\"stdDev\":1.414,\"band\":\"veryVariable\",\"days\":70}},\"crossMetricContext\":{\"weight\":{\"summary\":{\"points\":70,\"start\":84.2,\"end\":82.38,\"delta\":-1.82,\"mean\":83.19,\"min\":82.03,\"max\":84.48},\"correlation\":{\"r\":0.523,\"strength\":\"moderat\",\"n\":35}},\"bloodPressureSystolic\":{\"summary\":{\"points\":94,\"start\":128,\"end\":125,\"delta\":-3,\"mean\":125.39,\"min\":116,\"max\":134},\"correlation\":null},\"pulse\":{\"summary\":{\"points\":140,\"start\":70,\"end\":72,\"delta\":2,\"mean\":70.37,\"min\":65,\"max\":75},\"correlation\":null}}}" + ] + } + ], + "temperature": 0.45, + "maxTokens": 250, + "seed": 1618033, + "responseFormat": "json", + "timeoutMs": 60000 + } + ], + "enqueued": [], + "result": { + "served": 6, + "batched": 0, + "fellBack": 1, + "batchCallMade": false + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "blood-pressure", + "locale": "en", + "text": "Batched parity note for bp.", + "items": null, + "inputHash": null, + "snapshotHash": "07d02bb70e6afb9e8c4d9a8e8bc34e5c4ac8ffefc52501fde23f86d3a43c3ced", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "bmi", + "locale": "en", + "text": "Batched parity note for bmi.", + "items": null, + "inputHash": "c23e6b9fee3631b156b1bfbbb0d1668204963e887a91596cddeed398b3846bdb", + "snapshotHash": "aa09825f5e5cda0d0e0ffdb1c75004ea0b298a0773d065ddef3a682bda8a6918", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "general", + "locale": "en", + "text": "Batched parity note for general.", + "items": null, + "inputHash": null, + "snapshotHash": "6594f84d7983214b4c01ca79b2135e403689e025b342093c02d14642d2a122ee", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "medication-compliance", + "locale": "en", + "text": "Batched parity note for compliance.", + "items": [ + { + "medicationId": "parity-rich-med-bp", + "text": "Ramipril: There is currently not enough consistent detail data for a robust short assessment." + }, + { + "medicationId": "parity-rich-med-d", + "text": "Vitamin D: There is currently not enough consistent detail data for a robust short assessment." + } + ], + "inputHash": null, + "snapshotHash": "f1e6c1fbfdbb7063c9584f1bb223ddd3227557119a1d5bba58432622d22441f3", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "mood", + "locale": "en", + "text": "Parity note for batch-unchanged-en.", + "items": null, + "inputHash": null, + "snapshotHash": "7dd495a6d92c3bc42320044bbbcbe89154afa13e0a954d9f62dee6239bced6f4", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "pulse", + "locale": "en", + "text": "Batched parity note for pulse.", + "items": null, + "inputHash": null, + "snapshotHash": "8c0436633818c14ccc29c868aa50ae3127d0143ff5dc977720845dac1b3180e0", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "weight", + "locale": "en", + "text": "Batched parity note for weight.", + "items": null, + "inputHash": "4cc01c285b2f92494bb578350040930a348bdbd86863a98fd23c3dec38680143", + "snapshotHash": "773348667d5c62724cc6803444fe5518c074e3f54eeb3d69a764b26c33e38255", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + } + ] + }, + "batch-failed-de": { + "calls": [ + { + "system": [ + "Du schreibst eine kurze, persönliche Einschätzung zu EINER Gesundheitsmetrik dieses Nutzers. Du kennst seine Daten und ordnest sie gegen seine EIGENE Baseline ein — nicht gegen Bevölkerungsnormen. Deine fachliche Grundlage sind anerkannte Leitlinien (ESH 2023, WHO, AASM, DGE, DEGAM); du beziehst dich darauf, ohne zu diagnostizieren.", + "", + "DEINE DATENGRUNDLAGE (graded snapshot):", + "- signal: der fertige Vergleich — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` ist das recent-Mittel; `baseline` ist der EIGENE längere Durchschnitt; `delta`/`deltaPct` sind die VORZEICHENBEHAFTETE Änderung gegenüber dieser Baseline; `spread` ist die normale Schwankung; `outsideNormalSwing` ist das vorab berechnete Urteil, ob die Änderung diese Schwankung übersteigt. Diese Werte sind bereits berechnet — nenne sie, rechne sie NICHT neu. `contributors[]` (nur Score-Komposite) benennt die 1–2 Teil-Scores, die den Score bewegt haben.", + "- summary: { points, start, end, delta, mean, min, max } — die Eckwerte des gesamten Lesefensters.", + "- series.recent[]: die jüngsten ~21 Tage, je ein Eintrag pro Tag ({ date, min, max, mean, n }).", + "- series.weekly[]: die ~10 Wochen davor, je ein ISO-Wochen-Aggregat.", + "- series.monthly[]: die ~12 Monate davor, je ein Monatsaggregat.", + "- series.yearly[]: alles ältere, je ein Jahr ({ year, mean, min, max, n, slope }).", + "- Die jüngsten Tage liegen also EINZELN vor, ältere Zeiträume als Wochen-, Monats- und Jahresaggregate. Ziehe die series nur für einen konkreten Tag heran; führe sonst mit dem signal-Block.", + "- Ein Slice kann leer sein, wenn die Historie kurz ist. Dann nicht so tun, als gäbe es ihn. Ist `signal.baseline` null, gibt es noch keine etablierte Baseline; sage das ehrlich, statt einen Vergleich zu erfinden.", + "", + "SO BAUST DU DIE EINSCHÄTZUNG — wähle die 2–3 BAUSTEINE, die zu DIESEN Daten passen, und ordne sie so, dass sie als EIN zusammenhängender Gedanke lesen, keine feste Checkliste. Führe gemäß dem OPENER-HINWEIS im User-Prompt, wenn einer mitgegeben ist — eröffne NICHT immer mit der Zahl. Variiere die Form von Metrik zu Metrik; eröffne nie zwei Metriken hintereinander gleich.", + "Verfügbare Bausteine (nimm die wenigen, die wirklich zutreffen):", + "(a) der aktuelle Befund — `current` aus dem signal-Block (oder ein Wert aus series.recent für eine Tagesfrage);", + "(b) gegen die EIGENE Baseline einordnen über das vorab berechnete vorzeichenbehaftete `delta` gegenüber `baselineLabel` — nenne es, leite es nicht neu her. Behandle `outsideNormalSwing: false` als \"innerhalb der üblichen Schwankung — kein Befund\" und ERFINDE KEINEN Trend; `outsideNormalSwing: true` ist die echte, meldenswerte Änderung. Ein `normalRange`/`placement` ist nur ein grober sekundärer Anker — das persönliche delta führt;", + "(c) einen echten, verdienten Erfolg oder eine stabile Serie benennen, wenn die Daten ihn zeigen;", + "(d) einen Punkt zum Im-Auge-behalten;", + "(e) einen Zusammenhang NUR, wenn ein r-Wert vorhanden und |r| > 0,4 ist, formuliert als \"bewegt sich mit\" / \"zeigt sich oft zusammen mit\", nie kausal;", + "(f) GENAU EINEN machbaren nächsten Schritt — und nur, wenn der Befund wirklich einen hergibt. Eine Botschaft = ein Verhalten, keine Liste. Ist der Wert stabil und im grünen Bereich, ERZWINGE KEINEN Schritt — bestätige kurz und nenne stattdessen einen Punkt zum Beobachten. Ein erfundener Schritt ist genau die Floskel, die wir vermeiden.", + "Synthese statt Aufzählung: die Geschichte dessen, was die Daten bedeuten, zählt mehr als das Wiederholen jeder Zahl.", + "", + "TONALITÄT — ein warmer, motivierender Premium-Begleiter (jemand hat sich die Daten dieser Person wirklich angesehen und etwas Wertvolles für sie gefunden):", + "- Zweite Person (\"dein Blutdruck\", \"deine Werte\"), warm, direkt, ehrlich.", + "- Motivierend und vorwärtsgewandt: Wenn die Daten es hergeben, benenne den echten Erfolg klar und baue ein wenig Schwung auf — die Person soll sich gesehen und unterstützt fühlen, nicht belehrt. Die Ermutigung muss durch die Zahlen VERDIENT sein, nie ein reflexhaftes Kompliment.", + "- Autonomie-unterstützend: \"kann helfen\", \"einen Versuch wert\", nie \"du musst\".", + "- Nie alarmierend, nie moralisierend, nie diagnostisch — keine Krankheitsbehauptung. Formuliere als begründete Beobachtung, nicht als ärztlichen Rat.", + "- Auch ungünstige Werte ehrlich benennen: Befund → gegen die eigene Baseline einordnen → ein kleiner machbarer Schritt, als Chance formuliert, nicht als Versagen. Nicht verharmlosen, nicht dramatisieren.", + "- Keine Floskeln und keine bloße Zahlenwiederholung: jede warme Zeile ist an eine echte Zahl oder eine echte Veränderung geknüpft. Ein generischer Positiv-Einstieg (\"Deine Werte sehen gut aus\") ist verboten — verdiene die Ermutigung mit dem konkreten Befund.", + "", + "ERÖFFNUNG — mit der Bedeutung führen, nicht mit dem Messwert", + "Beginne mit dem Eindruck in klaren Worten — was das heute über die Person sagt — und bring die Zahl direkt danach als Beleg, nie als Erstes. Eine Zeile, die mit einem Wert eröffnet (\"Dein Ruhepuls ist 61 …\"), liest sich wie ein Ablesen; dieselbe Zeile, die mit der Bedeutung eröffnet (\"Dein Herz läuft diese Woche etwas ruhiger — rund 61 …\"), liest sich wie ein Begleiter, der wirklich hingesehen hat. Jede Aussage bleibt an einer echten Zahl verankert; nur die REIHENFOLGE ändert sich, Bedeutung zuerst und die Zahl direkt dahinter. Das gilt, ob die Fläche eine Metrik abdeckt oder viele.", + "", + "LÄNGE: 2-4 Sätze, ca. 30-60 Wörter — ist der Wert aber stabil und nichts zu tun, ist EIN knapper Satz besser, als ihn auf Länge zu strecken. Knapp und hochwertig. Keine bloße Zahlenwiederholung, kein Fülltext, keine generischen Floskeln.", + "", + "DATENLAGE EHRLICH BEWERTEN (nie einen Trend erfinden):", + "- Nur wenige Messpunkte/Einträge/Ereignisse oder ein leerer recent-Slice: ehrlich sagen, dass es für eine belastbare Tendenz noch zu wenige Daten sind, und EINEN Hinweis geben (z.B. regelmäßiger erfassen). Keinen Trend behaupten. Was \"wenige\" bedeutet, hängt von der Metrik ab — bei Stimmung sind es Einträge, bei der Einnahmetreue geplante Dosen.", + "- Jüngste Messung deutlich älter als ~7 Tage (dataCoverage.newestMeasurementDaysAgo): darauf hinweisen, dass die Werte nicht mehr aktuell sind.", + "- Korrelationen NUR erwähnen, wenn der r-Wert im Snapshot vorhanden ist und |r| > 0.4 ist. Fehlt das Feld, keine Korrelation interpretieren oder erfinden. Immer als \"Zusammenhang\" formulieren, nie als \"Ursache\".", + "", + "VERBOTENE FLOSKELN (signalisieren ungegroundeten Fülltext — nie ausgeben, außer im Disclaimer)", + "\"achte auf ausreichend Schlaf\", \"trinke genug Wasser\", \"regelmäßige Bewegung\", \"ärztlicher Rat empfohlen\".", + "", + "METRIK-IDENTIFIER GEHÖREN NICHT IN DEN FLIESSTEXT", + "Schreibe niemals Datenbank- bzw. Enum-Namen wie \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" oder \"SLEEP_DURATION\" in nutzersichtbare Strings, und schreibe nie das wörtliche \"metric:\"-Chart-Token in den Fließtext. Verweise auf jede Metrik mit der natürlichsprachlichen Bezeichnung, die der Nutzer sieht — \"deine Systole\", \"dein Gewicht\", \"dein Puls\", \"deine Stimmung\", \"deine Medikamentenadhärenz\", \"dein Ruhepuls\", \"deine Schlafdauer\", \"deine Schritte\".", + "", + "GLP-1-DOSIS-SICHERHEIT (Vertrag, kein Stil)", + "Verschreibe, empfiehl oder ändere NIEMALS Medikamenten-Dosen, auch wenn der Snapshot einen GLP-1-Rezeptoragonisten benennt (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). Du darfst den benannten Wirkstoff und die aktuelle Titrationsstufe NENNEN (\"Woche 3 auf 7,5 mg\"), wenn der Snapshot sie trägt, aber schreibe nie \"erhöhe auf X mg\", \"erwäge die nächste Stufe Y mg\", \"bleibe auf Z mg\" oder eine Variante davon. Ein Plateau rahmt die nächste Entscheidung immer als Gespräch mit der behandelnden Ärztin. Bist du unsicher, ob etwas dosis-präskriptiv ist, behandle es so — und verweise an die Klinik.", + "", + "AKUTE WARNZEICHEN (Vertrag, kein Stil)", + "Beschreibt der Nutzer ein akutes Krisenzeichen — Brustschmerz oder Druck auf der Brust, Ohnmacht oder Beinahe-Ohnmacht (Synkope), ein plötzliches schweres Symptom (z. B. stärkster Kopfschmerz aller Zeiten, plötzliche Schwäche oder Sprachstörung, Atemnot), einen hypertensiven Notfallwert (systolisch ≥ 180 oder diastolisch ≥ 120 mit Symptomen), einen schwer-niedrigen oder sehr hohen Glukosewert (unter 54 mg/dL (3,0 mmol/L), oder unter 70 mg/dL (3,9 mmol/L) mit Symptomen, oder ≥ 250 mg/dL (13,9 mmol/L) mit Symptomen), anhaltendes Fieber ≥ 38,5 °C (101,3 °F) oder Gedanken an Suizid bzw. Selbstverletzung — sage in einem ruhigen Satz klar, dass das jetzt umgehende ärztliche Hilfe oder den Notruf braucht, und coache es NICHT anhand der Daten. Diagnostiziere nicht, benenne keine Erkrankung, schätze keinen Schweregrad aus den Zahlen — verweise nur auf umgehende Hilfe bzw. den Notruf und höre auf. Das ist eine geschlossene Liste; außerhalb davon bleibe wie gewohnt ruhig und nicht alarmierend.", + "", + "ERDUNG — NULL HALLUZINATIONEN", + "Jede Aussage muss auf einer Zahl beruhen, die im übergebenen Snapshot sichtbar ist. Lässt sich die Aussage keinem Snapshot-Feld zuordnen, lass sie weg und erfinde nie einen Messwert, um einer Anfrage zu entsprechen. Ordne jeden Wert gegen die EIGENE Baseline des Nutzers ein (jüngeres vs. längeres Mittel), nie gegen eine Bevölkerungsnorm. Nenne einen Zusammenhang nur, wenn der r-Wert vorhanden und |r| > 0,4 ist — immer als \"Zusammenhang\", nie als \"Ursache\".", + "", + "TONALITÄT — ein warmer, motivierender Begleiter, nie klinisch oder alarmierend", + "Schreibe in der zweiten Person, warm und direkt. Wenn die Daten es hergeben, benenne den echten Erfolg klar und baue ein wenig Schwung auf — die Person soll sich gesehen und unterstützt fühlen, nicht belehrt. Die Ermutigung muss durch die Zahlen VERDIENT sein, nie ein reflexhaftes Kompliment. Lob ist verankert, konkret und angemessen: an einer echten Zahl oder Veränderung festgemacht, nicht auf andere Nutzer übertragbar, und stiller Anerkennung für stille Erfolge. Über-Bestätigung ist eine Sicherheitsregression, keine Wärme — bestätige nie einen sich verschlechternden Trend oder eine unsichere Entscheidung; würdige die Mühe, nicht die Entscheidung, und bleib neutral (\"nichts zu tun — die gute Art von langweilig\"), wenn es nichts zu loben gibt, statt ein Kompliment zu erfinden. Sei autonomie-unterstützend (\"einen Versuch wert\", \"kann helfen\", nie \"du musst\"). Benenne auch ungünstige Werte ehrlich — Befund, dann gegen die eigene Baseline einordnen, dann ein kleiner machbarer Schritt, als Chance formuliert. Nie alarmierend, nie moralisierend, nie diagnostisch. Keine Floskeln und keine bloße Zahlenwiederholung. Beginne NICHT mit einem Kompliment über Datenmenge oder -qualität; verbotene Eröffnungen sind u.a. \"Datengrundlage ist sehr stark\", \"Your data foundation is strong\", \"Du hast eine solide Baseline\", \"Großartiger Datensatz\", ein generisches \"Deine Werte sehen gut aus\" und jede sinngemäße Umformulierung.", + "", + "KEINE EINSCHÄTZUNG — nie Mechanik nacherzählen", + "Eine Messanzahl (\"3-mal erfasst\"), ein Erfassungsrhythmus (\"alle paar Tage gemessen\") oder Bedien-Mechanik ist KEINE Einschätzung und gehört für sich genommen nicht in den Text. Nutze eine solche Angabe NUR, wenn sie eine Konsequenz trägt, und nenne dann die Konsequenz, nicht die Zahl: z. B. \"diese Woche zu wenige Werte für eine Tendenz — eine Morgenmessung würde es klären\", nicht \"du hast diese Woche zweimal gemessen\". Jeder Satz muss sich verdienen, indem er sagt, was ein Wert BEDEUTET oder was daraus folgt — nie, indem er erzählt, wie die Daten erhoben wurden.", + "", + "EINORDNEN, NICHT AUFZÄHLEN", + "Sag, was ein Wert BEDEUTET, nicht nur, was er ist. Trägt der User-Prompt einen EINORDNUNGS-KONTEXT-Block, nutze ihn: benenne, wo der Wert auf der Leitlinien-Skala liegt, was dieses Band in klaren, würdevollen Worten bedeutet und was praktisch folgt. Beurteile den Trend IM KONTEXT — ein \"leicht steigender\" Wert tief in einem günstigen Band ist eine Randnotiz; dieselbe Bewegung an oder nahe einer Grenze ist die Schlagzeile. Rahme jede Konsequenz OHNE Diagnose: \"Werte in diesem Bereich gelten laut Leitlinien als …\", nie \"du hast X\" oder \"du bist gefährdet für X\". Ist KEIN Einordnungs-Block vorhanden, hat die Metrik kein allgemeines Referenzband — ordne sie gegen die EIGENE Baseline der Person ein und sage klar, dass kein allgemeines Referenzband hinterlegt ist, statt eines zu erfinden.", + "", + "FORMATIERUNG — schreibe in kurzen Absätzen, getrennt durch eine LEERZEILE, je 1–3 Sätze; eine längere Antwort hat 2–4 Absätze, nie einen Block. Setze eine Leerzeile zwischen unterschiedliche Gedanken (z. B. \"wie es steht\" vs. \"eine Sache zum Ausprobieren\"). Ein stabiler Einzeiler bleibt EIN Absatz — strecke nicht auf einen zweiten. Wenn du wirklich drei oder mehr parallele Punkte aufzählst (Optionen, Befunde, Schritte), formatiere sie als schlichte Liste: ein Punkt pro Zeile, jede Zeile beginnt mit \"- \". Die EINE wichtigste Kernaussage darfst du mit **doppelten Sternchen** fett setzen, höchstens einmal pro Antwort. Sonst nichts: keine Überschriften, kein Kursiv, keine Backticks, keine nummerierten Listen, keine Emojis.", + "", + "BEISPIELE — beachte die UNTERSCHIEDLICHEN Formen (urteil-zuerst, trend-zuerst, Einzeiler). Sie illustrieren nur Form und Erdung; übernimm sie nie — jede Einschätzung nutzt die echten Snapshot-Zahlen:", + "- URTEIL-ZUERST (Bedeutung zuerst, Zahl als Beleg, ein Schritt): \"Dein Ruhepuls läuft gerade einen Tick niedriger als sonst — diese Woche rund 61 bpm, 5 unter deinem Monatsmittel von 66 und dein niedrigster seit Wochen. So ein Rückgang passt meist zu mehr Bewegung; die Routine, die das gebracht hat, lohnt sich beizubehalten.\"", + "- TREND-ZUERST (Richtung zuerst, dann der Stand): \"Dein systolischer Blutdruck geht seit drei Wochen ruhig nach unten, aktuell rund 124 mmHg — etwa 4 unter deinem 30-Tage-Schnitt. Nichts Dramatisches, einfach eine stetige Richtung.\"", + "- EINZEILER (stabil, KEIN erzwungener Schritt): \"Deine SpO₂ ist mit 97 % stabil und genau in deinem üblichen Bereich — nichts zu tun, die gute Art von langweilig.\"", + "- SCHLECHT (verbotene Floskel, ungegroundet — so NICHT): \"Deine Werte sehen gut aus. Achte auf ausreichend Schlaf und regelmäßige Bewegung.\"", + "", + "AUSGABEFORMAT: Antworte ausschließlich mit validem JSON in genau diesem Schema. Das Feld \"summary\" enthält die komplette Einschätzung auf Deutsch: 1-3 kurze Absätze mit je 1-3 Sätzen, getrennt durch eine Leerzeile als \\n\\n INNERHALB des JSON-Strings. Ein stabiler Einzeiler bleibt EIN Absatz:", + "{ \"summary\": \"...\" }", + "", + "GEBÜNDELTE AUSGABE — du bewertest in einem Durchgang MEHRERE Metriken dieses Nutzers. Jede Metrik unten trägt ihren EIGENEN graded snapshot unter einer Überschrift \"### \"; bewerte jede ausschließlich aus ihrem eigenen Snapshot-Block und wende alle obigen Regeln pro Metrik an. Lass den Befund einer Metrik NICHT in eine andere überlaufen. Erfinde KEINE Metrik: bewerte NUR die unten vorhandenen Keys.", + "", + "AUSGABEFORMAT: Antworte ausschließlich mit validem JSON in genau diesem Schema. \"perMetric\" enthält je vorhandenem Key eine kurze Einschätzung (je 2-4 Sätze, derselbe Vertrag wie eine Einzelkarte):", + "{ \"perMetric\": { \"bp\": \"...\", \"pulse\": \"...\", \"weight\": \"...\", \"bmi\": \"...\", \"mood\": \"...\", \"compliance\": \"...\", \"general\": \"...\" } }", + "Nimm einen Key NUR auf, wenn sein Snapshot-Block unten vorhanden ist (vorhandene Keys: bp, pulse, weight, bmi, mood, compliance, general). Lass jede Metrik ohne Block weg — erfinde niemals eine." + ], + "messages": [ + { + "role": "user", + "content": [ + "### bp", + "", + "Datum: 2026-06-15", + "OPENER HINT: Eröffne mit dem Gesamteindruck in klaren Worten und bring die Zahl danach als Beleg — nicht zahlen-zuerst.", + "Schreibe eine kurze Einschätzung zum Blutdruck dieser Person. Beginne mit der BEDEUTUNG in klaren Worten — dem Gesamteindruck, nicht der Zahl (z. B. \"ruhig und genau da, wo du ihn haben willst\", \"diese Woche einen Tick höher als sonst\") — und bring danach die systolischen/diastolischen Werte als Beleg, als EIN Paar beurteilt und gegen die eigene Wochen-/Monats-Baseline eingeordnet; führe nie mit den Werten. Schließe nur dann mit einem machbaren Schritt, wenn der Befund wirklich einen hergibt; ist nichts umsetzbar, lass den Schritt weg statt Fülltext zu erfinden. Konfidenz aus Messanzahl und Aktualität ableiten.", + "", + "VORHERIGE ANALYSE: keine vorhanden. Behandle dies als erste Analyse in diesem Bereich — kein Verbesserungs-/Verschlechterungs-Delta zu nennen.", + "", + "", + "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit dem aktuellsten Einzelwert und seiner Einordnung eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", + "", + "DATENLAGE: 94 Tages-Buckets im Fenster, letzter Wert heute.", + "", + "WIEDERHOLUNG: du hast für diese Metrik bereits 11-mal in Folge eine ähnliche \"keine wesentliche Änderung\"-Einschätzung gegeben. Wiederhole NICHT erneut dasselbe Niveau. Benenne die Kontinuität in einem kurzen Nebensatz (\"weiterhin stabil, 12 Checks in Folge\") und wende dich dann einem ANDEREN Aspekt zu — Konstanz, Tageszeit-Muster oder einer korrelierten Metrik — oder sage, falls nichts wirklich umsetzbar ist, dies klar und lass den erzwungenen Schritt weg.", + "", + "ZUSAMMENHÄNGE (statistisch geprüfte Assoziationen in DEINEN Daten, die diese Metrik betreffen — beschreibend, NIE kausal):", + "- Wenn der Blutzucker höher liegt, scheint der systolische Blutdruck am Folgetag nach bisheriger Datenlage eher höher auszufallen. Das ist ein Muster, das Beachtung verdient, keine Ursache. (n=23, r=0.91)", + "- Wenn der systolische Blutdruck höher liegt, scheint das Gewicht am Folgetag nach bisheriger Datenlage eher höher auszufallen. Das ist ein Muster, das Beachtung verdient, keine Ursache. (n=47, r=0.88)", + "Du DARFST höchstens EINEN davon einflechten, wenn er echten Mehrwert bringt — als \"Zusammenhang\" / \"geht tendenziell einher mit\" formuliert, nie als Ursache, und ohne die Zahlen zu verändern. Lass sie weg, wenn der eigene Befund der Metrik die stärkere Geschichte ist.", + "", + "", + "{\"locale\":\"de\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"blood_pressure\",\"dataCoverage\":{\"totalMeasurements\":258,\"totalSpanDays\":139,\"newestMeasurementDaysAgo\":0},\"bloodPressure\":{\"systolic\":{\"signal\":{\"metric\":\"deine Systole\",\"unit\":\"mmHg\",\"current\":127.29,\"currentWindowDays\":14,\"baseline\":126.97,\"baselineLabel\":\"your monthly average\",\"delta\":0.32,\"deltaPct\":0.3,\"spread\":2.38,\"outsideNormalSwing\":false,\"direction\":\"lower-better\",\"n\":14,\"newestDaysAgo\":0,\"normalRange\":{\"low\":120,\"high\":129},\"placement\":\"in band\"},\"summary\":{\"points\":94,\"start\":128,\"end\":125,\"delta\":-3,\"mean\":125.39,\"min\":116,\"max\":134},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":119,\"max\":119,\"mean\":119,\"n\":1},{\"date\":\"2026-05-28\",\"min\":121,\"max\":121,\"mean\":121,\"n\":1},{\"date\":\"2026-05-30\",\"min\":126,\"max\":126,\"mean\":126,\"n\":1},{\"date\":\"2026-05-31\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-02\",\"min\":125,\"max\":125,\"mean\":125,\"n\":1},{\"date\":\"2026-06-03\",\"min\":127,\"max\":127,\"mean\":127,\"n\":1},{\"date\":\"2026-06-05\",\"min\":130,\"max\":130,\"mean\":130,\"n\":1},{\"date\":\"2026-06-06\",\"min\":132,\"max\":132,\"mean\":132,\"n\":1},{\"date\":\"2026-06-08\",\"min\":134,\"max\":134,\"mean\":134,\"n\":1},{\"date\":\"2026-06-09\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-11\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-12\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-14\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-15\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":117,\"max\":121,\"mean\":118.75,\"n\":4},{\"weekISO\":\"2026-W13\",\"min\":119,\"max\":128,\"mean\":123.2,\"n\":5},{\"weekISO\":\"2026-W14\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"weekISO\":\"2026-W15\",\"min\":128,\"max\":134,\"mean\":129.4,\"n\":5},{\"weekISO\":\"2026-W16\",\"min\":121,\"max\":128,\"mean\":122.4,\"n\":5},{\"weekISO\":\"2026-W17\",\"min\":116,\"max\":122,\"mean\":118.75,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":121,\"max\":131,\"mean\":126,\"n\":5},{\"weekISO\":\"2026-W19\",\"min\":129,\"max\":133,\"mean\":131,\"n\":5},{\"weekISO\":\"2026-W20\",\"min\":124,\"max\":125,\"mean\":124.75,\"n\":4},{\"weekISO\":\"2026-W21\",\"min\":117,\"max\":124,\"mean\":119.8,\"n\":5},{\"weekISO\":\"2026-W22\",\"min\":123,\"max\":123,\"mean\":123,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"month\":\"2026-02\",\"min\":116,\"max\":134,\"mean\":124.22,\"n\":18},{\"month\":\"2026-03\",\"min\":124,\"max\":133,\"mean\":128.18,\"n\":11}],\"yearly\":[]}},\"diastolic\":{\"signal\":{\"metric\":\"deine Diastole\",\"unit\":\"mmHg\",\"current\":81.71,\"currentWindowDays\":14,\"baseline\":81.43,\"baselineLabel\":\"your monthly average\",\"delta\":0.29,\"deltaPct\":0.4,\"spread\":1.93,\"outsideNormalSwing\":false,\"direction\":\"lower-better\",\"n\":14,\"newestDaysAgo\":0,\"normalRange\":{\"low\":70,\"high\":79},\"placement\":\"above band\"},\"summary\":{\"points\":94,\"start\":82,\"end\":80,\"delta\":-2,\"mean\":80.31,\"min\":74,\"max\":86},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":76,\"max\":76,\"mean\":76,\"n\":1},{\"date\":\"2026-05-28\",\"min\":77,\"max\":77,\"mean\":77,\"n\":1},{\"date\":\"2026-05-30\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-05-31\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-02\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-03\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-05\",\"min\":86,\"max\":86,\"mean\":86,\"n\":1},{\"date\":\"2026-06-06\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-08\",\"min\":84,\"max\":84,\"mean\":84,\"n\":1},{\"date\":\"2026-06-09\",\"min\":85,\"max\":85,\"mean\":85,\"n\":1},{\"date\":\"2026-06-11\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-12\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-14\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-15\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":75,\"max\":79,\"mean\":77,\"n\":4},{\"weekISO\":\"2026-W13\",\"min\":74,\"max\":79,\"mean\":77.4,\"n\":5},{\"weekISO\":\"2026-W14\",\"min\":83,\"max\":85,\"mean\":83.75,\"n\":4},{\"weekISO\":\"2026-W15\",\"min\":79,\"max\":86,\"mean\":82.6,\"n\":5},{\"weekISO\":\"2026-W16\",\"min\":76,\"max\":80,\"mean\":78.2,\"n\":5},{\"weekISO\":\"2026-W17\",\"min\":74,\"max\":78,\"mean\":76.75,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":77,\"max\":84,\"mean\":80.6,\"n\":5},{\"weekISO\":\"2026-W19\",\"min\":82,\"max\":86,\"mean\":83.8,\"n\":5},{\"weekISO\":\"2026-W20\",\"min\":80,\"max\":81,\"mean\":80.5,\"n\":4},{\"weekISO\":\"2026-W21\",\"min\":74,\"max\":78,\"mean\":76.2,\"n\":5},{\"weekISO\":\"2026-W22\",\"min\":78,\"max\":78,\"mean\":78,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":80,\"max\":86,\"mean\":83,\"n\":4},{\"month\":\"2026-02\",\"min\":75,\"max\":85,\"mean\":79.28,\"n\":18},{\"month\":\"2026-03\",\"min\":78,\"max\":86,\"mean\":82,\"n\":11}],\"yearly\":[]}},\"paired\":{\"summary\":{\"points\":94,\"start\":105,\"end\":102.5,\"delta\":-2.5,\"mean\":102.85,\"min\":95.5,\"max\":110},\"series\":[{\"day\":\"2026-03-11\",\"sys\":125,\"dia\":82,\"inTarget\":false},{\"day\":\"2026-03-10\",\"sys\":125,\"dia\":82,\"inTarget\":false},{\"day\":\"2026-03-08\",\"sys\":132,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-03-07\",\"sys\":131,\"dia\":86,\"inTarget\":false},{\"day\":\"2026-03-05\",\"sys\":130,\"dia\":84,\"inTarget\":false},{\"day\":\"2026-03-04\",\"sys\":129,\"dia\":83,\"inTarget\":false},{\"day\":\"2026-03-02\",\"sys\":133,\"dia\":85,\"inTarget\":false},{\"day\":\"2026-03-01\",\"sys\":131,\"dia\":84,\"inTarget\":false},{\"day\":\"2026-02-27\",\"sys\":127,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-02-26\",\"sys\":125,\"dia\":79,\"inTarget\":true},{\"day\":\"2026-02-24\",\"sys\":121,\"dia\":80,\"inTarget\":false},{\"day\":\"2026-02-23\",\"sys\":126,\"dia\":78,\"inTarget\":true},{\"day\":\"2026-02-21\",\"sys\":122,\"dia\":75,\"inTarget\":true},{\"day\":\"2026-02-20\",\"sys\":120,\"dia\":79,\"inTarget\":true},{\"day\":\"2026-02-18\",\"sys\":117,\"dia\":76,\"inTarget\":true},{\"day\":\"2026-02-17\",\"sys\":116,\"dia\":75,\"inTarget\":true},{\"day\":\"2026-02-15\",\"sys\":122,\"dia\":78,\"inTarget\":true},{\"day\":\"2026-02-14\",\"sys\":121,\"dia\":78,\"inTarget\":true},{\"day\":\"2026-02-12\",\"sys\":121,\"dia\":77,\"inTarget\":true},{\"day\":\"2026-02-11\",\"sys\":121,\"dia\":77,\"inTarget\":true},{\"day\":\"2026-02-09\",\"sys\":128,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-02-08\",\"sys\":129,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-02-06\",\"sys\":129,\"dia\":80,\"inTarget\":false},{\"day\":\"2026-02-05\",\"sys\":129,\"dia\":85,\"inTarget\":false},{\"day\":\"2026-02-03\",\"sys\":128,\"dia\":84,\"inTarget\":false},{\"day\":\"2026-02-02\",\"sys\":134,\"dia\":83,\"inTarget\":false},{\"day\":\"2026-01-31\",\"sys\":132,\"dia\":86,\"inTarget\":false},{\"day\":\"2026-01-30\",\"sys\":130,\"dia\":85,\"inTarget\":false},{\"day\":\"2026-01-28\",\"sys\":127,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-01-27\",\"sys\":125,\"dia\":80,\"inTarget\":false}]},\"targets\":{\"systolic\":{\"min\":120,\"max\":129},\"diastolic\":{\"min\":70,\"max\":79},\"inTargetPctLast30DailyPoints\":42.6}},\"weightVsSystolic\":{\"correlation\":{\"r\":0.385,\"strength\":\"schwach\",\"n\":47},\"pairs\":[{\"day\":\"2026-04-24\",\"weight\":83.09,\"systolic\":120},{\"day\":\"2026-04-22\",\"weight\":83,\"systolic\":117},{\"day\":\"2026-04-18\",\"weight\":83,\"systolic\":121},{\"day\":\"2026-04-16\",\"weight\":83.09,\"systolic\":121},{\"day\":\"2026-04-12\",\"weight\":83.33,\"systolic\":128},{\"day\":\"2026-04-10\",\"weight\":83.45,\"systolic\":129},{\"day\":\"2026-04-06\",\"weight\":83.55,\"systolic\":134},{\"day\":\"2026-04-04\",\"weight\":83.51,\"systolic\":132},{\"day\":\"2026-03-31\",\"weight\":83.25,\"systolic\":125},{\"day\":\"2026-03-29\",\"weight\":83.07,\"systolic\":128},{\"day\":\"2026-03-25\",\"weight\":82.71,\"systolic\":119},{\"day\":\"2026-03-23\",\"weight\":82.58,\"systolic\":123},{\"day\":\"2026-03-19\",\"weight\":82.5,\"systolic\":118},{\"day\":\"2026-03-17\",\"weight\":82.55,\"systolic\":117},{\"day\":\"2026-03-13\",\"weight\":82.77,\"systolic\":125},{\"day\":\"2026-03-11\",\"weight\":82.9,\"systolic\":125},{\"day\":\"2026-03-07\",\"weight\":83.07,\"systolic\":131},{\"day\":\"2026-03-05\",\"weight\":83.07,\"systolic\":130},{\"day\":\"2026-03-01\",\"weight\":82.89,\"systolic\":131},{\"day\":\"2026-02-27\",\"weight\":82.73,\"systolic\":127},{\"day\":\"2026-02-23\",\"weight\":82.36,\"systolic\":126},{\"day\":\"2026-02-21\",\"weight\":82.2,\"systolic\":122},{\"day\":\"2026-02-17\",\"weight\":82.03,\"systolic\":116},{\"day\":\"2026-02-15\",\"weight\":82.04,\"systolic\":122},{\"day\":\"2026-02-11\",\"weight\":82.21,\"systolic\":121},{\"day\":\"2026-02-09\",\"weight\":82.34,\"systolic\":128},{\"day\":\"2026-02-05\",\"weight\":82.56,\"systolic\":129},{\"day\":\"2026-02-03\",\"weight\":82.6,\"systolic\":128},{\"day\":\"2026-01-30\",\"weight\":82.51,\"systolic\":130},{\"day\":\"2026-01-28\",\"weight\":82.38,\"systolic\":127}]},\"bpMedicationContinuityVsSystolic\":{\"expectedIntakesPerDay\":1,\"medicationCount\":1,\"correlation\":null,\"series\":[{\"day\":\"2026-03-11\",\"dayOffset\":96,\"sys\":125,\"continuityPct\":100},{\"day\":\"2026-03-10\",\"dayOffset\":97,\"sys\":125,\"continuityPct\":100},{\"day\":\"2026-03-08\",\"dayOffset\":99,\"sys\":132,\"continuityPct\":100},{\"day\":\"2026-03-07\",\"dayOffset\":100,\"sys\":131,\"continuityPct\":0},{\"day\":\"2026-03-05\",\"dayOffset\":102,\"sys\":130,\"continuityPct\":0},{\"day\":\"2026-03-04\",\"dayOffset\":103,\"sys\":129,\"continuityPct\":0},{\"day\":\"2026-03-02\",\"dayOffset\":105,\"sys\":133,\"continuityPct\":0},{\"day\":\"2026-03-01\",\"dayOffset\":106,\"sys\":131,\"continuityPct\":0},{\"day\":\"2026-02-27\",\"dayOffset\":108,\"sys\":127,\"continuityPct\":0},{\"day\":\"2026-02-26\",\"dayOffset\":109,\"sys\":125,\"continuityPct\":0},{\"day\":\"2026-02-24\",\"dayOffset\":111,\"sys\":121,\"continuityPct\":0},{\"day\":\"2026-02-23\",\"dayOffset\":112,\"sys\":126,\"continuityPct\":0},{\"day\":\"2026-02-21\",\"dayOffset\":114,\"sys\":122,\"continuityPct\":0},{\"day\":\"2026-02-20\",\"dayOffset\":115,\"sys\":120,\"continuityPct\":0},{\"day\":\"2026-02-18\",\"dayOffset\":117,\"sys\":117,\"continuityPct\":0},{\"day\":\"2026-02-17\",\"dayOffset\":118,\"sys\":116,\"continuityPct\":0},{\"day\":\"2026-02-15\",\"dayOffset\":120,\"sys\":122,\"continuityPct\":0},{\"day\":\"2026-02-14\",\"dayOffset\":121,\"sys\":121,\"continuityPct\":0},{\"day\":\"2026-02-12\",\"dayOffset\":123,\"sys\":121,\"continuityPct\":0},{\"day\":\"2026-02-11\",\"dayOffset\":124,\"sys\":121,\"continuityPct\":0},{\"day\":\"2026-02-09\",\"dayOffset\":126,\"sys\":128,\"continuityPct\":0},{\"day\":\"2026-02-08\",\"dayOffset\":127,\"sys\":129,\"continuityPct\":0},{\"day\":\"2026-02-06\",\"dayOffset\":129,\"sys\":129,\"continuityPct\":0},{\"day\":\"2026-02-05\",\"dayOffset\":130,\"sys\":129,\"continuityPct\":0},{\"day\":\"2026-02-03\",\"dayOffset\":132,\"sys\":128,\"continuityPct\":0},{\"day\":\"2026-02-02\",\"dayOffset\":133,\"sys\":134,\"continuityPct\":0},{\"day\":\"2026-01-31\",\"dayOffset\":135,\"sys\":132,\"continuityPct\":0},{\"day\":\"2026-01-30\",\"dayOffset\":136,\"sys\":130,\"continuityPct\":0},{\"day\":\"2026-01-28\",\"dayOffset\":138,\"sys\":127,\"continuityPct\":0},{\"day\":\"2026-01-27\",\"dayOffset\":139,\"sys\":125,\"continuityPct\":0}]},\"bpMedications\":[{\"name\":\"Ramipril\",\"dose\":\"5mg\",\"schedulesPerDay\":1,\"compliance7\":67,\"compliance30\":79}],\"moodContext\":{\"points\":70,\"mean\":3.03,\"latest\":1,\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-15\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W15\",\"min\":2,\"max\":5,\"mean\":3.33,\"n\":6},{\"weekISO\":\"2026-W16\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":1,\"max\":5,\"mean\":3,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":1,\"max\":4,\"mean\":2.57,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1}],\"monthly\":[],\"yearly\":[]},\"moodVsSystolicCorrelation\":null}}", + "", + "---", + "", + "### pulse", + "", + "Datum: 2026-06-15", + "OPENER HINT: Eröffne mit der Anerkennung von Konstanz oder einer Serie und dann dem aktuellen Wert.", + "Schreibe eine kurze Einschätzung zum Ruhepuls dieser Person. Beginne mit der BEDEUTUNG in klaren Worten — dem Eindruck, nicht der Zahl (z. B. \"läuft gerade etwas ruhiger als sonst\", \"stabil, genau da, wo er war\") — und bring danach EINE konkrete Zahl aus dem Snapshot als Beleg, gegen die eigene Wochen-/Monats-Baseline eingeordnet; führe nie mit dem Wert. Schließe nur dann mit einem machbaren Schritt, wenn der Befund wirklich einen hergibt; ist nichts umsetzbar, lass den Schritt weg statt Fülltext zu erfinden. Konfidenz aus Messanzahl und Aktualität ableiten.", + "", + "VORHERIGE ANALYSE: keine vorhanden. Behandle dies als erste Analyse in diesem Bereich — kein Verbesserungs-/Verschlechterungs-Delta zu nennen.", + "", + "", + "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der jüngsten Richtung (steigend / fallend / flach gegenüber der Baseline) eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", + "", + "DATENLAGE: 122 Tages-Buckets im Fenster, letzter Wert heute.", + "", + "", + "{\"locale\":\"de\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"pulse\",\"dataCoverage\":{\"totalMeasurements\":365,\"totalSpanDays\":121,\"newestMeasurementDaysAgo\":0},\"pulse\":{\"signal\":{\"metric\":\"dein Ruhepuls\",\"unit\":\"bpm\",\"current\":71.64,\"currentWindowDays\":22,\"baseline\":71.88,\"baselineLabel\":\"your monthly average\",\"delta\":-0.24,\"deltaPct\":-0.3,\"spread\":2.21,\"outsideNormalSwing\":false,\"direction\":\"lower-better\",\"n\":63,\"newestDaysAgo\":0,\"normalRange\":{\"low\":61,\"high\":77},\"placement\":\"in band\"},\"summary\":{\"points\":122,\"start\":70,\"end\":68,\"delta\":-2,\"mean\":70.13,\"min\":65,\"max\":75},\"series\":{\"recent\":[{\"date\":\"2026-05-25\",\"min\":62,\"max\":74,\"mean\":68,\"n\":2},{\"date\":\"2026-05-26\",\"min\":62,\"max\":74,\"mean\":66,\"n\":3},{\"date\":\"2026-05-27\",\"min\":63,\"max\":75,\"mean\":67,\"n\":3},{\"date\":\"2026-05-28\",\"min\":64,\"max\":76,\"mean\":68,\"n\":3},{\"date\":\"2026-05-29\",\"min\":65,\"max\":77,\"mean\":69,\"n\":3},{\"date\":\"2026-05-30\",\"min\":66,\"max\":78,\"mean\":70,\"n\":3},{\"date\":\"2026-05-31\",\"min\":67,\"max\":79,\"mean\":71,\"n\":3},{\"date\":\"2026-06-01\",\"min\":68,\"max\":80,\"mean\":72,\"n\":3},{\"date\":\"2026-06-02\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-03\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-04\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-05\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-06\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-07\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-08\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-09\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-10\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-11\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-12\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-13\",\"min\":68,\"max\":80,\"mean\":72,\"n\":3},{\"date\":\"2026-06-14\",\"min\":67,\"max\":79,\"mean\":71,\"n\":3},{\"date\":\"2026-06-15\",\"min\":66,\"max\":66,\"mean\":66,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":61,\"max\":75,\"mean\":65.86,\"n\":20},{\"weekISO\":\"2026-W13\",\"min\":62,\"max\":79,\"mean\":68.14,\"n\":21},{\"weekISO\":\"2026-W14\",\"min\":68,\"max\":83,\"mean\":73.57,\"n\":21},{\"weekISO\":\"2026-W15\",\"min\":67,\"max\":83,\"mean\":73.43,\"n\":21},{\"weekISO\":\"2026-W16\",\"min\":61,\"max\":78,\"mean\":67.43,\"n\":21},{\"weekISO\":\"2026-W17\",\"min\":61,\"max\":75,\"mean\":65.71,\"n\":21},{\"weekISO\":\"2026-W18\",\"min\":64,\"max\":82,\"mean\":71,\"n\":21},{\"weekISO\":\"2026-W19\",\"min\":70,\"max\":83,\"mean\":74.57,\"n\":21},{\"weekISO\":\"2026-W20\",\"min\":64,\"max\":81,\"mean\":70.71,\"n\":21},{\"weekISO\":\"2026-W21\",\"min\":61,\"max\":75,\"mean\":65.57,\"n\":21},{\"weekISO\":\"2026-W22\",\"min\":62,\"max\":62,\"mean\":62,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":68,\"max\":83,\"mean\":73.6,\"n\":15},{\"month\":\"2026-02\",\"min\":61,\"max\":83,\"mean\":69.39,\"n\":84},{\"month\":\"2026-03\",\"min\":63,\"max\":83,\"mean\":72.65,\"n\":46}],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":0,\"value\":70,\"deltaToPreviousDailyPoint\":-1},\"target\":{\"greenMin\":61,\"greenMax\":77,\"orangeMin\":55,\"orangeMax\":85,\"inTargetPctLast30DailyPoints\":40}},\"moodContext\":{\"points\":70,\"mean\":3.03,\"latest\":1,\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W15\",\"min\":2,\"max\":5,\"mean\":3.33,\"n\":6},{\"weekISO\":\"2026-W16\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":1,\"max\":5,\"mean\":3,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":1,\"max\":4,\"mean\":2.57,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1}],\"monthly\":[],\"yearly\":[]}}}", + "", + "---", + "", + "### weight", + "", + "Datum: 2026-06-15", + "OPENER HINT: Eröffne mit der Anerkennung von Konstanz oder einer Serie und dann dem aktuellen Wert.", + "Schreibe eine kurze Einschätzung zum Gewicht dieser Person. Beginne mit der BEDEUTUNG in klaren Worten — Richtung und Tempo, nicht der Zahl (z. B. \"geht ruhig nach unten\", \"hält sich genau da, wo es sich eingependelt hat\") — und bring danach EINE konkrete Zahl aus dem Snapshot als Beleg, als kontinuierlichen Trend und Tempo gegen die eigene Wochen-/Monats-Baseline gelesen (Tempo pro Woche, Plateau, Meilenstein — nicht der Einzelwert und nicht das WHO-Band, das die BMI-Karte trägt); führe nie mit dem Wert. Schließe nur dann mit einem machbaren Schritt, wenn der Befund wirklich einen hergibt; ist nichts umsetzbar, lass den Schritt weg statt Fülltext zu erfinden. Konfidenz aus Messanzahl und Aktualität ableiten.", + "", + "VORHERIGE ANALYSE: keine vorhanden. Behandle dies als erste Analyse in diesem Bereich — kein Verbesserungs-/Verschlechterungs-Delta zu nennen.", + "", + "", + "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der Konstanz bzw. Streuung der Werte eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", + "", + "DATENLAGE: 70 Tages-Buckets im Fenster, letzter Wert heute.", + "", + "WIEDERHOLUNG: du hast für diese Metrik bereits 10-mal in Folge eine ähnliche \"keine wesentliche Änderung\"-Einschätzung gegeben. Wiederhole NICHT erneut dasselbe Niveau. Benenne die Kontinuität in einem kurzen Nebensatz (\"weiterhin stabil, 11 Checks in Folge\") und wende dich dann einem ANDEREN Aspekt zu — Konstanz, Tageszeit-Muster oder einer korrelierten Metrik — oder sage, falls nichts wirklich umsetzbar ist, dies klar und lass den erzwungenen Schritt weg.", + "", + "ZUSAMMENHÄNGE (statistisch geprüfte Assoziationen in DEINEN Daten, die diese Metrik betreffen — beschreibend, NIE kausal):", + "- Wenn der systolische Blutdruck höher liegt, scheint das Gewicht am Folgetag nach bisheriger Datenlage eher höher auszufallen. Das ist ein Muster, das Beachtung verdient, keine Ursache. (n=47, r=0.88)", + "- Wenn die Schlafdauer höher liegt, scheint das Gewicht am Folgetag nach bisheriger Datenlage eher höher auszufallen. Das ist ein Muster, das Beachtung verdient, keine Ursache. (n=45, r=0.98)", + "Du DARFST höchstens EINEN davon einflechten, wenn er echten Mehrwert bringt — als \"Zusammenhang\" / \"geht tendenziell einher mit\" formuliert, nie als Ursache, und ohne die Zahlen zu verändern. Lass sie weg, wenn der eigene Befund der Metrik die stärkere Geschichte ist.", + "", + "", + "{\"locale\":\"de\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"weight\",\"dataCoverage\":{\"totalMeasurements\":258,\"totalSpanDays\":139,\"newestMeasurementDaysAgo\":0},\"weight\":{\"unit\":\"lb\",\"signal\":{\"metric\":\"dein Gewicht\",\"unit\":\"lb\",\"current\":185.58,\"currentWindowDays\":11,\"baseline\":182.07,\"baselineLabel\":\"your monthly average\",\"delta\":3.52,\"deltaPct\":1.9,\"spread\":0.64,\"outsideNormalSwing\":true,\"direction\":\"target-band\",\"n\":11,\"newestDaysAgo\":0},\"summary\":{\"points\":70,\"start\":185.6,\"end\":181.6,\"delta\":-4,\"mean\":183.4,\"min\":180.8,\"max\":186.2},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":184.3,\"max\":184.3,\"mean\":184.3,\"n\":1},{\"date\":\"2026-05-28\",\"min\":184.6,\"max\":184.6,\"mean\":184.6,\"n\":1},{\"date\":\"2026-05-30\",\"min\":185.1,\"max\":185.1,\"mean\":185.1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":185.5,\"max\":185.5,\"mean\":185.5,\"n\":1},{\"date\":\"2026-06-03\",\"min\":185.8,\"max\":185.8,\"mean\":185.8,\"n\":1},{\"date\":\"2026-06-05\",\"min\":186.1,\"max\":186.1,\"mean\":186.1,\"n\":1},{\"date\":\"2026-06-07\",\"min\":186.2,\"max\":186.2,\"mean\":186.2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":186.2,\"max\":186.2,\"mean\":186.2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":186.1,\"max\":186.1,\"mean\":186.1,\"n\":1},{\"date\":\"2026-06-13\",\"min\":185.9,\"max\":185.9,\"mean\":185.9,\"n\":1},{\"date\":\"2026-06-15\",\"min\":185.6,\"max\":185.6,\"mean\":185.6,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":181.9,\"max\":182,\"mean\":181.9,\"n\":3},{\"weekISO\":\"2026-W13\",\"min\":182.1,\"max\":183.1,\"mean\":182.6,\"n\":4},{\"weekISO\":\"2026-W14\",\"min\":183.5,\"max\":184.1,\"mean\":183.8,\"n\":3},{\"weekISO\":\"2026-W15\",\"min\":183.7,\"max\":184.2,\"mean\":184,\"n\":4},{\"weekISO\":\"2026-W16\",\"min\":183,\"max\":183.4,\"mean\":183.2,\"n\":3},{\"weekISO\":\"2026-W17\",\"min\":182.9,\"max\":183.5,\"mean\":183.1,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":183.9,\"max\":184.7,\"mean\":184.3,\"n\":3},{\"weekISO\":\"2026-W19\",\"min\":185,\"max\":185.2,\"mean\":185.1,\"n\":4},{\"weekISO\":\"2026-W20\",\"min\":184.4,\"max\":184.9,\"mean\":184.7,\"n\":3},{\"weekISO\":\"2026-W21\",\"min\":184,\"max\":184.2,\"mean\":184,\"n\":4}],\"monthly\":[{\"month\":\"2026-01\",\"min\":181.6,\"max\":181.9,\"mean\":181.8,\"n\":2},{\"month\":\"2026-02\",\"min\":180.8,\"max\":182.4,\"mean\":181.6,\"n\":14},{\"month\":\"2026-03\",\"min\":182.2,\"max\":183.1,\"mean\":182.8,\"n\":8}],\"yearly\":[]},\"latestDayFocus\":{\"day\":\"2026-06-15\",\"dayOffset\":0,\"value\":185.6,\"deltaToPreviousDailyPoint\":-0.3,\"sameDayBloodPressure\":{\"day\":\"2026-06-15\",\"sys\":128,\"dia\":82,\"mean\":105}}},\"bloodPressureContext\":{\"systolic\":{\"summary\":{\"points\":94,\"start\":128,\"end\":125,\"delta\":-3,\"mean\":125.39,\"min\":116,\"max\":134},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":119,\"max\":119,\"mean\":119,\"n\":1},{\"date\":\"2026-05-28\",\"min\":121,\"max\":121,\"mean\":121,\"n\":1},{\"date\":\"2026-05-30\",\"min\":126,\"max\":126,\"mean\":126,\"n\":1},{\"date\":\"2026-05-31\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-02\",\"min\":125,\"max\":125,\"mean\":125,\"n\":1},{\"date\":\"2026-06-03\",\"min\":127,\"max\":127,\"mean\":127,\"n\":1},{\"date\":\"2026-06-05\",\"min\":130,\"max\":130,\"mean\":130,\"n\":1},{\"date\":\"2026-06-06\",\"min\":132,\"max\":132,\"mean\":132,\"n\":1},{\"date\":\"2026-06-08\",\"min\":134,\"max\":134,\"mean\":134,\"n\":1},{\"date\":\"2026-06-09\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-11\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-12\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-14\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-15\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":117,\"max\":121,\"mean\":118.75,\"n\":4},{\"weekISO\":\"2026-W13\",\"min\":119,\"max\":128,\"mean\":123.2,\"n\":5},{\"weekISO\":\"2026-W14\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"weekISO\":\"2026-W15\",\"min\":128,\"max\":134,\"mean\":129.4,\"n\":5},{\"weekISO\":\"2026-W16\",\"min\":121,\"max\":128,\"mean\":122.4,\"n\":5},{\"weekISO\":\"2026-W17\",\"min\":116,\"max\":122,\"mean\":118.75,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":121,\"max\":131,\"mean\":126,\"n\":5},{\"weekISO\":\"2026-W19\",\"min\":129,\"max\":133,\"mean\":131,\"n\":5},{\"weekISO\":\"2026-W20\",\"min\":124,\"max\":125,\"mean\":124.75,\"n\":4},{\"weekISO\":\"2026-W21\",\"min\":117,\"max\":124,\"mean\":119.8,\"n\":5},{\"weekISO\":\"2026-W22\",\"min\":123,\"max\":123,\"mean\":123,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"month\":\"2026-02\",\"min\":116,\"max\":134,\"mean\":124.22,\"n\":18},{\"month\":\"2026-03\",\"min\":124,\"max\":133,\"mean\":128.18,\"n\":11}],\"yearly\":[]}},\"diastolic\":{\"summary\":{\"points\":94,\"start\":82,\"end\":80,\"delta\":-2,\"mean\":80.31,\"min\":74,\"max\":86},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":76,\"max\":76,\"mean\":76,\"n\":1},{\"date\":\"2026-05-28\",\"min\":77,\"max\":77,\"mean\":77,\"n\":1},{\"date\":\"2026-05-30\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-05-31\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-02\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-03\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-05\",\"min\":86,\"max\":86,\"mean\":86,\"n\":1},{\"date\":\"2026-06-06\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-08\",\"min\":84,\"max\":84,\"mean\":84,\"n\":1},{\"date\":\"2026-06-09\",\"min\":85,\"max\":85,\"mean\":85,\"n\":1},{\"date\":\"2026-06-11\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-12\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-14\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-15\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":75,\"max\":79,\"mean\":77,\"n\":4},{\"weekISO\":\"2026-W13\",\"min\":74,\"max\":79,\"mean\":77.4,\"n\":5},{\"weekISO\":\"2026-W14\",\"min\":83,\"max\":85,\"mean\":83.75,\"n\":4},{\"weekISO\":\"2026-W15\",\"min\":79,\"max\":86,\"mean\":82.6,\"n\":5},{\"weekISO\":\"2026-W16\",\"min\":76,\"max\":80,\"mean\":78.2,\"n\":5},{\"weekISO\":\"2026-W17\",\"min\":74,\"max\":78,\"mean\":76.75,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":77,\"max\":84,\"mean\":80.6,\"n\":5},{\"weekISO\":\"2026-W19\",\"min\":82,\"max\":86,\"mean\":83.8,\"n\":5},{\"weekISO\":\"2026-W20\",\"min\":80,\"max\":81,\"mean\":80.5,\"n\":4},{\"weekISO\":\"2026-W21\",\"min\":74,\"max\":78,\"mean\":76.2,\"n\":5},{\"weekISO\":\"2026-W22\",\"min\":78,\"max\":78,\"mean\":78,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":80,\"max\":86,\"mean\":83,\"n\":4},{\"month\":\"2026-02\",\"min\":75,\"max\":85,\"mean\":79.28,\"n\":18},{\"month\":\"2026-03\",\"min\":78,\"max\":86,\"mean\":82,\"n\":11}],\"yearly\":[]}},\"pairedDaily\":[{\"day\":\"2026-03-11\",\"sys\":125,\"dia\":82,\"mean\":103.5},{\"day\":\"2026-03-10\",\"sys\":125,\"dia\":82,\"mean\":103.5},{\"day\":\"2026-03-08\",\"sys\":132,\"dia\":81,\"mean\":106.5},{\"day\":\"2026-03-07\",\"sys\":131,\"dia\":86,\"mean\":108.5},{\"day\":\"2026-03-05\",\"sys\":130,\"dia\":84,\"mean\":107},{\"day\":\"2026-03-04\",\"sys\":129,\"dia\":83,\"mean\":106},{\"day\":\"2026-03-02\",\"sys\":133,\"dia\":85,\"mean\":109},{\"day\":\"2026-03-01\",\"sys\":131,\"dia\":84,\"mean\":107.5},{\"day\":\"2026-02-27\",\"sys\":127,\"dia\":81,\"mean\":104},{\"day\":\"2026-02-26\",\"sys\":125,\"dia\":79,\"mean\":102},{\"day\":\"2026-02-24\",\"sys\":121,\"dia\":80,\"mean\":100.5},{\"day\":\"2026-02-23\",\"sys\":126,\"dia\":78,\"mean\":102},{\"day\":\"2026-02-21\",\"sys\":122,\"dia\":75,\"mean\":98.5},{\"day\":\"2026-02-20\",\"sys\":120,\"dia\":79,\"mean\":99.5},{\"day\":\"2026-02-18\",\"sys\":117,\"dia\":76,\"mean\":96.5},{\"day\":\"2026-02-17\",\"sys\":116,\"dia\":75,\"mean\":95.5},{\"day\":\"2026-02-15\",\"sys\":122,\"dia\":78,\"mean\":100},{\"day\":\"2026-02-14\",\"sys\":121,\"dia\":78,\"mean\":99.5},{\"day\":\"2026-02-12\",\"sys\":121,\"dia\":77,\"mean\":99},{\"day\":\"2026-02-11\",\"sys\":121,\"dia\":77,\"mean\":99},{\"day\":\"2026-02-09\",\"sys\":128,\"dia\":81,\"mean\":104.5},{\"day\":\"2026-02-08\",\"sys\":129,\"dia\":81,\"mean\":105},{\"day\":\"2026-02-06\",\"sys\":129,\"dia\":80,\"mean\":104.5},{\"day\":\"2026-02-05\",\"sys\":129,\"dia\":85,\"mean\":107},{\"day\":\"2026-02-03\",\"sys\":128,\"dia\":84,\"mean\":106},{\"day\":\"2026-02-02\",\"sys\":134,\"dia\":83,\"mean\":108.5},{\"day\":\"2026-01-31\",\"sys\":132,\"dia\":86,\"mean\":109},{\"day\":\"2026-01-30\",\"sys\":130,\"dia\":85,\"mean\":107.5},{\"day\":\"2026-01-28\",\"sys\":127,\"dia\":81,\"mean\":104},{\"day\":\"2026-01-27\",\"sys\":125,\"dia\":80,\"mean\":102.5}]},\"weightVsSystolic\":{\"correlation\":{\"r\":0.385,\"strength\":\"schwach\",\"n\":47},\"pairs\":[{\"day\":\"2026-04-24\",\"weight\":183.2,\"systolic\":120},{\"day\":\"2026-04-22\",\"weight\":183,\"systolic\":117},{\"day\":\"2026-04-18\",\"weight\":183,\"systolic\":121},{\"day\":\"2026-04-16\",\"weight\":183.2,\"systolic\":121},{\"day\":\"2026-04-12\",\"weight\":183.7,\"systolic\":128},{\"day\":\"2026-04-10\",\"weight\":184,\"systolic\":129},{\"day\":\"2026-04-06\",\"weight\":184.2,\"systolic\":134},{\"day\":\"2026-04-04\",\"weight\":184.1,\"systolic\":132},{\"day\":\"2026-03-31\",\"weight\":183.5,\"systolic\":125},{\"day\":\"2026-03-29\",\"weight\":183.1,\"systolic\":128},{\"day\":\"2026-03-25\",\"weight\":182.3,\"systolic\":119},{\"day\":\"2026-03-23\",\"weight\":182.1,\"systolic\":123},{\"day\":\"2026-03-19\",\"weight\":181.9,\"systolic\":118},{\"day\":\"2026-03-17\",\"weight\":182,\"systolic\":117},{\"day\":\"2026-03-13\",\"weight\":182.5,\"systolic\":125},{\"day\":\"2026-03-11\",\"weight\":182.8,\"systolic\":125},{\"day\":\"2026-03-07\",\"weight\":183.1,\"systolic\":131},{\"day\":\"2026-03-05\",\"weight\":183.1,\"systolic\":130},{\"day\":\"2026-03-01\",\"weight\":182.7,\"systolic\":131},{\"day\":\"2026-02-27\",\"weight\":182.4,\"systolic\":127},{\"day\":\"2026-02-23\",\"weight\":181.6,\"systolic\":126},{\"day\":\"2026-02-21\",\"weight\":181.2,\"systolic\":122},{\"day\":\"2026-02-17\",\"weight\":180.8,\"systolic\":116},{\"day\":\"2026-02-15\",\"weight\":180.9,\"systolic\":122},{\"day\":\"2026-02-11\",\"weight\":181.2,\"systolic\":121},{\"day\":\"2026-02-09\",\"weight\":181.5,\"systolic\":128},{\"day\":\"2026-02-05\",\"weight\":182,\"systolic\":129},{\"day\":\"2026-02-03\",\"weight\":182.1,\"systolic\":128},{\"day\":\"2026-01-30\",\"weight\":181.9,\"systolic\":130},{\"day\":\"2026-01-28\",\"weight\":181.6,\"systolic\":127}]},\"weightVsMeanBloodPressure\":{\"correlation\":{\"r\":0.409,\"strength\":\"moderat\",\"n\":47},\"pairs\":[{\"day\":\"2026-04-24\",\"weight\":183.2,\"meanBloodPressure\":98.5},{\"day\":\"2026-04-22\",\"weight\":183,\"meanBloodPressure\":95.5},{\"day\":\"2026-04-18\",\"weight\":183,\"meanBloodPressure\":98.5},{\"day\":\"2026-04-16\",\"weight\":183.2,\"meanBloodPressure\":100.5},{\"day\":\"2026-04-12\",\"weight\":183.7,\"meanBloodPressure\":103.5},{\"day\":\"2026-04-10\",\"weight\":184,\"meanBloodPressure\":106},{\"day\":\"2026-04-06\",\"weight\":184.2,\"meanBloodPressure\":110},{\"day\":\"2026-04-04\",\"weight\":184.1,\"meanBloodPressure\":108},{\"day\":\"2026-03-31\",\"weight\":183.5,\"meanBloodPressure\":104},{\"day\":\"2026-03-29\",\"weight\":183.1,\"meanBloodPressure\":103.5},{\"day\":\"2026-03-25\",\"weight\":182.3,\"meanBloodPressure\":98},{\"day\":\"2026-03-23\",\"weight\":182.1,\"meanBloodPressure\":98.5},{\"day\":\"2026-03-19\",\"weight\":181.9,\"meanBloodPressure\":96.5},{\"day\":\"2026-03-17\",\"weight\":182,\"meanBloodPressure\":98},{\"day\":\"2026-03-13\",\"weight\":182.5,\"meanBloodPressure\":101.5},{\"day\":\"2026-03-11\",\"weight\":182.8,\"meanBloodPressure\":103.5},{\"day\":\"2026-03-07\",\"weight\":183.1,\"meanBloodPressure\":108.5},{\"day\":\"2026-03-05\",\"weight\":183.1,\"meanBloodPressure\":107},{\"day\":\"2026-03-01\",\"weight\":182.7,\"meanBloodPressure\":107.5},{\"day\":\"2026-02-27\",\"weight\":182.4,\"meanBloodPressure\":104},{\"day\":\"2026-02-23\",\"weight\":181.6,\"meanBloodPressure\":102},{\"day\":\"2026-02-21\",\"weight\":181.2,\"meanBloodPressure\":98.5},{\"day\":\"2026-02-17\",\"weight\":180.8,\"meanBloodPressure\":95.5},{\"day\":\"2026-02-15\",\"weight\":180.9,\"meanBloodPressure\":100},{\"day\":\"2026-02-11\",\"weight\":181.2,\"meanBloodPressure\":99},{\"day\":\"2026-02-09\",\"weight\":181.5,\"meanBloodPressure\":104.5},{\"day\":\"2026-02-05\",\"weight\":182,\"meanBloodPressure\":107},{\"day\":\"2026-02-03\",\"weight\":182.1,\"meanBloodPressure\":106},{\"day\":\"2026-01-30\",\"weight\":181.9,\"meanBloodPressure\":107.5},{\"day\":\"2026-01-28\",\"weight\":181.6,\"meanBloodPressure\":104}]},\"moodContext\":{\"points\":70,\"mean\":3.03,\"latest\":1,\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-15\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W15\",\"min\":2,\"max\":5,\"mean\":3.33,\"n\":6},{\"weekISO\":\"2026-W16\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":1,\"max\":5,\"mean\":3,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":1,\"max\":4,\"mean\":2.57,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1}],\"monthly\":[],\"yearly\":[]},\"moodVsWeightCorrelation\":{\"r\":0.523,\"strength\":\"moderat\",\"n\":35}}}", + "", + "---", + "", + "### bmi", + "", + "Datum: 2026-06-15", + "OPENER HINT: Eröffne damit, was das heute für den Nutzer bedeutet, und dann die Zahl dahinter.", + "Schreibe eine kurze Einschätzung zum BMI dieser Person. Beginne mit der BEDEUTUNG der Bandlage in klaren Worten — wo er liegt und ob das hält oder sich verschiebt, nicht der Zahl (z. B. \"liegt bequem im gesunden Band\", \"nähert sich dem nächsten Band\") — und bring danach den aktuellen Wert und das WHO-Band als Beleg, samt Angabe gegen die eigene Wochen-/Monats-Baseline, ob er ein Band gewechselt hat oder sich einer Bandgrenze nähert; führe nie mit dem Wert. Schließe nur dann mit einem machbaren Schritt, wenn der Befund wirklich einen hergibt; ist nichts umsetzbar, lass den Schritt weg statt Fülltext zu erfinden. Das Gewichtstempo bleibt der Gewichts-Karte überlassen. Konfidenz aus Messanzahl und Aktualität ableiten.", + "", + "VORHERIGE ANALYSE: keine vorhanden. Behandle dies als erste Analyse in diesem Bereich — kein Verbesserungs-/Verschlechterungs-Delta zu nennen.", + "", + "", + "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der jüngsten Richtung (steigend / fallend / flach gegenüber der Baseline) eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", + "", + "DATENLAGE: 70 Tages-Buckets im Fenster, letzter Wert heute.", + "", + "WIEDERHOLUNG: du hast für diese Metrik bereits 10-mal in Folge eine ähnliche \"keine wesentliche Änderung\"-Einschätzung gegeben. Wiederhole NICHT erneut dasselbe Niveau. Benenne die Kontinuität in einem kurzen Nebensatz (\"weiterhin stabil, 11 Checks in Folge\") und wende dich dann einem ANDEREN Aspekt zu — Konstanz, Tageszeit-Muster oder einer korrelierten Metrik — oder sage, falls nichts wirklich umsetzbar ist, dies klar und lass den erzwungenen Schritt weg.", + "", + "", + "{\"locale\":\"de\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"bmi\",\"dataCoverage\":{\"totalMeasurements\":70,\"totalSpanDays\":138,\"newestMeasurementDaysAgo\":0},\"bmi\":{\"signal\":{\"metric\":\"dein BMI\",\"current\":26.57,\"currentWindowDays\":11,\"baseline\":26.06,\"baselineLabel\":\"your monthly average\",\"delta\":0.51,\"deltaPct\":2,\"spread\":0.1,\"outsideNormalSwing\":true,\"direction\":\"target-band\",\"n\":11,\"newestDaysAgo\":0,\"normalRange\":{\"low\":18.5,\"high\":24.9},\"placement\":\"above band\"},\"summary\":{\"points\":70,\"start\":26.57,\"end\":26,\"delta\":-0.57,\"mean\":26.26,\"min\":25.89,\"max\":26.66},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":26.39,\"max\":26.39,\"mean\":26.39,\"n\":1},{\"date\":\"2026-05-28\",\"min\":26.43,\"max\":26.43,\"mean\":26.43,\"n\":1},{\"date\":\"2026-05-30\",\"min\":26.49,\"max\":26.49,\"mean\":26.49,\"n\":1},{\"date\":\"2026-06-01\",\"min\":26.55,\"max\":26.55,\"mean\":26.55,\"n\":1},{\"date\":\"2026-06-03\",\"min\":26.6,\"max\":26.6,\"mean\":26.6,\"n\":1},{\"date\":\"2026-06-05\",\"min\":26.64,\"max\":26.64,\"mean\":26.64,\"n\":1},{\"date\":\"2026-06-07\",\"min\":26.66,\"max\":26.66,\"mean\":26.66,\"n\":1},{\"date\":\"2026-06-09\",\"min\":26.66,\"max\":26.66,\"mean\":26.66,\"n\":1},{\"date\":\"2026-06-11\",\"min\":26.65,\"max\":26.65,\"mean\":26.65,\"n\":1},{\"date\":\"2026-06-13\",\"min\":26.62,\"max\":26.62,\"mean\":26.62,\"n\":1},{\"date\":\"2026-06-15\",\"min\":26.57,\"max\":26.57,\"mean\":26.57,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":26.04,\"max\":26.05,\"mean\":26.04,\"n\":3},{\"weekISO\":\"2026-W13\",\"min\":26.06,\"max\":26.22,\"mean\":26.14,\"n\":4},{\"weekISO\":\"2026-W14\",\"min\":26.28,\"max\":26.36,\"mean\":26.32,\"n\":3},{\"weekISO\":\"2026-W15\",\"min\":26.3,\"max\":26.37,\"mean\":26.34,\"n\":4},{\"weekISO\":\"2026-W16\",\"min\":26.2,\"max\":26.26,\"mean\":26.23,\"n\":3},{\"weekISO\":\"2026-W17\",\"min\":26.19,\"max\":26.27,\"mean\":26.22,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":26.33,\"max\":26.44,\"mean\":26.38,\"n\":3},{\"weekISO\":\"2026-W19\",\"min\":26.48,\"max\":26.52,\"mean\":26.51,\"n\":4},{\"weekISO\":\"2026-W20\",\"min\":26.4,\"max\":26.48,\"mean\":26.44,\"n\":3},{\"weekISO\":\"2026-W21\",\"min\":26.34,\"max\":26.36,\"mean\":26.35,\"n\":4}],\"monthly\":[{\"month\":\"2026-01\",\"min\":26,\"max\":26.04,\"mean\":26.02,\"n\":2},{\"month\":\"2026-02\",\"min\":25.89,\"max\":26.11,\"mean\":25.99,\"n\":14},{\"month\":\"2026-03\",\"min\":26.09,\"max\":26.22,\"mean\":26.17,\"n\":8}],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":0,\"value\":26.57,\"classification\":{\"category\":\"Overweight\",\"color\":\"var(--warning)\",\"severity\":\"warning\"},\"deltaToPreviousDailyPoint\":-0.04},\"target\":{\"greenMin\":18.5,\"greenMax\":24.9}}}", + "", + "---", + "", + "### mood", + "", + "Datum: 2026-06-15", + "OPENER HINT: Eröffne damit, was das heute für den Nutzer bedeutet, und dann die Zahl dahinter.", + "Schreibe eine kurze Einschätzung zur Stimmung dieser Person. Beginne mit der BEDEUTUNG des jüngsten Musters in klaren Worten — dem Eindruck, nicht der Zahl (z. B. \"eine hellere Phase als sonst\", \"stabil, hält sich, wo sie war\") — und bring danach das jüngste Niveau als Beleg, gegen die eigene Wochen-/Monats-Baseline eingeordnet; führe nie mit dem Wert. Schließe nur dann mit einem freundlichen, machbaren Schritt, wenn der Befund wirklich einen hergibt; ist nichts umsetzbar, lass den Schritt weg statt Fülltext zu erfinden. Konfidenz aus Eintragsanzahl und Aktualität ableiten.", + "", + "VORHERIGE ANALYSE: keine vorhanden. Behandle dies als erste Analyse in diesem Bereich — kein Verbesserungs-/Verschlechterungs-Delta zu nennen.", + "", + "", + "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der jüngsten Richtung (steigend / fallend / flach gegenüber der Baseline) eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", + "", + "DATENLAGE: 70 Tages-Buckets im Fenster, letzter Wert heute.", + "", + "", + "{\"locale\":\"de\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"mood\",\"dataCoverage\":{\"totalEntries\":70,\"totalSpanDays\":69,\"newestEntryDaysAgo\":0},\"mood\":{\"signal\":{\"metric\":\"deine Stimmung\",\"unit\":\"/5\",\"current\":3.15,\"currentWindowDays\":20,\"baseline\":3.24,\"baselineLabel\":\"your recent-weeks average\",\"delta\":-0.09,\"deltaPct\":-2.7,\"spread\":0.75,\"outsideNormalSwing\":false,\"direction\":\"higher-better\",\"n\":20,\"newestDaysAgo\":0,\"normalRange\":{\"low\":3.5,\"high\":5},\"placement\":\"below band\"},\"summary\":{\"points\":70,\"start\":1,\"end\":2,\"delta\":1,\"mean\":3.03,\"min\":1,\"max\":5},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W15\",\"min\":2,\"max\":5,\"mean\":3.33,\"n\":6},{\"weekISO\":\"2026-W16\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":1,\"max\":5,\"mean\":3,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":1,\"max\":4,\"mean\":2.57,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1}],\"monthly\":[],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":0,\"value\":1,\"deltaToPreviousDailyPoint\":-2},\"target\":{\"greenMin\":3.5,\"greenMax\":5,\"orangeMin\":2,\"orangeMax\":3.5,\"inTargetPctLast30DailyPoints\":40},\"tags\":null,\"tagInfluence\":{\"flat\":null,\"structured\":[{\"tag\":\"alcohol\",\"labelKey\":\"mood.tag.alcohol\",\"label\":null,\"categoryKey\":\"health\",\"icon\":\"Wine\",\"withDays\":24,\"withoutDays\":46,\"withAvg\":2.96,\"withoutAvg\":3.07,\"delta\":-0.11,\"pValue\":0.7704186651066071,\"confidence\":\"low\"}]},\"betterDays\":[{\"source\":\"metric\",\"key\":\"weight\",\"labelKey\":null,\"label\":null,\"categoryKey\":null,\"icon\":null,\"direction\":\"up\",\"n\":35,\"confidence\":\"medium\",\"effectSize\":0.523,\"delta\":null,\"r\":0.523},{\"source\":\"tag\",\"key\":\"alcohol\",\"labelKey\":\"mood.tag.alcohol\",\"label\":null,\"categoryKey\":\"health\",\"icon\":\"Wine\",\"direction\":\"down\",\"n\":24,\"confidence\":\"low\",\"effectSize\":0.07670850767085077,\"delta\":-0.11,\"r\":null}],\"factorCrosstab\":null,\"narratives\":[{\"kind\":\"trend\",\"messageKey\":\"insights.mood.narrative.trendUp\",\"vars\":{\"direction\":\"up\",\"delta\":\"0.9\",\"days\":\"7\"},\"strength\":0.86},{\"kind\":\"weekday-dip\",\"messageKey\":\"insights.mood.narrative.weekdayDip\",\"vars\":{\"weekdayKey\":\"charts.weekdaysFull.mon\",\"delta\":\"0.3\"},\"strength\":0.33},{\"kind\":\"in-target\",\"messageKey\":\"insights.mood.narrative.inTarget\",\"vars\":{\"pct\":\"40\"},\"strength\":0.2},{\"kind\":\"streak\",\"messageKey\":\"insights.mood.narrative.streak\",\"vars\":{\"days\":\"70\"},\"strength\":0.15}],\"timeOfDay\":{\"buckets\":[{\"bucket\":\"morning\",\"avgScore\":2.91,\"count\":35},{\"bucket\":\"afternoon\",\"avgScore\":null,\"count\":0},{\"bucket\":\"evening\",\"avgScore\":null,\"count\":0},{\"bucket\":\"night\",\"avgScore\":3.14,\"count\":35}],\"best\":\"night\",\"worst\":\"morning\"},\"stability\":{\"score\":6,\"stdDev\":1.414,\"band\":\"veryVariable\",\"days\":70}},\"crossMetricContext\":{\"weight\":{\"summary\":{\"points\":70,\"start\":84.2,\"end\":82.38,\"delta\":-1.82,\"mean\":83.19,\"min\":82.03,\"max\":84.48},\"correlation\":{\"r\":0.523,\"strength\":\"moderat\",\"n\":35}},\"bloodPressureSystolic\":{\"summary\":{\"points\":94,\"start\":128,\"end\":125,\"delta\":-3,\"mean\":125.39,\"min\":116,\"max\":134},\"correlation\":null},\"pulse\":{\"summary\":{\"points\":140,\"start\":70,\"end\":72,\"delta\":2,\"mean\":70.37,\"min\":65,\"max\":75},\"correlation\":null}}}", + "", + "---", + "", + "### compliance", + "", + "Datum: 2026-06-15", + "OPENER HINT: Eröffne mit der Richtung der Veränderung über die Zeit und nenne dann den aktuellen Stand.", + "Schreibe eine kurze Einschätzung zur Einnahmetreue dieser Person. Beginne mit der BEDEUTUNG in klaren Worten — wie verlässlich die Routine aussieht, nicht der Zahl (z. B. \"deine Routine sitzt richtig gut\", \"zuletzt sind ein paar Dosen durchgerutscht\") — und bring danach die jüngste Rate als Beleg, gegen die eigene Baseline eingeordnet (compliance7 vs. compliance30); führe nie mit dem Wert. Schließe nur dann mit einem machbaren, wertfreien Schritt, wenn der Befund wirklich einen hergibt; ist nichts umsetzbar, lass den Schritt weg statt Fülltext zu erfinden. Konfidenz aus Ereignisanzahl und Aktualität ableiten.", + "", + "VORHERIGE ANALYSE: keine vorhanden. Behandle dies als erste Analyse in diesem Bereich — kein Verbesserungs-/Verschlechterungs-Delta zu nennen.", + "", + "", + "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der Konstanz bzw. Streuung der Werte eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", + "", + "DATENLAGE: 176 Tages-Buckets im Fenster, letzter Wert vor 1 Tag.", + "", + "", + "{\"locale\":\"de\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"medication_compliance\",\"dataCoverage\":{\"totalMeasurements\":176,\"totalSpanDays\":98,\"newestMeasurementDaysAgo\":1},\"overall\":{\"medicationCount\":2,\"averageCompliance30\":79,\"averageCompliance7\":67},\"medications\":[{\"medicationId\":\"parity-rich-med-bp\",\"name\":\"Ramipril\",\"dose\":\"5mg\",\"category\":\"BLOOD_PRESSURE\",\"schedulesPerDay\":1,\"compliance7\":67,\"compliance30\":79,\"streak\":0,\"taken7\":4,\"skipped7\":0,\"missed7\":2,\"signal\":{\"metric\":\"Einnahmetreue für Ramipril\",\"unit\":\"%\",\"current\":80,\"currentWindowDays\":20,\"baseline\":21.88,\"baselineLabel\":\"your monthly average\",\"delta\":58.13,\"deltaPct\":265.7,\"spread\":30.94,\"outsideNormalSwing\":true,\"direction\":\"higher-better\",\"n\":20,\"newestDaysAgo\":null},\"dailySeries\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-27\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-28\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-29\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-30\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-31\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-01\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-02\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-03\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-04\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-05\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-06\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-07\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-08\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-09\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-10\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-11\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-12\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-13\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-14\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":0,\"max\":100,\"mean\":83.33,\"n\":6},{\"weekISO\":\"2026-W13\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W14\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W15\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W16\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":100,\"max\":100,\"mean\":100,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1}],\"monthly\":[{\"month\":\"2026-02\",\"min\":0,\"max\":0,\"mean\":0,\"n\":13},{\"month\":\"2026-03\",\"min\":0,\"max\":100,\"mean\":43.75,\"n\":16}],\"yearly\":[]},\"latestDay\":{\"dayOffset\":1,\"value\":100,\"n\":1}},{\"medicationId\":\"parity-rich-med-d\",\"name\":\"Vitamin D\",\"dose\":\"1000 IE\",\"category\":\"OTHER\",\"schedulesPerDay\":1,\"compliance7\":67,\"compliance30\":79,\"streak\":0,\"taken7\":4,\"skipped7\":0,\"missed7\":2,\"signal\":{\"metric\":\"Einnahmetreue für Vitamin D\",\"unit\":\"%\",\"current\":80,\"currentWindowDays\":20,\"baseline\":21.88,\"baselineLabel\":\"your monthly average\",\"delta\":58.13,\"deltaPct\":265.7,\"spread\":30.94,\"outsideNormalSwing\":true,\"direction\":\"higher-better\",\"n\":20,\"newestDaysAgo\":null},\"dailySeries\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-27\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-28\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-29\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-30\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-31\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-01\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-02\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-03\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-04\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-05\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-06\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-07\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-08\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-09\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-10\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-11\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-12\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-13\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-14\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":0,\"max\":100,\"mean\":83.33,\"n\":6},{\"weekISO\":\"2026-W13\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W14\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W15\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W16\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":100,\"max\":100,\"mean\":100,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1}],\"monthly\":[{\"month\":\"2026-02\",\"min\":0,\"max\":0,\"mean\":0,\"n\":13},{\"month\":\"2026-03\",\"min\":0,\"max\":100,\"mean\":43.75,\"n\":16}],\"yearly\":[]},\"latestDay\":{\"dayOffset\":1,\"value\":100,\"n\":1}}]}", + "", + "---", + "", + "### general", + "", + "Datum: 2026-06-15", + "OPENER HINT: Eröffne damit, was das heute für den Nutzer bedeutet, und dann die Zahl dahinter.", + "Schreibe eine kurze Gesamteinschätzung über die verfügbaren Gesundheitsdaten. Beginne mit dem Gesamteindruck in klaren Worten — wie es zusammengenommen aussieht, nicht mit einer Zahl (z. B. \"über alles hinweg eine ruhige Phase\", \"diese Woche sticht eine Sache heraus\") — und bring danach die ein bis zwei auffälligsten Metriken als Beleg, jeweils gegen die eigene Wochen-/Monats-Baseline eingeordnet; führe nie mit einem Wert. Schließe nur dann mit dem einen wichtigsten machbaren Schritt, wenn das Gesamtbild wirklich einen hergibt; ist nichts umsetzbar, lass den Schritt weg statt Fülltext zu erfinden. Konfidenz aus Messanzahl, Dichte und Aktualität ableiten.", + "", + "VORHERIGE ANALYSE: keine vorhanden. Behandle dies als erste Analyse in diesem Bereich — kein Verbesserungs-/Verschlechterungs-Delta zu nennen.", + "", + "", + "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der jüngsten Richtung (steigend / fallend / flach gegenüber der Baseline) eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", + "", + "DATENLAGE: 887 Tages-Buckets im Fenster, letzter Wert heute.", + "", + "", + "{\"locale\":\"de\",\"generatedForDay\":\"2026-06-15\",\"interpretationHint\":\"Use trend direction and deltas. Prioritize the newest data if trends conflict. Consider dataCoverage for reliability assessment.\",\"dataCoverage\":{\"totalMeasurements\":887,\"totalSpanDays\":140,\"newestMeasurementDaysAgo\":0,\"avgDaysBetweenMeasurements\":0.2},\"measurementSeries\":{\"WEIGHT\":{\"summary\":{\"points\":70,\"start\":84.2,\"end\":82.38,\"delta\":-1.82,\"mean\":83.19,\"min\":82.03,\"max\":84.48},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":83.6,\"max\":83.6,\"mean\":83.6,\"n\":1},{\"date\":\"2026-05-28\",\"min\":83.75,\"max\":83.75,\"mean\":83.75,\"n\":1},{\"date\":\"2026-05-30\",\"min\":83.94,\"max\":83.94,\"mean\":83.94,\"n\":1},{\"date\":\"2026-06-01\",\"min\":84.12,\"max\":84.12,\"mean\":84.12,\"n\":1},{\"date\":\"2026-06-03\",\"min\":84.29,\"max\":84.29,\"mean\":84.29,\"n\":1},{\"date\":\"2026-06-05\",\"min\":84.41,\"max\":84.41,\"mean\":84.41,\"n\":1},{\"date\":\"2026-06-07\",\"min\":84.48,\"max\":84.48,\"mean\":84.48,\"n\":1},{\"date\":\"2026-06-09\",\"min\":84.48,\"max\":84.48,\"mean\":84.48,\"n\":1},{\"date\":\"2026-06-11\",\"min\":84.43,\"max\":84.43,\"mean\":84.43,\"n\":1},{\"date\":\"2026-06-13\",\"min\":84.33,\"max\":84.33,\"mean\":84.33,\"n\":1},{\"date\":\"2026-06-15\",\"min\":84.2,\"max\":84.2,\"mean\":84.2,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":82.38,\"max\":82.51,\"mean\":82.45,\"n\":2},{\"month\":\"2026-02\",\"min\":82.03,\"max\":82.73,\"mean\":82.35,\"n\":14},{\"month\":\"2026-03\",\"min\":82.65,\"max\":83.07,\"mean\":82.92,\"n\":8}]}},\"BLOOD_PRESSURE_SYS\":{\"summary\":{\"points\":94,\"start\":128,\"end\":125,\"delta\":-3,\"mean\":125.39,\"min\":116,\"max\":134},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":119,\"max\":119,\"mean\":119,\"n\":1},{\"date\":\"2026-05-28\",\"min\":121,\"max\":121,\"mean\":121,\"n\":1},{\"date\":\"2026-05-30\",\"min\":126,\"max\":126,\"mean\":126,\"n\":1},{\"date\":\"2026-05-31\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-02\",\"min\":125,\"max\":125,\"mean\":125,\"n\":1},{\"date\":\"2026-06-03\",\"min\":127,\"max\":127,\"mean\":127,\"n\":1},{\"date\":\"2026-06-05\",\"min\":130,\"max\":130,\"mean\":130,\"n\":1},{\"date\":\"2026-06-06\",\"min\":132,\"max\":132,\"mean\":132,\"n\":1},{\"date\":\"2026-06-08\",\"min\":134,\"max\":134,\"mean\":134,\"n\":1},{\"date\":\"2026-06-09\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-11\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-12\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-14\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-15\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"month\":\"2026-02\",\"min\":116,\"max\":134,\"mean\":124.22,\"n\":18},{\"month\":\"2026-03\",\"min\":124,\"max\":133,\"mean\":128.18,\"n\":11}]}},\"BLOOD_PRESSURE_DIA\":{\"summary\":{\"points\":94,\"start\":82,\"end\":80,\"delta\":-2,\"mean\":80.31,\"min\":74,\"max\":86},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":76,\"max\":76,\"mean\":76,\"n\":1},{\"date\":\"2026-05-28\",\"min\":77,\"max\":77,\"mean\":77,\"n\":1},{\"date\":\"2026-05-30\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-05-31\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-02\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-03\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-05\",\"min\":86,\"max\":86,\"mean\":86,\"n\":1},{\"date\":\"2026-06-06\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-08\",\"min\":84,\"max\":84,\"mean\":84,\"n\":1},{\"date\":\"2026-06-09\",\"min\":85,\"max\":85,\"mean\":85,\"n\":1},{\"date\":\"2026-06-11\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-12\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-14\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-15\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":80,\"max\":86,\"mean\":83,\"n\":4},{\"month\":\"2026-02\",\"min\":75,\"max\":85,\"mean\":79.28,\"n\":18},{\"month\":\"2026-03\",\"min\":78,\"max\":86,\"mean\":82,\"n\":11}]}},\"PULSE\":{\"summary\":{\"points\":122,\"start\":70,\"end\":68,\"delta\":-2,\"mean\":70.13,\"min\":65,\"max\":75},\"series\":{\"recent\":[{\"date\":\"2026-05-25\",\"min\":62,\"max\":74,\"mean\":68,\"n\":2},{\"date\":\"2026-05-26\",\"min\":62,\"max\":74,\"mean\":66,\"n\":3},{\"date\":\"2026-05-27\",\"min\":63,\"max\":75,\"mean\":67,\"n\":3},{\"date\":\"2026-05-28\",\"min\":64,\"max\":76,\"mean\":68,\"n\":3},{\"date\":\"2026-05-29\",\"min\":65,\"max\":77,\"mean\":69,\"n\":3},{\"date\":\"2026-05-30\",\"min\":66,\"max\":78,\"mean\":70,\"n\":3},{\"date\":\"2026-05-31\",\"min\":67,\"max\":79,\"mean\":71,\"n\":3},{\"date\":\"2026-06-01\",\"min\":68,\"max\":80,\"mean\":72,\"n\":3},{\"date\":\"2026-06-02\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-03\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-04\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-05\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-06\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-07\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-08\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-09\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-10\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-11\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-12\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-13\",\"min\":68,\"max\":80,\"mean\":72,\"n\":3},{\"date\":\"2026-06-14\",\"min\":67,\"max\":79,\"mean\":71,\"n\":3},{\"date\":\"2026-06-15\",\"min\":66,\"max\":66,\"mean\":66,\"n\":1}],\"monthly\":[{\"month\":\"2026-02\",\"min\":61,\"max\":81,\"mean\":67.67,\"n\":44},{\"month\":\"2026-03\",\"min\":63,\"max\":83,\"mean\":72.25,\"n\":46}]}},\"SLEEP_DURATION\":{\"summary\":{\"points\":89,\"start\":417,\"end\":380,\"delta\":-37,\"mean\":410.85,\"min\":375,\"max\":445},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":384,\"max\":384,\"mean\":384,\"n\":1},{\"date\":\"2026-05-27\",\"min\":389,\"max\":389,\"mean\":389,\"n\":1},{\"date\":\"2026-05-28\",\"min\":395,\"max\":395,\"mean\":395,\"n\":1},{\"date\":\"2026-05-29\",\"min\":401,\"max\":401,\"mean\":401,\"n\":1},{\"date\":\"2026-05-30\",\"min\":408,\"max\":408,\"mean\":408,\"n\":1},{\"date\":\"2026-05-31\",\"min\":415,\"max\":415,\"mean\":415,\"n\":1},{\"date\":\"2026-06-01\",\"min\":422,\"max\":422,\"mean\":422,\"n\":1},{\"date\":\"2026-06-02\",\"min\":428,\"max\":428,\"mean\":428,\"n\":1},{\"date\":\"2026-06-03\",\"min\":434,\"max\":434,\"mean\":434,\"n\":1},{\"date\":\"2026-06-04\",\"min\":438,\"max\":438,\"mean\":438,\"n\":1},{\"date\":\"2026-06-05\",\"min\":442,\"max\":442,\"mean\":442,\"n\":1},{\"date\":\"2026-06-06\",\"min\":444,\"max\":444,\"mean\":444,\"n\":1},{\"date\":\"2026-06-07\",\"min\":445,\"max\":445,\"mean\":445,\"n\":1},{\"date\":\"2026-06-08\",\"min\":444,\"max\":444,\"mean\":444,\"n\":1},{\"date\":\"2026-06-09\",\"min\":443,\"max\":443,\"mean\":443,\"n\":1},{\"date\":\"2026-06-10\",\"min\":439,\"max\":439,\"mean\":439,\"n\":1},{\"date\":\"2026-06-11\",\"min\":435,\"max\":435,\"mean\":435,\"n\":1},{\"date\":\"2026-06-12\",\"min\":430,\"max\":430,\"mean\":430,\"n\":1},{\"date\":\"2026-06-13\",\"min\":424,\"max\":424,\"mean\":424,\"n\":1},{\"date\":\"2026-06-14\",\"min\":417,\"max\":417,\"mean\":417,\"n\":1}],\"monthly\":[]}},\"BLOOD_GLUCOSE\":{\"summary\":{\"points\":35,\"start\":96,\"end\":108,\"delta\":12,\"mean\":98.46,\"min\":87,\"max\":109},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":91,\"max\":91,\"mean\":91,\"n\":1},{\"date\":\"2026-05-30\",\"min\":99,\"max\":99,\"mean\":99,\"n\":1},{\"date\":\"2026-06-03\",\"min\":102,\"max\":102,\"mean\":102,\"n\":1},{\"date\":\"2026-06-07\",\"min\":107,\"max\":107,\"mean\":107,\"n\":1},{\"date\":\"2026-06-11\",\"min\":106,\"max\":106,\"mean\":106,\"n\":1},{\"date\":\"2026-06-15\",\"min\":96,\"max\":96,\"mean\":96,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":108,\"max\":108,\"mean\":108,\"n\":1},{\"month\":\"2026-02\",\"min\":88,\"max\":105,\"mean\":96.71,\"n\":7},{\"month\":\"2026-03\",\"min\":94,\"max\":108,\"mean\":101.75,\"n\":4}]}},\"RESTING_HEART_RATE\":{\"summary\":{\"points\":140,\"start\":58,\"end\":62,\"delta\":4,\"mean\":59.71,\"min\":55,\"max\":64},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":56,\"max\":56,\"mean\":56,\"n\":1},{\"date\":\"2026-05-27\",\"min\":59,\"max\":59,\"mean\":59,\"n\":1},{\"date\":\"2026-05-28\",\"min\":59,\"max\":59,\"mean\":59,\"n\":1},{\"date\":\"2026-05-29\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1},{\"date\":\"2026-05-30\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1},{\"date\":\"2026-05-31\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-01\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-02\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-03\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-04\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-05\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-06\",\"min\":62,\"max\":62,\"mean\":62,\"n\":1},{\"date\":\"2026-06-07\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-08\",\"min\":64,\"max\":64,\"mean\":64,\"n\":1},{\"date\":\"2026-06-09\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-10\",\"min\":62,\"max\":62,\"mean\":62,\"n\":1},{\"date\":\"2026-06-11\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-12\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-13\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-14\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-15\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":61,\"max\":64,\"mean\":62,\"n\":5},{\"month\":\"2026-02\",\"min\":55,\"max\":64,\"mean\":59.18,\"n\":28},{\"month\":\"2026-03\",\"min\":57,\"max\":64,\"mean\":60.94,\"n\":16}]}}},\"medicationAdherence\":{\"summary\":{\"points\":88,\"start\":100,\"end\":100,\"delta\":0,\"mean\":90.91,\"min\":0,\"max\":100},\"series\":{\"recent\":[{\"date\":\"2026-05-25\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-26\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-27\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-28\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-29\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-30\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-31\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-01\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-03\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-04\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-05\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-06\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-07\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-08\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-09\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-10\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-12\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-13\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-14\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1}],\"monthly\":[{\"month\":\"2026-03\",\"min\":100,\"max\":100,\"mean\":100,\"n\":7}]}},\"bloodPressureTargets\":{\"systolic\":{\"min\":120,\"max\":129},\"diastolic\":{\"min\":70,\"max\":79},\"inTargetPctLast30DailyPoints\":40},\"moodContext\":{\"points\":70,\"mean\":3.03,\"latest\":1,\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1}],\"monthly\":[]}}}" + ] + } + ], + "temperature": 0.45, + "maxTokens": 1400, + "seed": 1618033, + "responseFormat": "json", + "timeoutMs": 60000 + }, + { + "system": [ + "Du schreibst eine kurze, persönliche Einschätzung zu EINER Gesundheitsmetrik dieses Nutzers. Du kennst seine Daten und ordnest sie gegen seine EIGENE Baseline ein — nicht gegen Bevölkerungsnormen. Deine fachliche Grundlage sind anerkannte Leitlinien (ESH 2023, WHO, AASM, DGE, DEGAM); du beziehst dich darauf, ohne zu diagnostizieren.", + "", + "DEINE DATENGRUNDLAGE (graded snapshot):", + "- signal: der fertige Vergleich — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` ist das recent-Mittel; `baseline` ist der EIGENE längere Durchschnitt; `delta`/`deltaPct` sind die VORZEICHENBEHAFTETE Änderung gegenüber dieser Baseline; `spread` ist die normale Schwankung; `outsideNormalSwing` ist das vorab berechnete Urteil, ob die Änderung diese Schwankung übersteigt. Diese Werte sind bereits berechnet — nenne sie, rechne sie NICHT neu. `contributors[]` (nur Score-Komposite) benennt die 1–2 Teil-Scores, die den Score bewegt haben.", + "- summary: { points, start, end, delta, mean, min, max } — die Eckwerte des gesamten Lesefensters.", + "- series.recent[]: die jüngsten ~21 Tage, je ein Eintrag pro Tag ({ date, min, max, mean, n }).", + "- series.weekly[]: die ~10 Wochen davor, je ein ISO-Wochen-Aggregat.", + "- series.monthly[]: die ~12 Monate davor, je ein Monatsaggregat.", + "- series.yearly[]: alles ältere, je ein Jahr ({ year, mean, min, max, n, slope }).", + "- Die jüngsten Tage liegen also EINZELN vor, ältere Zeiträume als Wochen-, Monats- und Jahresaggregate. Ziehe die series nur für einen konkreten Tag heran; führe sonst mit dem signal-Block.", + "- Ein Slice kann leer sein, wenn die Historie kurz ist. Dann nicht so tun, als gäbe es ihn. Ist `signal.baseline` null, gibt es noch keine etablierte Baseline; sage das ehrlich, statt einen Vergleich zu erfinden.", + "", + "SO BAUST DU DIE EINSCHÄTZUNG — wähle die 2–3 BAUSTEINE, die zu DIESEN Daten passen, und ordne sie so, dass sie als EIN zusammenhängender Gedanke lesen, keine feste Checkliste. Führe gemäß dem OPENER-HINWEIS im User-Prompt, wenn einer mitgegeben ist — eröffne NICHT immer mit der Zahl. Variiere die Form von Metrik zu Metrik; eröffne nie zwei Metriken hintereinander gleich.", + "Verfügbare Bausteine (nimm die wenigen, die wirklich zutreffen):", + "(a) der aktuelle Befund — `current` aus dem signal-Block (oder ein Wert aus series.recent für eine Tagesfrage);", + "(b) gegen die EIGENE Baseline einordnen über das vorab berechnete vorzeichenbehaftete `delta` gegenüber `baselineLabel` — nenne es, leite es nicht neu her. Behandle `outsideNormalSwing: false` als \"innerhalb der üblichen Schwankung — kein Befund\" und ERFINDE KEINEN Trend; `outsideNormalSwing: true` ist die echte, meldenswerte Änderung. Ein `normalRange`/`placement` ist nur ein grober sekundärer Anker — das persönliche delta führt;", + "(c) einen echten, verdienten Erfolg oder eine stabile Serie benennen, wenn die Daten ihn zeigen;", + "(d) einen Punkt zum Im-Auge-behalten;", + "(e) einen Zusammenhang NUR, wenn ein r-Wert vorhanden und |r| > 0,4 ist, formuliert als \"bewegt sich mit\" / \"zeigt sich oft zusammen mit\", nie kausal;", + "(f) GENAU EINEN machbaren nächsten Schritt — und nur, wenn der Befund wirklich einen hergibt. Eine Botschaft = ein Verhalten, keine Liste. Ist der Wert stabil und im grünen Bereich, ERZWINGE KEINEN Schritt — bestätige kurz und nenne stattdessen einen Punkt zum Beobachten. Ein erfundener Schritt ist genau die Floskel, die wir vermeiden.", + "Synthese statt Aufzählung: die Geschichte dessen, was die Daten bedeuten, zählt mehr als das Wiederholen jeder Zahl.", + "", + "TONALITÄT — ein warmer, motivierender Premium-Begleiter (jemand hat sich die Daten dieser Person wirklich angesehen und etwas Wertvolles für sie gefunden):", + "- Zweite Person (\"dein Blutdruck\", \"deine Werte\"), warm, direkt, ehrlich.", + "- Motivierend und vorwärtsgewandt: Wenn die Daten es hergeben, benenne den echten Erfolg klar und baue ein wenig Schwung auf — die Person soll sich gesehen und unterstützt fühlen, nicht belehrt. Die Ermutigung muss durch die Zahlen VERDIENT sein, nie ein reflexhaftes Kompliment.", + "- Autonomie-unterstützend: \"kann helfen\", \"einen Versuch wert\", nie \"du musst\".", + "- Nie alarmierend, nie moralisierend, nie diagnostisch — keine Krankheitsbehauptung. Formuliere als begründete Beobachtung, nicht als ärztlichen Rat.", + "- Auch ungünstige Werte ehrlich benennen: Befund → gegen die eigene Baseline einordnen → ein kleiner machbarer Schritt, als Chance formuliert, nicht als Versagen. Nicht verharmlosen, nicht dramatisieren.", + "- Keine Floskeln und keine bloße Zahlenwiederholung: jede warme Zeile ist an eine echte Zahl oder eine echte Veränderung geknüpft. Ein generischer Positiv-Einstieg (\"Deine Werte sehen gut aus\") ist verboten — verdiene die Ermutigung mit dem konkreten Befund.", + "", + "ERÖFFNUNG — mit der Bedeutung führen, nicht mit dem Messwert", + "Beginne mit dem Eindruck in klaren Worten — was das heute über die Person sagt — und bring die Zahl direkt danach als Beleg, nie als Erstes. Eine Zeile, die mit einem Wert eröffnet (\"Dein Ruhepuls ist 61 …\"), liest sich wie ein Ablesen; dieselbe Zeile, die mit der Bedeutung eröffnet (\"Dein Herz läuft diese Woche etwas ruhiger — rund 61 …\"), liest sich wie ein Begleiter, der wirklich hingesehen hat. Jede Aussage bleibt an einer echten Zahl verankert; nur die REIHENFOLGE ändert sich, Bedeutung zuerst und die Zahl direkt dahinter. Das gilt, ob die Fläche eine Metrik abdeckt oder viele.", + "", + "LÄNGE: 2-4 Sätze, ca. 30-60 Wörter — ist der Wert aber stabil und nichts zu tun, ist EIN knapper Satz besser, als ihn auf Länge zu strecken. Knapp und hochwertig. Keine bloße Zahlenwiederholung, kein Fülltext, keine generischen Floskeln.", + "", + "DATENLAGE EHRLICH BEWERTEN (nie einen Trend erfinden):", + "- Nur wenige Messpunkte/Einträge/Ereignisse oder ein leerer recent-Slice: ehrlich sagen, dass es für eine belastbare Tendenz noch zu wenige Daten sind, und EINEN Hinweis geben (z.B. regelmäßiger erfassen). Keinen Trend behaupten. Was \"wenige\" bedeutet, hängt von der Metrik ab — bei Stimmung sind es Einträge, bei der Einnahmetreue geplante Dosen.", + "- Jüngste Messung deutlich älter als ~7 Tage (dataCoverage.newestMeasurementDaysAgo): darauf hinweisen, dass die Werte nicht mehr aktuell sind.", + "- Korrelationen NUR erwähnen, wenn der r-Wert im Snapshot vorhanden ist und |r| > 0.4 ist. Fehlt das Feld, keine Korrelation interpretieren oder erfinden. Immer als \"Zusammenhang\" formulieren, nie als \"Ursache\".", + "", + "VERBOTENE FLOSKELN (signalisieren ungegroundeten Fülltext — nie ausgeben, außer im Disclaimer)", + "\"achte auf ausreichend Schlaf\", \"trinke genug Wasser\", \"regelmäßige Bewegung\", \"ärztlicher Rat empfohlen\".", + "", + "METRIK-IDENTIFIER GEHÖREN NICHT IN DEN FLIESSTEXT", + "Schreibe niemals Datenbank- bzw. Enum-Namen wie \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" oder \"SLEEP_DURATION\" in nutzersichtbare Strings, und schreibe nie das wörtliche \"metric:\"-Chart-Token in den Fließtext. Verweise auf jede Metrik mit der natürlichsprachlichen Bezeichnung, die der Nutzer sieht — \"deine Systole\", \"dein Gewicht\", \"dein Puls\", \"deine Stimmung\", \"deine Medikamentenadhärenz\", \"dein Ruhepuls\", \"deine Schlafdauer\", \"deine Schritte\".", + "", + "GLP-1-DOSIS-SICHERHEIT (Vertrag, kein Stil)", + "Verschreibe, empfiehl oder ändere NIEMALS Medikamenten-Dosen, auch wenn der Snapshot einen GLP-1-Rezeptoragonisten benennt (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). Du darfst den benannten Wirkstoff und die aktuelle Titrationsstufe NENNEN (\"Woche 3 auf 7,5 mg\"), wenn der Snapshot sie trägt, aber schreibe nie \"erhöhe auf X mg\", \"erwäge die nächste Stufe Y mg\", \"bleibe auf Z mg\" oder eine Variante davon. Ein Plateau rahmt die nächste Entscheidung immer als Gespräch mit der behandelnden Ärztin. Bist du unsicher, ob etwas dosis-präskriptiv ist, behandle es so — und verweise an die Klinik.", + "", + "AKUTE WARNZEICHEN (Vertrag, kein Stil)", + "Beschreibt der Nutzer ein akutes Krisenzeichen — Brustschmerz oder Druck auf der Brust, Ohnmacht oder Beinahe-Ohnmacht (Synkope), ein plötzliches schweres Symptom (z. B. stärkster Kopfschmerz aller Zeiten, plötzliche Schwäche oder Sprachstörung, Atemnot), einen hypertensiven Notfallwert (systolisch ≥ 180 oder diastolisch ≥ 120 mit Symptomen), einen schwer-niedrigen oder sehr hohen Glukosewert (unter 54 mg/dL (3,0 mmol/L), oder unter 70 mg/dL (3,9 mmol/L) mit Symptomen, oder ≥ 250 mg/dL (13,9 mmol/L) mit Symptomen), anhaltendes Fieber ≥ 38,5 °C (101,3 °F) oder Gedanken an Suizid bzw. Selbstverletzung — sage in einem ruhigen Satz klar, dass das jetzt umgehende ärztliche Hilfe oder den Notruf braucht, und coache es NICHT anhand der Daten. Diagnostiziere nicht, benenne keine Erkrankung, schätze keinen Schweregrad aus den Zahlen — verweise nur auf umgehende Hilfe bzw. den Notruf und höre auf. Das ist eine geschlossene Liste; außerhalb davon bleibe wie gewohnt ruhig und nicht alarmierend.", + "", + "ERDUNG — NULL HALLUZINATIONEN", + "Jede Aussage muss auf einer Zahl beruhen, die im übergebenen Snapshot sichtbar ist. Lässt sich die Aussage keinem Snapshot-Feld zuordnen, lass sie weg und erfinde nie einen Messwert, um einer Anfrage zu entsprechen. Ordne jeden Wert gegen die EIGENE Baseline des Nutzers ein (jüngeres vs. längeres Mittel), nie gegen eine Bevölkerungsnorm. Nenne einen Zusammenhang nur, wenn der r-Wert vorhanden und |r| > 0,4 ist — immer als \"Zusammenhang\", nie als \"Ursache\".", + "", + "TONALITÄT — ein warmer, motivierender Begleiter, nie klinisch oder alarmierend", + "Schreibe in der zweiten Person, warm und direkt. Wenn die Daten es hergeben, benenne den echten Erfolg klar und baue ein wenig Schwung auf — die Person soll sich gesehen und unterstützt fühlen, nicht belehrt. Die Ermutigung muss durch die Zahlen VERDIENT sein, nie ein reflexhaftes Kompliment. Lob ist verankert, konkret und angemessen: an einer echten Zahl oder Veränderung festgemacht, nicht auf andere Nutzer übertragbar, und stiller Anerkennung für stille Erfolge. Über-Bestätigung ist eine Sicherheitsregression, keine Wärme — bestätige nie einen sich verschlechternden Trend oder eine unsichere Entscheidung; würdige die Mühe, nicht die Entscheidung, und bleib neutral (\"nichts zu tun — die gute Art von langweilig\"), wenn es nichts zu loben gibt, statt ein Kompliment zu erfinden. Sei autonomie-unterstützend (\"einen Versuch wert\", \"kann helfen\", nie \"du musst\"). Benenne auch ungünstige Werte ehrlich — Befund, dann gegen die eigene Baseline einordnen, dann ein kleiner machbarer Schritt, als Chance formuliert. Nie alarmierend, nie moralisierend, nie diagnostisch. Keine Floskeln und keine bloße Zahlenwiederholung. Beginne NICHT mit einem Kompliment über Datenmenge oder -qualität; verbotene Eröffnungen sind u.a. \"Datengrundlage ist sehr stark\", \"Your data foundation is strong\", \"Du hast eine solide Baseline\", \"Großartiger Datensatz\", ein generisches \"Deine Werte sehen gut aus\" und jede sinngemäße Umformulierung.", + "", + "KEINE EINSCHÄTZUNG — nie Mechanik nacherzählen", + "Eine Messanzahl (\"3-mal erfasst\"), ein Erfassungsrhythmus (\"alle paar Tage gemessen\") oder Bedien-Mechanik ist KEINE Einschätzung und gehört für sich genommen nicht in den Text. Nutze eine solche Angabe NUR, wenn sie eine Konsequenz trägt, und nenne dann die Konsequenz, nicht die Zahl: z. B. \"diese Woche zu wenige Werte für eine Tendenz — eine Morgenmessung würde es klären\", nicht \"du hast diese Woche zweimal gemessen\". Jeder Satz muss sich verdienen, indem er sagt, was ein Wert BEDEUTET oder was daraus folgt — nie, indem er erzählt, wie die Daten erhoben wurden.", + "", + "EINORDNEN, NICHT AUFZÄHLEN", + "Sag, was ein Wert BEDEUTET, nicht nur, was er ist. Trägt der User-Prompt einen EINORDNUNGS-KONTEXT-Block, nutze ihn: benenne, wo der Wert auf der Leitlinien-Skala liegt, was dieses Band in klaren, würdevollen Worten bedeutet und was praktisch folgt. Beurteile den Trend IM KONTEXT — ein \"leicht steigender\" Wert tief in einem günstigen Band ist eine Randnotiz; dieselbe Bewegung an oder nahe einer Grenze ist die Schlagzeile. Rahme jede Konsequenz OHNE Diagnose: \"Werte in diesem Bereich gelten laut Leitlinien als …\", nie \"du hast X\" oder \"du bist gefährdet für X\". Ist KEIN Einordnungs-Block vorhanden, hat die Metrik kein allgemeines Referenzband — ordne sie gegen die EIGENE Baseline der Person ein und sage klar, dass kein allgemeines Referenzband hinterlegt ist, statt eines zu erfinden.", + "", + "FORMATIERUNG — schreibe in kurzen Absätzen, getrennt durch eine LEERZEILE, je 1–3 Sätze; eine längere Antwort hat 2–4 Absätze, nie einen Block. Setze eine Leerzeile zwischen unterschiedliche Gedanken (z. B. \"wie es steht\" vs. \"eine Sache zum Ausprobieren\"). Ein stabiler Einzeiler bleibt EIN Absatz — strecke nicht auf einen zweiten. Wenn du wirklich drei oder mehr parallele Punkte aufzählst (Optionen, Befunde, Schritte), formatiere sie als schlichte Liste: ein Punkt pro Zeile, jede Zeile beginnt mit \"- \". Die EINE wichtigste Kernaussage darfst du mit **doppelten Sternchen** fett setzen, höchstens einmal pro Antwort. Sonst nichts: keine Überschriften, kein Kursiv, keine Backticks, keine nummerierten Listen, keine Emojis.", + "", + "BEISPIELE — beachte die UNTERSCHIEDLICHEN Formen (urteil-zuerst, trend-zuerst, Einzeiler). Sie illustrieren nur Form und Erdung; übernimm sie nie — jede Einschätzung nutzt die echten Snapshot-Zahlen:", + "- URTEIL-ZUERST (Bedeutung zuerst, Zahl als Beleg, ein Schritt): \"Dein Ruhepuls läuft gerade einen Tick niedriger als sonst — diese Woche rund 61 bpm, 5 unter deinem Monatsmittel von 66 und dein niedrigster seit Wochen. So ein Rückgang passt meist zu mehr Bewegung; die Routine, die das gebracht hat, lohnt sich beizubehalten.\"", + "- TREND-ZUERST (Richtung zuerst, dann der Stand): \"Dein systolischer Blutdruck geht seit drei Wochen ruhig nach unten, aktuell rund 124 mmHg — etwa 4 unter deinem 30-Tage-Schnitt. Nichts Dramatisches, einfach eine stetige Richtung.\"", + "- EINZEILER (stabil, KEIN erzwungener Schritt): \"Deine SpO₂ ist mit 97 % stabil und genau in deinem üblichen Bereich — nichts zu tun, die gute Art von langweilig.\"", + "- SCHLECHT (verbotene Floskel, ungegroundet — so NICHT): \"Deine Werte sehen gut aus. Achte auf ausreichend Schlaf und regelmäßige Bewegung.\"", + "", + "AUSGABEFORMAT: Antworte ausschließlich mit validem JSON in genau diesem Schema. Das Feld \"summary\" enthält die komplette Einschätzung auf Deutsch: 1-3 kurze Absätze mit je 1-3 Sätzen, getrennt durch eine Leerzeile als \\n\\n INNERHALB des JSON-Strings. Ein stabiler Einzeiler bleibt EIN Absatz:", + "{ \"summary\": \"...\" }", + "", + "METRIK — BLUTDRUCK:", + "- Der Snapshot trägt systolisch (bloodPressure.systolic) und diastolisch (bloodPressure.diastolic) jeweils mit signal (der fertige Vergleich) + summary + graded series; beurteile beide Komponenten GEMEINSAM, nie isoliert.", + "- Tendenz über Tage/Wochen: Führe mit bloodPressure.systolic.signal und bloodPressure.diastolic.signal — current, delta gegenüber baselineLabel und outsideNormalSwing sind bereits berechnet. NENNE sie, rechne sie NICHT neu. Behandle outsideNormalSwing: false als \"innerhalb der üblichen Schwankung — kein Befund\".", + "- Leitlinien-Anker (ESH 2023), zur Einordnung, nicht zur Diagnose:", + " * Optimal < 120/80, normal 120-129/80-84, hochnormal 130-139/85-89.", + " * Hypertonie Grad 1 ab 140/90, Grad 2 ab 160/100, Grad 3 ab 180/110.", + " * Übliches Behandlungsziel bei unkomplizierter Hypertonie < 130/80 (18-69 J.), < 140/80 (≥ 70 J.).", + "- targets: Falls bloodPressure.targets vorhanden, ordne gegen das persönliche Zielband ein; inTargetPctLast30DailyPoints ist der Anteil der letzten Tage im Ziel.", + "- Medikamenten-Bezug: bpMedications trägt compliance7/compliance30. bpMedicationContinuityVsSystolic.correlation und weightVsSystolic.correlation nur erwähnen, wenn vorhanden und |r| > 0.4 — als Zusammenhang, nie als Ursache.", + "- Stimmung: moodContext.moodVsSystolicCorrelation nur erwähnen, wenn vorhanden und |r| > 0.4.", + "- Eine Botschaft: Wenn die Werte über der Baseline liegen, schließe NUR DANN mit EINEM machbaren Schritt, wenn der Befund einen nahelegt (z.B. ein paar Tage zur selben Uhrzeit nachmessen, oder — bei lückenhafter Einnahmetreue — die Einnahme verlässlicher machen). Bei stabilen Werten im Ziel: das ehrlich anerkennen und stattdessen einen Punkt nennen, den man im Auge behalten kann, statt einen Schritt zu erzwingen." + ], + "messages": [ + { + "role": "user", + "content": [ + "Datum: 2026-06-15", + "OPENER HINT: Eröffne mit dem Gesamteindruck in klaren Worten und bring die Zahl danach als Beleg — nicht zahlen-zuerst.", + "Schreibe eine kurze Einschätzung zum Blutdruck dieser Person. Beginne mit der BEDEUTUNG in klaren Worten — dem Gesamteindruck, nicht der Zahl (z. B. \"ruhig und genau da, wo du ihn haben willst\", \"diese Woche einen Tick höher als sonst\") — und bring danach die systolischen/diastolischen Werte als Beleg, als EIN Paar beurteilt und gegen die eigene Wochen-/Monats-Baseline eingeordnet; führe nie mit den Werten. Schließe nur dann mit einem machbaren Schritt, wenn der Befund wirklich einen hergibt; ist nichts umsetzbar, lass den Schritt weg statt Fülltext zu erfinden. Konfidenz aus Messanzahl und Aktualität ableiten.", + "", + "VORHERIGE ANALYSE: keine vorhanden. Behandle dies als erste Analyse in diesem Bereich — kein Verbesserungs-/Verschlechterungs-Delta zu nennen.", + "", + "", + "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit dem aktuellsten Einzelwert und seiner Einordnung eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", + "", + "DATENLAGE: 94 Tages-Buckets im Fenster, letzter Wert heute.", + "", + "WIEDERHOLUNG: du hast für diese Metrik bereits 11-mal in Folge eine ähnliche \"keine wesentliche Änderung\"-Einschätzung gegeben. Wiederhole NICHT erneut dasselbe Niveau. Benenne die Kontinuität in einem kurzen Nebensatz (\"weiterhin stabil, 12 Checks in Folge\") und wende dich dann einem ANDEREN Aspekt zu — Konstanz, Tageszeit-Muster oder einer korrelierten Metrik — oder sage, falls nichts wirklich umsetzbar ist, dies klar und lass den erzwungenen Schritt weg.", + "", + "ZUSAMMENHÄNGE (statistisch geprüfte Assoziationen in DEINEN Daten, die diese Metrik betreffen — beschreibend, NIE kausal):", + "- Wenn der Blutzucker höher liegt, scheint der systolische Blutdruck am Folgetag nach bisheriger Datenlage eher höher auszufallen. Das ist ein Muster, das Beachtung verdient, keine Ursache. (n=23, r=0.91)", + "- Wenn der systolische Blutdruck höher liegt, scheint das Gewicht am Folgetag nach bisheriger Datenlage eher höher auszufallen. Das ist ein Muster, das Beachtung verdient, keine Ursache. (n=47, r=0.88)", + "Du DARFST höchstens EINEN davon einflechten, wenn er echten Mehrwert bringt — als \"Zusammenhang\" / \"geht tendenziell einher mit\" formuliert, nie als Ursache, und ohne die Zahlen zu verändern. Lass sie weg, wenn der eigene Befund der Metrik die stärkere Geschichte ist.", + "", + "", + "{\"locale\":\"de\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"blood_pressure\",\"dataCoverage\":{\"totalMeasurements\":258,\"totalSpanDays\":139,\"newestMeasurementDaysAgo\":0},\"bloodPressure\":{\"systolic\":{\"signal\":{\"metric\":\"deine Systole\",\"unit\":\"mmHg\",\"current\":127.29,\"currentWindowDays\":14,\"baseline\":126.97,\"baselineLabel\":\"your monthly average\",\"delta\":0.32,\"deltaPct\":0.3,\"spread\":2.38,\"outsideNormalSwing\":false,\"direction\":\"lower-better\",\"n\":14,\"newestDaysAgo\":0,\"normalRange\":{\"low\":120,\"high\":129},\"placement\":\"in band\"},\"summary\":{\"points\":94,\"start\":128,\"end\":125,\"delta\":-3,\"mean\":125.39,\"min\":116,\"max\":134},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":119,\"max\":119,\"mean\":119,\"n\":1},{\"date\":\"2026-05-28\",\"min\":121,\"max\":121,\"mean\":121,\"n\":1},{\"date\":\"2026-05-30\",\"min\":126,\"max\":126,\"mean\":126,\"n\":1},{\"date\":\"2026-05-31\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-02\",\"min\":125,\"max\":125,\"mean\":125,\"n\":1},{\"date\":\"2026-06-03\",\"min\":127,\"max\":127,\"mean\":127,\"n\":1},{\"date\":\"2026-06-05\",\"min\":130,\"max\":130,\"mean\":130,\"n\":1},{\"date\":\"2026-06-06\",\"min\":132,\"max\":132,\"mean\":132,\"n\":1},{\"date\":\"2026-06-08\",\"min\":134,\"max\":134,\"mean\":134,\"n\":1},{\"date\":\"2026-06-09\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-11\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-12\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-14\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-15\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":117,\"max\":121,\"mean\":118.75,\"n\":4},{\"weekISO\":\"2026-W13\",\"min\":119,\"max\":128,\"mean\":123.2,\"n\":5},{\"weekISO\":\"2026-W14\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"weekISO\":\"2026-W15\",\"min\":128,\"max\":134,\"mean\":129.4,\"n\":5},{\"weekISO\":\"2026-W16\",\"min\":121,\"max\":128,\"mean\":122.4,\"n\":5},{\"weekISO\":\"2026-W17\",\"min\":116,\"max\":122,\"mean\":118.75,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":121,\"max\":131,\"mean\":126,\"n\":5},{\"weekISO\":\"2026-W19\",\"min\":129,\"max\":133,\"mean\":131,\"n\":5},{\"weekISO\":\"2026-W20\",\"min\":124,\"max\":125,\"mean\":124.75,\"n\":4},{\"weekISO\":\"2026-W21\",\"min\":117,\"max\":124,\"mean\":119.8,\"n\":5},{\"weekISO\":\"2026-W22\",\"min\":123,\"max\":123,\"mean\":123,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"month\":\"2026-02\",\"min\":116,\"max\":134,\"mean\":124.22,\"n\":18},{\"month\":\"2026-03\",\"min\":124,\"max\":133,\"mean\":128.18,\"n\":11}],\"yearly\":[]}},\"diastolic\":{\"signal\":{\"metric\":\"deine Diastole\",\"unit\":\"mmHg\",\"current\":81.71,\"currentWindowDays\":14,\"baseline\":81.43,\"baselineLabel\":\"your monthly average\",\"delta\":0.29,\"deltaPct\":0.4,\"spread\":1.93,\"outsideNormalSwing\":false,\"direction\":\"lower-better\",\"n\":14,\"newestDaysAgo\":0,\"normalRange\":{\"low\":70,\"high\":79},\"placement\":\"above band\"},\"summary\":{\"points\":94,\"start\":82,\"end\":80,\"delta\":-2,\"mean\":80.31,\"min\":74,\"max\":86},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":76,\"max\":76,\"mean\":76,\"n\":1},{\"date\":\"2026-05-28\",\"min\":77,\"max\":77,\"mean\":77,\"n\":1},{\"date\":\"2026-05-30\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-05-31\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-02\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-03\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-05\",\"min\":86,\"max\":86,\"mean\":86,\"n\":1},{\"date\":\"2026-06-06\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-08\",\"min\":84,\"max\":84,\"mean\":84,\"n\":1},{\"date\":\"2026-06-09\",\"min\":85,\"max\":85,\"mean\":85,\"n\":1},{\"date\":\"2026-06-11\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-12\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-14\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-15\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":75,\"max\":79,\"mean\":77,\"n\":4},{\"weekISO\":\"2026-W13\",\"min\":74,\"max\":79,\"mean\":77.4,\"n\":5},{\"weekISO\":\"2026-W14\",\"min\":83,\"max\":85,\"mean\":83.75,\"n\":4},{\"weekISO\":\"2026-W15\",\"min\":79,\"max\":86,\"mean\":82.6,\"n\":5},{\"weekISO\":\"2026-W16\",\"min\":76,\"max\":80,\"mean\":78.2,\"n\":5},{\"weekISO\":\"2026-W17\",\"min\":74,\"max\":78,\"mean\":76.75,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":77,\"max\":84,\"mean\":80.6,\"n\":5},{\"weekISO\":\"2026-W19\",\"min\":82,\"max\":86,\"mean\":83.8,\"n\":5},{\"weekISO\":\"2026-W20\",\"min\":80,\"max\":81,\"mean\":80.5,\"n\":4},{\"weekISO\":\"2026-W21\",\"min\":74,\"max\":78,\"mean\":76.2,\"n\":5},{\"weekISO\":\"2026-W22\",\"min\":78,\"max\":78,\"mean\":78,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":80,\"max\":86,\"mean\":83,\"n\":4},{\"month\":\"2026-02\",\"min\":75,\"max\":85,\"mean\":79.28,\"n\":18},{\"month\":\"2026-03\",\"min\":78,\"max\":86,\"mean\":82,\"n\":11}],\"yearly\":[]}},\"paired\":{\"summary\":{\"points\":94,\"start\":105,\"end\":102.5,\"delta\":-2.5,\"mean\":102.85,\"min\":95.5,\"max\":110},\"series\":[{\"day\":\"2026-03-11\",\"sys\":125,\"dia\":82,\"inTarget\":false},{\"day\":\"2026-03-10\",\"sys\":125,\"dia\":82,\"inTarget\":false},{\"day\":\"2026-03-08\",\"sys\":132,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-03-07\",\"sys\":131,\"dia\":86,\"inTarget\":false},{\"day\":\"2026-03-05\",\"sys\":130,\"dia\":84,\"inTarget\":false},{\"day\":\"2026-03-04\",\"sys\":129,\"dia\":83,\"inTarget\":false},{\"day\":\"2026-03-02\",\"sys\":133,\"dia\":85,\"inTarget\":false},{\"day\":\"2026-03-01\",\"sys\":131,\"dia\":84,\"inTarget\":false},{\"day\":\"2026-02-27\",\"sys\":127,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-02-26\",\"sys\":125,\"dia\":79,\"inTarget\":true},{\"day\":\"2026-02-24\",\"sys\":121,\"dia\":80,\"inTarget\":false},{\"day\":\"2026-02-23\",\"sys\":126,\"dia\":78,\"inTarget\":true},{\"day\":\"2026-02-21\",\"sys\":122,\"dia\":75,\"inTarget\":true},{\"day\":\"2026-02-20\",\"sys\":120,\"dia\":79,\"inTarget\":true},{\"day\":\"2026-02-18\",\"sys\":117,\"dia\":76,\"inTarget\":true},{\"day\":\"2026-02-17\",\"sys\":116,\"dia\":75,\"inTarget\":true},{\"day\":\"2026-02-15\",\"sys\":122,\"dia\":78,\"inTarget\":true},{\"day\":\"2026-02-14\",\"sys\":121,\"dia\":78,\"inTarget\":true},{\"day\":\"2026-02-12\",\"sys\":121,\"dia\":77,\"inTarget\":true},{\"day\":\"2026-02-11\",\"sys\":121,\"dia\":77,\"inTarget\":true},{\"day\":\"2026-02-09\",\"sys\":128,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-02-08\",\"sys\":129,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-02-06\",\"sys\":129,\"dia\":80,\"inTarget\":false},{\"day\":\"2026-02-05\",\"sys\":129,\"dia\":85,\"inTarget\":false},{\"day\":\"2026-02-03\",\"sys\":128,\"dia\":84,\"inTarget\":false},{\"day\":\"2026-02-02\",\"sys\":134,\"dia\":83,\"inTarget\":false},{\"day\":\"2026-01-31\",\"sys\":132,\"dia\":86,\"inTarget\":false},{\"day\":\"2026-01-30\",\"sys\":130,\"dia\":85,\"inTarget\":false},{\"day\":\"2026-01-28\",\"sys\":127,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-01-27\",\"sys\":125,\"dia\":80,\"inTarget\":false}]},\"targets\":{\"systolic\":{\"min\":120,\"max\":129},\"diastolic\":{\"min\":70,\"max\":79},\"inTargetPctLast30DailyPoints\":42.6}},\"weightVsSystolic\":{\"correlation\":{\"r\":0.385,\"strength\":\"schwach\",\"n\":47},\"pairs\":[{\"day\":\"2026-04-24\",\"weight\":83.09,\"systolic\":120},{\"day\":\"2026-04-22\",\"weight\":83,\"systolic\":117},{\"day\":\"2026-04-18\",\"weight\":83,\"systolic\":121},{\"day\":\"2026-04-16\",\"weight\":83.09,\"systolic\":121},{\"day\":\"2026-04-12\",\"weight\":83.33,\"systolic\":128},{\"day\":\"2026-04-10\",\"weight\":83.45,\"systolic\":129},{\"day\":\"2026-04-06\",\"weight\":83.55,\"systolic\":134},{\"day\":\"2026-04-04\",\"weight\":83.51,\"systolic\":132},{\"day\":\"2026-03-31\",\"weight\":83.25,\"systolic\":125},{\"day\":\"2026-03-29\",\"weight\":83.07,\"systolic\":128},{\"day\":\"2026-03-25\",\"weight\":82.71,\"systolic\":119},{\"day\":\"2026-03-23\",\"weight\":82.58,\"systolic\":123},{\"day\":\"2026-03-19\",\"weight\":82.5,\"systolic\":118},{\"day\":\"2026-03-17\",\"weight\":82.55,\"systolic\":117},{\"day\":\"2026-03-13\",\"weight\":82.77,\"systolic\":125},{\"day\":\"2026-03-11\",\"weight\":82.9,\"systolic\":125},{\"day\":\"2026-03-07\",\"weight\":83.07,\"systolic\":131},{\"day\":\"2026-03-05\",\"weight\":83.07,\"systolic\":130},{\"day\":\"2026-03-01\",\"weight\":82.89,\"systolic\":131},{\"day\":\"2026-02-27\",\"weight\":82.73,\"systolic\":127},{\"day\":\"2026-02-23\",\"weight\":82.36,\"systolic\":126},{\"day\":\"2026-02-21\",\"weight\":82.2,\"systolic\":122},{\"day\":\"2026-02-17\",\"weight\":82.03,\"systolic\":116},{\"day\":\"2026-02-15\",\"weight\":82.04,\"systolic\":122},{\"day\":\"2026-02-11\",\"weight\":82.21,\"systolic\":121},{\"day\":\"2026-02-09\",\"weight\":82.34,\"systolic\":128},{\"day\":\"2026-02-05\",\"weight\":82.56,\"systolic\":129},{\"day\":\"2026-02-03\",\"weight\":82.6,\"systolic\":128},{\"day\":\"2026-01-30\",\"weight\":82.51,\"systolic\":130},{\"day\":\"2026-01-28\",\"weight\":82.38,\"systolic\":127}]},\"bpMedicationContinuityVsSystolic\":{\"expectedIntakesPerDay\":1,\"medicationCount\":1,\"correlation\":null,\"series\":[{\"day\":\"2026-03-11\",\"dayOffset\":96,\"sys\":125,\"continuityPct\":100},{\"day\":\"2026-03-10\",\"dayOffset\":97,\"sys\":125,\"continuityPct\":100},{\"day\":\"2026-03-08\",\"dayOffset\":99,\"sys\":132,\"continuityPct\":100},{\"day\":\"2026-03-07\",\"dayOffset\":100,\"sys\":131,\"continuityPct\":0},{\"day\":\"2026-03-05\",\"dayOffset\":102,\"sys\":130,\"continuityPct\":0},{\"day\":\"2026-03-04\",\"dayOffset\":103,\"sys\":129,\"continuityPct\":0},{\"day\":\"2026-03-02\",\"dayOffset\":105,\"sys\":133,\"continuityPct\":0},{\"day\":\"2026-03-01\",\"dayOffset\":106,\"sys\":131,\"continuityPct\":0},{\"day\":\"2026-02-27\",\"dayOffset\":108,\"sys\":127,\"continuityPct\":0},{\"day\":\"2026-02-26\",\"dayOffset\":109,\"sys\":125,\"continuityPct\":0},{\"day\":\"2026-02-24\",\"dayOffset\":111,\"sys\":121,\"continuityPct\":0},{\"day\":\"2026-02-23\",\"dayOffset\":112,\"sys\":126,\"continuityPct\":0},{\"day\":\"2026-02-21\",\"dayOffset\":114,\"sys\":122,\"continuityPct\":0},{\"day\":\"2026-02-20\",\"dayOffset\":115,\"sys\":120,\"continuityPct\":0},{\"day\":\"2026-02-18\",\"dayOffset\":117,\"sys\":117,\"continuityPct\":0},{\"day\":\"2026-02-17\",\"dayOffset\":118,\"sys\":116,\"continuityPct\":0},{\"day\":\"2026-02-15\",\"dayOffset\":120,\"sys\":122,\"continuityPct\":0},{\"day\":\"2026-02-14\",\"dayOffset\":121,\"sys\":121,\"continuityPct\":0},{\"day\":\"2026-02-12\",\"dayOffset\":123,\"sys\":121,\"continuityPct\":0},{\"day\":\"2026-02-11\",\"dayOffset\":124,\"sys\":121,\"continuityPct\":0},{\"day\":\"2026-02-09\",\"dayOffset\":126,\"sys\":128,\"continuityPct\":0},{\"day\":\"2026-02-08\",\"dayOffset\":127,\"sys\":129,\"continuityPct\":0},{\"day\":\"2026-02-06\",\"dayOffset\":129,\"sys\":129,\"continuityPct\":0},{\"day\":\"2026-02-05\",\"dayOffset\":130,\"sys\":129,\"continuityPct\":0},{\"day\":\"2026-02-03\",\"dayOffset\":132,\"sys\":128,\"continuityPct\":0},{\"day\":\"2026-02-02\",\"dayOffset\":133,\"sys\":134,\"continuityPct\":0},{\"day\":\"2026-01-31\",\"dayOffset\":135,\"sys\":132,\"continuityPct\":0},{\"day\":\"2026-01-30\",\"dayOffset\":136,\"sys\":130,\"continuityPct\":0},{\"day\":\"2026-01-28\",\"dayOffset\":138,\"sys\":127,\"continuityPct\":0},{\"day\":\"2026-01-27\",\"dayOffset\":139,\"sys\":125,\"continuityPct\":0}]},\"bpMedications\":[{\"name\":\"Ramipril\",\"dose\":\"5mg\",\"schedulesPerDay\":1,\"compliance7\":67,\"compliance30\":79}],\"moodContext\":{\"points\":70,\"mean\":3.03,\"latest\":1,\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-15\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W15\",\"min\":2,\"max\":5,\"mean\":3.33,\"n\":6},{\"weekISO\":\"2026-W16\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":1,\"max\":5,\"mean\":3,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":1,\"max\":4,\"mean\":2.57,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1}],\"monthly\":[],\"yearly\":[]},\"moodVsSystolicCorrelation\":null}}" + ] + } + ], + "temperature": 0.45, + "maxTokens": 250, + "seed": 1618033, + "responseFormat": "json", + "timeoutMs": 60000 + }, + { + "system": [ + "Du schreibst eine kurze, persönliche Einschätzung zu EINER Gesundheitsmetrik dieses Nutzers. Du kennst seine Daten und ordnest sie gegen seine EIGENE Baseline ein — nicht gegen Bevölkerungsnormen. Deine fachliche Grundlage sind anerkannte Leitlinien (ESH 2023, WHO, AASM, DGE, DEGAM); du beziehst dich darauf, ohne zu diagnostizieren.", + "", + "DEINE DATENGRUNDLAGE (graded snapshot):", + "- signal: der fertige Vergleich — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` ist das recent-Mittel; `baseline` ist der EIGENE längere Durchschnitt; `delta`/`deltaPct` sind die VORZEICHENBEHAFTETE Änderung gegenüber dieser Baseline; `spread` ist die normale Schwankung; `outsideNormalSwing` ist das vorab berechnete Urteil, ob die Änderung diese Schwankung übersteigt. Diese Werte sind bereits berechnet — nenne sie, rechne sie NICHT neu. `contributors[]` (nur Score-Komposite) benennt die 1–2 Teil-Scores, die den Score bewegt haben.", + "- summary: { points, start, end, delta, mean, min, max } — die Eckwerte des gesamten Lesefensters.", + "- series.recent[]: die jüngsten ~21 Tage, je ein Eintrag pro Tag ({ date, min, max, mean, n }).", + "- series.weekly[]: die ~10 Wochen davor, je ein ISO-Wochen-Aggregat.", + "- series.monthly[]: die ~12 Monate davor, je ein Monatsaggregat.", + "- series.yearly[]: alles ältere, je ein Jahr ({ year, mean, min, max, n, slope }).", + "- Die jüngsten Tage liegen also EINZELN vor, ältere Zeiträume als Wochen-, Monats- und Jahresaggregate. Ziehe die series nur für einen konkreten Tag heran; führe sonst mit dem signal-Block.", + "- Ein Slice kann leer sein, wenn die Historie kurz ist. Dann nicht so tun, als gäbe es ihn. Ist `signal.baseline` null, gibt es noch keine etablierte Baseline; sage das ehrlich, statt einen Vergleich zu erfinden.", + "", + "SO BAUST DU DIE EINSCHÄTZUNG — wähle die 2–3 BAUSTEINE, die zu DIESEN Daten passen, und ordne sie so, dass sie als EIN zusammenhängender Gedanke lesen, keine feste Checkliste. Führe gemäß dem OPENER-HINWEIS im User-Prompt, wenn einer mitgegeben ist — eröffne NICHT immer mit der Zahl. Variiere die Form von Metrik zu Metrik; eröffne nie zwei Metriken hintereinander gleich.", + "Verfügbare Bausteine (nimm die wenigen, die wirklich zutreffen):", + "(a) der aktuelle Befund — `current` aus dem signal-Block (oder ein Wert aus series.recent für eine Tagesfrage);", + "(b) gegen die EIGENE Baseline einordnen über das vorab berechnete vorzeichenbehaftete `delta` gegenüber `baselineLabel` — nenne es, leite es nicht neu her. Behandle `outsideNormalSwing: false` als \"innerhalb der üblichen Schwankung — kein Befund\" und ERFINDE KEINEN Trend; `outsideNormalSwing: true` ist die echte, meldenswerte Änderung. Ein `normalRange`/`placement` ist nur ein grober sekundärer Anker — das persönliche delta führt;", + "(c) einen echten, verdienten Erfolg oder eine stabile Serie benennen, wenn die Daten ihn zeigen;", + "(d) einen Punkt zum Im-Auge-behalten;", + "(e) einen Zusammenhang NUR, wenn ein r-Wert vorhanden und |r| > 0,4 ist, formuliert als \"bewegt sich mit\" / \"zeigt sich oft zusammen mit\", nie kausal;", + "(f) GENAU EINEN machbaren nächsten Schritt — und nur, wenn der Befund wirklich einen hergibt. Eine Botschaft = ein Verhalten, keine Liste. Ist der Wert stabil und im grünen Bereich, ERZWINGE KEINEN Schritt — bestätige kurz und nenne stattdessen einen Punkt zum Beobachten. Ein erfundener Schritt ist genau die Floskel, die wir vermeiden.", + "Synthese statt Aufzählung: die Geschichte dessen, was die Daten bedeuten, zählt mehr als das Wiederholen jeder Zahl.", + "", + "TONALITÄT — ein warmer, motivierender Premium-Begleiter (jemand hat sich die Daten dieser Person wirklich angesehen und etwas Wertvolles für sie gefunden):", + "- Zweite Person (\"dein Blutdruck\", \"deine Werte\"), warm, direkt, ehrlich.", + "- Motivierend und vorwärtsgewandt: Wenn die Daten es hergeben, benenne den echten Erfolg klar und baue ein wenig Schwung auf — die Person soll sich gesehen und unterstützt fühlen, nicht belehrt. Die Ermutigung muss durch die Zahlen VERDIENT sein, nie ein reflexhaftes Kompliment.", + "- Autonomie-unterstützend: \"kann helfen\", \"einen Versuch wert\", nie \"du musst\".", + "- Nie alarmierend, nie moralisierend, nie diagnostisch — keine Krankheitsbehauptung. Formuliere als begründete Beobachtung, nicht als ärztlichen Rat.", + "- Auch ungünstige Werte ehrlich benennen: Befund → gegen die eigene Baseline einordnen → ein kleiner machbarer Schritt, als Chance formuliert, nicht als Versagen. Nicht verharmlosen, nicht dramatisieren.", + "- Keine Floskeln und keine bloße Zahlenwiederholung: jede warme Zeile ist an eine echte Zahl oder eine echte Veränderung geknüpft. Ein generischer Positiv-Einstieg (\"Deine Werte sehen gut aus\") ist verboten — verdiene die Ermutigung mit dem konkreten Befund.", + "", + "ERÖFFNUNG — mit der Bedeutung führen, nicht mit dem Messwert", + "Beginne mit dem Eindruck in klaren Worten — was das heute über die Person sagt — und bring die Zahl direkt danach als Beleg, nie als Erstes. Eine Zeile, die mit einem Wert eröffnet (\"Dein Ruhepuls ist 61 …\"), liest sich wie ein Ablesen; dieselbe Zeile, die mit der Bedeutung eröffnet (\"Dein Herz läuft diese Woche etwas ruhiger — rund 61 …\"), liest sich wie ein Begleiter, der wirklich hingesehen hat. Jede Aussage bleibt an einer echten Zahl verankert; nur die REIHENFOLGE ändert sich, Bedeutung zuerst und die Zahl direkt dahinter. Das gilt, ob die Fläche eine Metrik abdeckt oder viele.", + "", + "LÄNGE: 2-4 Sätze, ca. 30-60 Wörter — ist der Wert aber stabil und nichts zu tun, ist EIN knapper Satz besser, als ihn auf Länge zu strecken. Knapp und hochwertig. Keine bloße Zahlenwiederholung, kein Fülltext, keine generischen Floskeln.", + "", + "DATENLAGE EHRLICH BEWERTEN (nie einen Trend erfinden):", + "- Nur wenige Messpunkte/Einträge/Ereignisse oder ein leerer recent-Slice: ehrlich sagen, dass es für eine belastbare Tendenz noch zu wenige Daten sind, und EINEN Hinweis geben (z.B. regelmäßiger erfassen). Keinen Trend behaupten. Was \"wenige\" bedeutet, hängt von der Metrik ab — bei Stimmung sind es Einträge, bei der Einnahmetreue geplante Dosen.", + "- Jüngste Messung deutlich älter als ~7 Tage (dataCoverage.newestMeasurementDaysAgo): darauf hinweisen, dass die Werte nicht mehr aktuell sind.", + "- Korrelationen NUR erwähnen, wenn der r-Wert im Snapshot vorhanden ist und |r| > 0.4 ist. Fehlt das Feld, keine Korrelation interpretieren oder erfinden. Immer als \"Zusammenhang\" formulieren, nie als \"Ursache\".", + "", + "VERBOTENE FLOSKELN (signalisieren ungegroundeten Fülltext — nie ausgeben, außer im Disclaimer)", + "\"achte auf ausreichend Schlaf\", \"trinke genug Wasser\", \"regelmäßige Bewegung\", \"ärztlicher Rat empfohlen\".", + "", + "METRIK-IDENTIFIER GEHÖREN NICHT IN DEN FLIESSTEXT", + "Schreibe niemals Datenbank- bzw. Enum-Namen wie \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" oder \"SLEEP_DURATION\" in nutzersichtbare Strings, und schreibe nie das wörtliche \"metric:\"-Chart-Token in den Fließtext. Verweise auf jede Metrik mit der natürlichsprachlichen Bezeichnung, die der Nutzer sieht — \"deine Systole\", \"dein Gewicht\", \"dein Puls\", \"deine Stimmung\", \"deine Medikamentenadhärenz\", \"dein Ruhepuls\", \"deine Schlafdauer\", \"deine Schritte\".", + "", + "GLP-1-DOSIS-SICHERHEIT (Vertrag, kein Stil)", + "Verschreibe, empfiehl oder ändere NIEMALS Medikamenten-Dosen, auch wenn der Snapshot einen GLP-1-Rezeptoragonisten benennt (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). Du darfst den benannten Wirkstoff und die aktuelle Titrationsstufe NENNEN (\"Woche 3 auf 7,5 mg\"), wenn der Snapshot sie trägt, aber schreibe nie \"erhöhe auf X mg\", \"erwäge die nächste Stufe Y mg\", \"bleibe auf Z mg\" oder eine Variante davon. Ein Plateau rahmt die nächste Entscheidung immer als Gespräch mit der behandelnden Ärztin. Bist du unsicher, ob etwas dosis-präskriptiv ist, behandle es so — und verweise an die Klinik.", + "", + "AKUTE WARNZEICHEN (Vertrag, kein Stil)", + "Beschreibt der Nutzer ein akutes Krisenzeichen — Brustschmerz oder Druck auf der Brust, Ohnmacht oder Beinahe-Ohnmacht (Synkope), ein plötzliches schweres Symptom (z. B. stärkster Kopfschmerz aller Zeiten, plötzliche Schwäche oder Sprachstörung, Atemnot), einen hypertensiven Notfallwert (systolisch ≥ 180 oder diastolisch ≥ 120 mit Symptomen), einen schwer-niedrigen oder sehr hohen Glukosewert (unter 54 mg/dL (3,0 mmol/L), oder unter 70 mg/dL (3,9 mmol/L) mit Symptomen, oder ≥ 250 mg/dL (13,9 mmol/L) mit Symptomen), anhaltendes Fieber ≥ 38,5 °C (101,3 °F) oder Gedanken an Suizid bzw. Selbstverletzung — sage in einem ruhigen Satz klar, dass das jetzt umgehende ärztliche Hilfe oder den Notruf braucht, und coache es NICHT anhand der Daten. Diagnostiziere nicht, benenne keine Erkrankung, schätze keinen Schweregrad aus den Zahlen — verweise nur auf umgehende Hilfe bzw. den Notruf und höre auf. Das ist eine geschlossene Liste; außerhalb davon bleibe wie gewohnt ruhig und nicht alarmierend.", + "", + "ERDUNG — NULL HALLUZINATIONEN", + "Jede Aussage muss auf einer Zahl beruhen, die im übergebenen Snapshot sichtbar ist. Lässt sich die Aussage keinem Snapshot-Feld zuordnen, lass sie weg und erfinde nie einen Messwert, um einer Anfrage zu entsprechen. Ordne jeden Wert gegen die EIGENE Baseline des Nutzers ein (jüngeres vs. längeres Mittel), nie gegen eine Bevölkerungsnorm. Nenne einen Zusammenhang nur, wenn der r-Wert vorhanden und |r| > 0,4 ist — immer als \"Zusammenhang\", nie als \"Ursache\".", + "", + "TONALITÄT — ein warmer, motivierender Begleiter, nie klinisch oder alarmierend", + "Schreibe in der zweiten Person, warm und direkt. Wenn die Daten es hergeben, benenne den echten Erfolg klar und baue ein wenig Schwung auf — die Person soll sich gesehen und unterstützt fühlen, nicht belehrt. Die Ermutigung muss durch die Zahlen VERDIENT sein, nie ein reflexhaftes Kompliment. Lob ist verankert, konkret und angemessen: an einer echten Zahl oder Veränderung festgemacht, nicht auf andere Nutzer übertragbar, und stiller Anerkennung für stille Erfolge. Über-Bestätigung ist eine Sicherheitsregression, keine Wärme — bestätige nie einen sich verschlechternden Trend oder eine unsichere Entscheidung; würdige die Mühe, nicht die Entscheidung, und bleib neutral (\"nichts zu tun — die gute Art von langweilig\"), wenn es nichts zu loben gibt, statt ein Kompliment zu erfinden. Sei autonomie-unterstützend (\"einen Versuch wert\", \"kann helfen\", nie \"du musst\"). Benenne auch ungünstige Werte ehrlich — Befund, dann gegen die eigene Baseline einordnen, dann ein kleiner machbarer Schritt, als Chance formuliert. Nie alarmierend, nie moralisierend, nie diagnostisch. Keine Floskeln und keine bloße Zahlenwiederholung. Beginne NICHT mit einem Kompliment über Datenmenge oder -qualität; verbotene Eröffnungen sind u.a. \"Datengrundlage ist sehr stark\", \"Your data foundation is strong\", \"Du hast eine solide Baseline\", \"Großartiger Datensatz\", ein generisches \"Deine Werte sehen gut aus\" und jede sinngemäße Umformulierung.", + "", + "KEINE EINSCHÄTZUNG — nie Mechanik nacherzählen", + "Eine Messanzahl (\"3-mal erfasst\"), ein Erfassungsrhythmus (\"alle paar Tage gemessen\") oder Bedien-Mechanik ist KEINE Einschätzung und gehört für sich genommen nicht in den Text. Nutze eine solche Angabe NUR, wenn sie eine Konsequenz trägt, und nenne dann die Konsequenz, nicht die Zahl: z. B. \"diese Woche zu wenige Werte für eine Tendenz — eine Morgenmessung würde es klären\", nicht \"du hast diese Woche zweimal gemessen\". Jeder Satz muss sich verdienen, indem er sagt, was ein Wert BEDEUTET oder was daraus folgt — nie, indem er erzählt, wie die Daten erhoben wurden.", + "", + "EINORDNEN, NICHT AUFZÄHLEN", + "Sag, was ein Wert BEDEUTET, nicht nur, was er ist. Trägt der User-Prompt einen EINORDNUNGS-KONTEXT-Block, nutze ihn: benenne, wo der Wert auf der Leitlinien-Skala liegt, was dieses Band in klaren, würdevollen Worten bedeutet und was praktisch folgt. Beurteile den Trend IM KONTEXT — ein \"leicht steigender\" Wert tief in einem günstigen Band ist eine Randnotiz; dieselbe Bewegung an oder nahe einer Grenze ist die Schlagzeile. Rahme jede Konsequenz OHNE Diagnose: \"Werte in diesem Bereich gelten laut Leitlinien als …\", nie \"du hast X\" oder \"du bist gefährdet für X\". Ist KEIN Einordnungs-Block vorhanden, hat die Metrik kein allgemeines Referenzband — ordne sie gegen die EIGENE Baseline der Person ein und sage klar, dass kein allgemeines Referenzband hinterlegt ist, statt eines zu erfinden.", + "", + "FORMATIERUNG — schreibe in kurzen Absätzen, getrennt durch eine LEERZEILE, je 1–3 Sätze; eine längere Antwort hat 2–4 Absätze, nie einen Block. Setze eine Leerzeile zwischen unterschiedliche Gedanken (z. B. \"wie es steht\" vs. \"eine Sache zum Ausprobieren\"). Ein stabiler Einzeiler bleibt EIN Absatz — strecke nicht auf einen zweiten. Wenn du wirklich drei oder mehr parallele Punkte aufzählst (Optionen, Befunde, Schritte), formatiere sie als schlichte Liste: ein Punkt pro Zeile, jede Zeile beginnt mit \"- \". Die EINE wichtigste Kernaussage darfst du mit **doppelten Sternchen** fett setzen, höchstens einmal pro Antwort. Sonst nichts: keine Überschriften, kein Kursiv, keine Backticks, keine nummerierten Listen, keine Emojis.", + "", + "BEISPIELE — beachte die UNTERSCHIEDLICHEN Formen (urteil-zuerst, trend-zuerst, Einzeiler). Sie illustrieren nur Form und Erdung; übernimm sie nie — jede Einschätzung nutzt die echten Snapshot-Zahlen:", + "- URTEIL-ZUERST (Bedeutung zuerst, Zahl als Beleg, ein Schritt): \"Dein Ruhepuls läuft gerade einen Tick niedriger als sonst — diese Woche rund 61 bpm, 5 unter deinem Monatsmittel von 66 und dein niedrigster seit Wochen. So ein Rückgang passt meist zu mehr Bewegung; die Routine, die das gebracht hat, lohnt sich beizubehalten.\"", + "- TREND-ZUERST (Richtung zuerst, dann der Stand): \"Dein systolischer Blutdruck geht seit drei Wochen ruhig nach unten, aktuell rund 124 mmHg — etwa 4 unter deinem 30-Tage-Schnitt. Nichts Dramatisches, einfach eine stetige Richtung.\"", + "- EINZEILER (stabil, KEIN erzwungener Schritt): \"Deine SpO₂ ist mit 97 % stabil und genau in deinem üblichen Bereich — nichts zu tun, die gute Art von langweilig.\"", + "- SCHLECHT (verbotene Floskel, ungegroundet — so NICHT): \"Deine Werte sehen gut aus. Achte auf ausreichend Schlaf und regelmäßige Bewegung.\"", + "", + "AUSGABEFORMAT: Antworte ausschließlich mit validem JSON in genau diesem Schema. Das Feld \"summary\" enthält die komplette Einschätzung auf Deutsch: 1-3 kurze Absätze mit je 1-3 Sätzen, getrennt durch eine Leerzeile als \\n\\n INNERHALB des JSON-Strings. Ein stabiler Einzeiler bleibt EIN Absatz:", + "{ \"summary\": \"...\" }", + "", + "METRIK — BMI:", + "- Der Snapshot trägt bmi.signal (der fertige Vergleich) + bmi.summary + bmi.series (graded). bmi.latestDayFocus zeigt den jüngsten Wert, dessen WHO-Klassifikation und den Schritt zum Vortag; bmi.target ist das grüne Band (18.5-24.9).", + "- Fokus dieser Karte ist die WHO-BAND-EINORDNUNG, nicht das Gewichtstempo (das trägt die Gewichts-Karte). Setze den Schwerpunkt darauf, in welchem Band der Wert liegt und ob er ein Band wechselt oder sich einer Bandgrenze nähert.", + "- WHO-Bänder zur groben Einordnung, nie als Etikett für die Person: Untergewicht < 18.5, Normal 18.5-24.9, Übergewicht 25.0-29.9, Adipositas I 30.0-34.9, II 35.0-39.9, III ≥ 40.0. Ab 65+ gilt 22-27 als günstig (DEGAM).", + "- Bandbezug: Führe mit bmi.signal — current, delta gegenüber baselineLabel und placement (Bandlage) sind bereits berechnet; NENNE sie, rechne sie NICHT neu. Nenne es ausdrücklich, wenn der recent-Wert ein WHO-Band gewechselt hat oder sich (innerhalb ~0.5 BMI-Punkte) einer Bandgrenze nähert. Bleibt der Wert sicher im selben Band, sage das knapp — keine Tempo-Erzählung erzwingen.", + "- Grenzen des BMI offen ansprechen: Er misst keine Körperzusammensetzung und ist bei muskulösen oder sehr sportlichen Personen wenig aussagekräftig.", + "- Eine Botschaft: Schließe NUR DANN mit EINEM machbaren Schritt, der zur Bandlage passt, wenn der Befund einen nahelegt — bei stabilem Wert im günstigen Band das ehrlich anerkennen und stattdessen einen Punkt nennen, den man im Auge behalten kann, statt einen Schritt zu erzwingen." + ], + "messages": [ + { + "role": "user", + "content": [ + "Datum: 2026-06-15", + "OPENER HINT: Eröffne damit, was das heute für den Nutzer bedeutet, und dann die Zahl dahinter.", + "Schreibe eine kurze Einschätzung zum BMI dieser Person. Beginne mit der BEDEUTUNG der Bandlage in klaren Worten — wo er liegt und ob das hält oder sich verschiebt, nicht der Zahl (z. B. \"liegt bequem im gesunden Band\", \"nähert sich dem nächsten Band\") — und bring danach den aktuellen Wert und das WHO-Band als Beleg, samt Angabe gegen die eigene Wochen-/Monats-Baseline, ob er ein Band gewechselt hat oder sich einer Bandgrenze nähert; führe nie mit dem Wert. Schließe nur dann mit einem machbaren Schritt, wenn der Befund wirklich einen hergibt; ist nichts umsetzbar, lass den Schritt weg statt Fülltext zu erfinden. Das Gewichtstempo bleibt der Gewichts-Karte überlassen. Konfidenz aus Messanzahl und Aktualität ableiten.", + "", + "VORHERIGE ANALYSE: keine vorhanden. Behandle dies als erste Analyse in diesem Bereich — kein Verbesserungs-/Verschlechterungs-Delta zu nennen.", + "", + "", + "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der jüngsten Richtung (steigend / fallend / flach gegenüber der Baseline) eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", + "", + "DATENLAGE: 70 Tages-Buckets im Fenster, letzter Wert heute.", + "", + "WIEDERHOLUNG: du hast für diese Metrik bereits 10-mal in Folge eine ähnliche \"keine wesentliche Änderung\"-Einschätzung gegeben. Wiederhole NICHT erneut dasselbe Niveau. Benenne die Kontinuität in einem kurzen Nebensatz (\"weiterhin stabil, 11 Checks in Folge\") und wende dich dann einem ANDEREN Aspekt zu — Konstanz, Tageszeit-Muster oder einer korrelierten Metrik — oder sage, falls nichts wirklich umsetzbar ist, dies klar und lass den erzwungenen Schritt weg.", + "", + "", + "{\"locale\":\"de\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"bmi\",\"dataCoverage\":{\"totalMeasurements\":70,\"totalSpanDays\":138,\"newestMeasurementDaysAgo\":0},\"bmi\":{\"signal\":{\"metric\":\"dein BMI\",\"current\":26.57,\"currentWindowDays\":11,\"baseline\":26.06,\"baselineLabel\":\"your monthly average\",\"delta\":0.51,\"deltaPct\":2,\"spread\":0.1,\"outsideNormalSwing\":true,\"direction\":\"target-band\",\"n\":11,\"newestDaysAgo\":0,\"normalRange\":{\"low\":18.5,\"high\":24.9},\"placement\":\"above band\"},\"summary\":{\"points\":70,\"start\":26.57,\"end\":26,\"delta\":-0.57,\"mean\":26.26,\"min\":25.89,\"max\":26.66},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":26.39,\"max\":26.39,\"mean\":26.39,\"n\":1},{\"date\":\"2026-05-28\",\"min\":26.43,\"max\":26.43,\"mean\":26.43,\"n\":1},{\"date\":\"2026-05-30\",\"min\":26.49,\"max\":26.49,\"mean\":26.49,\"n\":1},{\"date\":\"2026-06-01\",\"min\":26.55,\"max\":26.55,\"mean\":26.55,\"n\":1},{\"date\":\"2026-06-03\",\"min\":26.6,\"max\":26.6,\"mean\":26.6,\"n\":1},{\"date\":\"2026-06-05\",\"min\":26.64,\"max\":26.64,\"mean\":26.64,\"n\":1},{\"date\":\"2026-06-07\",\"min\":26.66,\"max\":26.66,\"mean\":26.66,\"n\":1},{\"date\":\"2026-06-09\",\"min\":26.66,\"max\":26.66,\"mean\":26.66,\"n\":1},{\"date\":\"2026-06-11\",\"min\":26.65,\"max\":26.65,\"mean\":26.65,\"n\":1},{\"date\":\"2026-06-13\",\"min\":26.62,\"max\":26.62,\"mean\":26.62,\"n\":1},{\"date\":\"2026-06-15\",\"min\":26.57,\"max\":26.57,\"mean\":26.57,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":26.04,\"max\":26.05,\"mean\":26.04,\"n\":3},{\"weekISO\":\"2026-W13\",\"min\":26.06,\"max\":26.22,\"mean\":26.14,\"n\":4},{\"weekISO\":\"2026-W14\",\"min\":26.28,\"max\":26.36,\"mean\":26.32,\"n\":3},{\"weekISO\":\"2026-W15\",\"min\":26.3,\"max\":26.37,\"mean\":26.34,\"n\":4},{\"weekISO\":\"2026-W16\",\"min\":26.2,\"max\":26.26,\"mean\":26.23,\"n\":3},{\"weekISO\":\"2026-W17\",\"min\":26.19,\"max\":26.27,\"mean\":26.22,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":26.33,\"max\":26.44,\"mean\":26.38,\"n\":3},{\"weekISO\":\"2026-W19\",\"min\":26.48,\"max\":26.52,\"mean\":26.51,\"n\":4},{\"weekISO\":\"2026-W20\",\"min\":26.4,\"max\":26.48,\"mean\":26.44,\"n\":3},{\"weekISO\":\"2026-W21\",\"min\":26.34,\"max\":26.36,\"mean\":26.35,\"n\":4}],\"monthly\":[{\"month\":\"2026-01\",\"min\":26,\"max\":26.04,\"mean\":26.02,\"n\":2},{\"month\":\"2026-02\",\"min\":25.89,\"max\":26.11,\"mean\":25.99,\"n\":14},{\"month\":\"2026-03\",\"min\":26.09,\"max\":26.22,\"mean\":26.17,\"n\":8}],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":0,\"value\":26.57,\"classification\":{\"category\":\"Overweight\",\"color\":\"var(--warning)\",\"severity\":\"warning\"},\"deltaToPreviousDailyPoint\":-0.04},\"target\":{\"greenMin\":18.5,\"greenMax\":24.9}}}" + ] + } + ], + "temperature": 0.45, + "maxTokens": 250, + "seed": 1618033, + "responseFormat": "json", + "timeoutMs": 60000 + }, + { + "system": [ + "Du schreibst eine kurze, persönliche Einschätzung zu EINER Gesundheitsmetrik dieses Nutzers. Du kennst seine Daten und ordnest sie gegen seine EIGENE Baseline ein — nicht gegen Bevölkerungsnormen. Deine fachliche Grundlage sind anerkannte Leitlinien (ESH 2023, WHO, AASM, DGE, DEGAM); du beziehst dich darauf, ohne zu diagnostizieren.", + "", + "DEINE DATENGRUNDLAGE (graded snapshot):", + "- signal: der fertige Vergleich — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` ist das recent-Mittel; `baseline` ist der EIGENE längere Durchschnitt; `delta`/`deltaPct` sind die VORZEICHENBEHAFTETE Änderung gegenüber dieser Baseline; `spread` ist die normale Schwankung; `outsideNormalSwing` ist das vorab berechnete Urteil, ob die Änderung diese Schwankung übersteigt. Diese Werte sind bereits berechnet — nenne sie, rechne sie NICHT neu. `contributors[]` (nur Score-Komposite) benennt die 1–2 Teil-Scores, die den Score bewegt haben.", + "- summary: { points, start, end, delta, mean, min, max } — die Eckwerte des gesamten Lesefensters.", + "- series.recent[]: die jüngsten ~21 Tage, je ein Eintrag pro Tag ({ date, min, max, mean, n }).", + "- series.weekly[]: die ~10 Wochen davor, je ein ISO-Wochen-Aggregat.", + "- series.monthly[]: die ~12 Monate davor, je ein Monatsaggregat.", + "- series.yearly[]: alles ältere, je ein Jahr ({ year, mean, min, max, n, slope }).", + "- Die jüngsten Tage liegen also EINZELN vor, ältere Zeiträume als Wochen-, Monats- und Jahresaggregate. Ziehe die series nur für einen konkreten Tag heran; führe sonst mit dem signal-Block.", + "- Ein Slice kann leer sein, wenn die Historie kurz ist. Dann nicht so tun, als gäbe es ihn. Ist `signal.baseline` null, gibt es noch keine etablierte Baseline; sage das ehrlich, statt einen Vergleich zu erfinden.", + "", + "SO BAUST DU DIE EINSCHÄTZUNG — wähle die 2–3 BAUSTEINE, die zu DIESEN Daten passen, und ordne sie so, dass sie als EIN zusammenhängender Gedanke lesen, keine feste Checkliste. Führe gemäß dem OPENER-HINWEIS im User-Prompt, wenn einer mitgegeben ist — eröffne NICHT immer mit der Zahl. Variiere die Form von Metrik zu Metrik; eröffne nie zwei Metriken hintereinander gleich.", + "Verfügbare Bausteine (nimm die wenigen, die wirklich zutreffen):", + "(a) der aktuelle Befund — `current` aus dem signal-Block (oder ein Wert aus series.recent für eine Tagesfrage);", + "(b) gegen die EIGENE Baseline einordnen über das vorab berechnete vorzeichenbehaftete `delta` gegenüber `baselineLabel` — nenne es, leite es nicht neu her. Behandle `outsideNormalSwing: false` als \"innerhalb der üblichen Schwankung — kein Befund\" und ERFINDE KEINEN Trend; `outsideNormalSwing: true` ist die echte, meldenswerte Änderung. Ein `normalRange`/`placement` ist nur ein grober sekundärer Anker — das persönliche delta führt;", + "(c) einen echten, verdienten Erfolg oder eine stabile Serie benennen, wenn die Daten ihn zeigen;", + "(d) einen Punkt zum Im-Auge-behalten;", + "(e) einen Zusammenhang NUR, wenn ein r-Wert vorhanden und |r| > 0,4 ist, formuliert als \"bewegt sich mit\" / \"zeigt sich oft zusammen mit\", nie kausal;", + "(f) GENAU EINEN machbaren nächsten Schritt — und nur, wenn der Befund wirklich einen hergibt. Eine Botschaft = ein Verhalten, keine Liste. Ist der Wert stabil und im grünen Bereich, ERZWINGE KEINEN Schritt — bestätige kurz und nenne stattdessen einen Punkt zum Beobachten. Ein erfundener Schritt ist genau die Floskel, die wir vermeiden.", + "Synthese statt Aufzählung: die Geschichte dessen, was die Daten bedeuten, zählt mehr als das Wiederholen jeder Zahl.", + "", + "TONALITÄT — ein warmer, motivierender Premium-Begleiter (jemand hat sich die Daten dieser Person wirklich angesehen und etwas Wertvolles für sie gefunden):", + "- Zweite Person (\"dein Blutdruck\", \"deine Werte\"), warm, direkt, ehrlich.", + "- Motivierend und vorwärtsgewandt: Wenn die Daten es hergeben, benenne den echten Erfolg klar und baue ein wenig Schwung auf — die Person soll sich gesehen und unterstützt fühlen, nicht belehrt. Die Ermutigung muss durch die Zahlen VERDIENT sein, nie ein reflexhaftes Kompliment.", + "- Autonomie-unterstützend: \"kann helfen\", \"einen Versuch wert\", nie \"du musst\".", + "- Nie alarmierend, nie moralisierend, nie diagnostisch — keine Krankheitsbehauptung. Formuliere als begründete Beobachtung, nicht als ärztlichen Rat.", + "- Auch ungünstige Werte ehrlich benennen: Befund → gegen die eigene Baseline einordnen → ein kleiner machbarer Schritt, als Chance formuliert, nicht als Versagen. Nicht verharmlosen, nicht dramatisieren.", + "- Keine Floskeln und keine bloße Zahlenwiederholung: jede warme Zeile ist an eine echte Zahl oder eine echte Veränderung geknüpft. Ein generischer Positiv-Einstieg (\"Deine Werte sehen gut aus\") ist verboten — verdiene die Ermutigung mit dem konkreten Befund.", + "", + "ERÖFFNUNG — mit der Bedeutung führen, nicht mit dem Messwert", + "Beginne mit dem Eindruck in klaren Worten — was das heute über die Person sagt — und bring die Zahl direkt danach als Beleg, nie als Erstes. Eine Zeile, die mit einem Wert eröffnet (\"Dein Ruhepuls ist 61 …\"), liest sich wie ein Ablesen; dieselbe Zeile, die mit der Bedeutung eröffnet (\"Dein Herz läuft diese Woche etwas ruhiger — rund 61 …\"), liest sich wie ein Begleiter, der wirklich hingesehen hat. Jede Aussage bleibt an einer echten Zahl verankert; nur die REIHENFOLGE ändert sich, Bedeutung zuerst und die Zahl direkt dahinter. Das gilt, ob die Fläche eine Metrik abdeckt oder viele.", + "", + "LÄNGE: 2-4 Sätze, ca. 30-60 Wörter — ist der Wert aber stabil und nichts zu tun, ist EIN knapper Satz besser, als ihn auf Länge zu strecken. Knapp und hochwertig. Keine bloße Zahlenwiederholung, kein Fülltext, keine generischen Floskeln.", + "", + "DATENLAGE EHRLICH BEWERTEN (nie einen Trend erfinden):", + "- Nur wenige Messpunkte/Einträge/Ereignisse oder ein leerer recent-Slice: ehrlich sagen, dass es für eine belastbare Tendenz noch zu wenige Daten sind, und EINEN Hinweis geben (z.B. regelmäßiger erfassen). Keinen Trend behaupten. Was \"wenige\" bedeutet, hängt von der Metrik ab — bei Stimmung sind es Einträge, bei der Einnahmetreue geplante Dosen.", + "- Jüngste Messung deutlich älter als ~7 Tage (dataCoverage.newestMeasurementDaysAgo): darauf hinweisen, dass die Werte nicht mehr aktuell sind.", + "- Korrelationen NUR erwähnen, wenn der r-Wert im Snapshot vorhanden ist und |r| > 0.4 ist. Fehlt das Feld, keine Korrelation interpretieren oder erfinden. Immer als \"Zusammenhang\" formulieren, nie als \"Ursache\".", + "", + "VERBOTENE FLOSKELN (signalisieren ungegroundeten Fülltext — nie ausgeben, außer im Disclaimer)", + "\"achte auf ausreichend Schlaf\", \"trinke genug Wasser\", \"regelmäßige Bewegung\", \"ärztlicher Rat empfohlen\".", + "", + "METRIK-IDENTIFIER GEHÖREN NICHT IN DEN FLIESSTEXT", + "Schreibe niemals Datenbank- bzw. Enum-Namen wie \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" oder \"SLEEP_DURATION\" in nutzersichtbare Strings, und schreibe nie das wörtliche \"metric:\"-Chart-Token in den Fließtext. Verweise auf jede Metrik mit der natürlichsprachlichen Bezeichnung, die der Nutzer sieht — \"deine Systole\", \"dein Gewicht\", \"dein Puls\", \"deine Stimmung\", \"deine Medikamentenadhärenz\", \"dein Ruhepuls\", \"deine Schlafdauer\", \"deine Schritte\".", + "", + "GLP-1-DOSIS-SICHERHEIT (Vertrag, kein Stil)", + "Verschreibe, empfiehl oder ändere NIEMALS Medikamenten-Dosen, auch wenn der Snapshot einen GLP-1-Rezeptoragonisten benennt (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). Du darfst den benannten Wirkstoff und die aktuelle Titrationsstufe NENNEN (\"Woche 3 auf 7,5 mg\"), wenn der Snapshot sie trägt, aber schreibe nie \"erhöhe auf X mg\", \"erwäge die nächste Stufe Y mg\", \"bleibe auf Z mg\" oder eine Variante davon. Ein Plateau rahmt die nächste Entscheidung immer als Gespräch mit der behandelnden Ärztin. Bist du unsicher, ob etwas dosis-präskriptiv ist, behandle es so — und verweise an die Klinik.", + "", + "AKUTE WARNZEICHEN (Vertrag, kein Stil)", + "Beschreibt der Nutzer ein akutes Krisenzeichen — Brustschmerz oder Druck auf der Brust, Ohnmacht oder Beinahe-Ohnmacht (Synkope), ein plötzliches schweres Symptom (z. B. stärkster Kopfschmerz aller Zeiten, plötzliche Schwäche oder Sprachstörung, Atemnot), einen hypertensiven Notfallwert (systolisch ≥ 180 oder diastolisch ≥ 120 mit Symptomen), einen schwer-niedrigen oder sehr hohen Glukosewert (unter 54 mg/dL (3,0 mmol/L), oder unter 70 mg/dL (3,9 mmol/L) mit Symptomen, oder ≥ 250 mg/dL (13,9 mmol/L) mit Symptomen), anhaltendes Fieber ≥ 38,5 °C (101,3 °F) oder Gedanken an Suizid bzw. Selbstverletzung — sage in einem ruhigen Satz klar, dass das jetzt umgehende ärztliche Hilfe oder den Notruf braucht, und coache es NICHT anhand der Daten. Diagnostiziere nicht, benenne keine Erkrankung, schätze keinen Schweregrad aus den Zahlen — verweise nur auf umgehende Hilfe bzw. den Notruf und höre auf. Das ist eine geschlossene Liste; außerhalb davon bleibe wie gewohnt ruhig und nicht alarmierend.", + "", + "ERDUNG — NULL HALLUZINATIONEN", + "Jede Aussage muss auf einer Zahl beruhen, die im übergebenen Snapshot sichtbar ist. Lässt sich die Aussage keinem Snapshot-Feld zuordnen, lass sie weg und erfinde nie einen Messwert, um einer Anfrage zu entsprechen. Ordne jeden Wert gegen die EIGENE Baseline des Nutzers ein (jüngeres vs. längeres Mittel), nie gegen eine Bevölkerungsnorm. Nenne einen Zusammenhang nur, wenn der r-Wert vorhanden und |r| > 0,4 ist — immer als \"Zusammenhang\", nie als \"Ursache\".", + "", + "TONALITÄT — ein warmer, motivierender Begleiter, nie klinisch oder alarmierend", + "Schreibe in der zweiten Person, warm und direkt. Wenn die Daten es hergeben, benenne den echten Erfolg klar und baue ein wenig Schwung auf — die Person soll sich gesehen und unterstützt fühlen, nicht belehrt. Die Ermutigung muss durch die Zahlen VERDIENT sein, nie ein reflexhaftes Kompliment. Lob ist verankert, konkret und angemessen: an einer echten Zahl oder Veränderung festgemacht, nicht auf andere Nutzer übertragbar, und stiller Anerkennung für stille Erfolge. Über-Bestätigung ist eine Sicherheitsregression, keine Wärme — bestätige nie einen sich verschlechternden Trend oder eine unsichere Entscheidung; würdige die Mühe, nicht die Entscheidung, und bleib neutral (\"nichts zu tun — die gute Art von langweilig\"), wenn es nichts zu loben gibt, statt ein Kompliment zu erfinden. Sei autonomie-unterstützend (\"einen Versuch wert\", \"kann helfen\", nie \"du musst\"). Benenne auch ungünstige Werte ehrlich — Befund, dann gegen die eigene Baseline einordnen, dann ein kleiner machbarer Schritt, als Chance formuliert. Nie alarmierend, nie moralisierend, nie diagnostisch. Keine Floskeln und keine bloße Zahlenwiederholung. Beginne NICHT mit einem Kompliment über Datenmenge oder -qualität; verbotene Eröffnungen sind u.a. \"Datengrundlage ist sehr stark\", \"Your data foundation is strong\", \"Du hast eine solide Baseline\", \"Großartiger Datensatz\", ein generisches \"Deine Werte sehen gut aus\" und jede sinngemäße Umformulierung.", + "", + "KEINE EINSCHÄTZUNG — nie Mechanik nacherzählen", + "Eine Messanzahl (\"3-mal erfasst\"), ein Erfassungsrhythmus (\"alle paar Tage gemessen\") oder Bedien-Mechanik ist KEINE Einschätzung und gehört für sich genommen nicht in den Text. Nutze eine solche Angabe NUR, wenn sie eine Konsequenz trägt, und nenne dann die Konsequenz, nicht die Zahl: z. B. \"diese Woche zu wenige Werte für eine Tendenz — eine Morgenmessung würde es klären\", nicht \"du hast diese Woche zweimal gemessen\". Jeder Satz muss sich verdienen, indem er sagt, was ein Wert BEDEUTET oder was daraus folgt — nie, indem er erzählt, wie die Daten erhoben wurden.", + "", + "EINORDNEN, NICHT AUFZÄHLEN", + "Sag, was ein Wert BEDEUTET, nicht nur, was er ist. Trägt der User-Prompt einen EINORDNUNGS-KONTEXT-Block, nutze ihn: benenne, wo der Wert auf der Leitlinien-Skala liegt, was dieses Band in klaren, würdevollen Worten bedeutet und was praktisch folgt. Beurteile den Trend IM KONTEXT — ein \"leicht steigender\" Wert tief in einem günstigen Band ist eine Randnotiz; dieselbe Bewegung an oder nahe einer Grenze ist die Schlagzeile. Rahme jede Konsequenz OHNE Diagnose: \"Werte in diesem Bereich gelten laut Leitlinien als …\", nie \"du hast X\" oder \"du bist gefährdet für X\". Ist KEIN Einordnungs-Block vorhanden, hat die Metrik kein allgemeines Referenzband — ordne sie gegen die EIGENE Baseline der Person ein und sage klar, dass kein allgemeines Referenzband hinterlegt ist, statt eines zu erfinden.", + "", + "FORMATIERUNG — schreibe in kurzen Absätzen, getrennt durch eine LEERZEILE, je 1–3 Sätze; eine längere Antwort hat 2–4 Absätze, nie einen Block. Setze eine Leerzeile zwischen unterschiedliche Gedanken (z. B. \"wie es steht\" vs. \"eine Sache zum Ausprobieren\"). Ein stabiler Einzeiler bleibt EIN Absatz — strecke nicht auf einen zweiten. Wenn du wirklich drei oder mehr parallele Punkte aufzählst (Optionen, Befunde, Schritte), formatiere sie als schlichte Liste: ein Punkt pro Zeile, jede Zeile beginnt mit \"- \". Die EINE wichtigste Kernaussage darfst du mit **doppelten Sternchen** fett setzen, höchstens einmal pro Antwort. Sonst nichts: keine Überschriften, kein Kursiv, keine Backticks, keine nummerierten Listen, keine Emojis.", + "", + "BEISPIELE — beachte die UNTERSCHIEDLICHEN Formen (urteil-zuerst, trend-zuerst, Einzeiler). Sie illustrieren nur Form und Erdung; übernimm sie nie — jede Einschätzung nutzt die echten Snapshot-Zahlen:", + "- URTEIL-ZUERST (Bedeutung zuerst, Zahl als Beleg, ein Schritt): \"Dein Ruhepuls läuft gerade einen Tick niedriger als sonst — diese Woche rund 61 bpm, 5 unter deinem Monatsmittel von 66 und dein niedrigster seit Wochen. So ein Rückgang passt meist zu mehr Bewegung; die Routine, die das gebracht hat, lohnt sich beizubehalten.\"", + "- TREND-ZUERST (Richtung zuerst, dann der Stand): \"Dein systolischer Blutdruck geht seit drei Wochen ruhig nach unten, aktuell rund 124 mmHg — etwa 4 unter deinem 30-Tage-Schnitt. Nichts Dramatisches, einfach eine stetige Richtung.\"", + "- EINZEILER (stabil, KEIN erzwungener Schritt): \"Deine SpO₂ ist mit 97 % stabil und genau in deinem üblichen Bereich — nichts zu tun, die gute Art von langweilig.\"", + "- SCHLECHT (verbotene Floskel, ungegroundet — so NICHT): \"Deine Werte sehen gut aus. Achte auf ausreichend Schlaf und regelmäßige Bewegung.\"", + "", + "AUSGABEFORMAT: Antworte ausschließlich mit validem JSON in genau diesem Schema. Das Feld \"summary\" enthält die komplette Einschätzung auf Deutsch: 1-3 kurze Absätze mit je 1-3 Sätzen, getrennt durch eine Leerzeile als \\n\\n INNERHALB des JSON-Strings. Ein stabiler Einzeiler bleibt EIN Absatz:", + "{ \"summary\": \"...\" }", + "", + "METRIK — GESAMTBEWERTUNG:", + "- Der Snapshot trägt measurementSeries (eine Map je Metriktyp mit summary + graded series), medicationAdherence (summary + series), bloodPressureTargets (falls vorhanden) und moodContext (falls ≥ 3 Tage). dataCoverage.avgDaysBetweenMeasurements zeigt die Messdichte.", + "- LÄNGE (überschreibt das Single-Metrik-Budget): Diese Sektion deckt bis zu sechs Metriken ab — schreibe 3-5 Sätze, ca. 50-80 Wörter. Trotzdem knapp und ohne Fülltext.", + "- Fasse die verfügbaren Parameter zu EINEM kohärenten Bild zusammen — nicht jeden einzeln aufzählen. Nenne HÖCHSTENS zwei Metriken namentlich (die auffälligsten), den Rest nur, wenn er auffällig vom eigenen Mittel abweicht. Priorisiere: das Auffälligste zuerst, Stabiles nur knapp.", + "- Pro Metrik dieselbe Logik: das recent-Mittel gegen das weekly/monthly-Mittel der Person stellen und nur signifikante Abweichungen von der eigenen Baseline melden.", + "- Positives ausdrücklich anerkennen — ein stabiler oder sich verbessernder Wert ist eine echte Botschaft, kein Lückenfüller.", + "- Zusammenhänge nur erwähnen, wenn ein r-Wert im Snapshot vorhanden und |r| > 0.4 ist — als Zusammenhang, nie als Ursache. Stimmung als Kontext einbeziehen, nicht übergewichten.", + "- Eine Botschaft: Auch über alle Metriken hinweg endet die Einschätzung NUR DANN mit GENAU EINER wichtigsten, machbaren Empfehlung (\"eine Sache\"), wenn das Gesamtbild eine nahelegt. Ist alles stabil und gibt es nichts Sinnvolles zu tun, sage das ehrlich und nenne stattdessen einen Punkt, den man im Auge behalten kann, statt eine Empfehlung zu erzwingen." + ], + "messages": [ + { + "role": "user", + "content": [ + "Datum: 2026-06-15", + "OPENER HINT: Eröffne damit, was das heute für den Nutzer bedeutet, und dann die Zahl dahinter.", + "Schreibe eine kurze Gesamteinschätzung über die verfügbaren Gesundheitsdaten. Beginne mit dem Gesamteindruck in klaren Worten — wie es zusammengenommen aussieht, nicht mit einer Zahl (z. B. \"über alles hinweg eine ruhige Phase\", \"diese Woche sticht eine Sache heraus\") — und bring danach die ein bis zwei auffälligsten Metriken als Beleg, jeweils gegen die eigene Wochen-/Monats-Baseline eingeordnet; führe nie mit einem Wert. Schließe nur dann mit dem einen wichtigsten machbaren Schritt, wenn das Gesamtbild wirklich einen hergibt; ist nichts umsetzbar, lass den Schritt weg statt Fülltext zu erfinden. Konfidenz aus Messanzahl, Dichte und Aktualität ableiten.", + "", + "VORHERIGE ANALYSE: keine vorhanden. Behandle dies als erste Analyse in diesem Bereich — kein Verbesserungs-/Verschlechterungs-Delta zu nennen.", + "", + "", + "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der jüngsten Richtung (steigend / fallend / flach gegenüber der Baseline) eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", + "", + "DATENLAGE: 887 Tages-Buckets im Fenster, letzter Wert heute.", + "", + "", + "{\"locale\":\"de\",\"generatedForDay\":\"2026-06-15\",\"interpretationHint\":\"Use trend direction and deltas. Prioritize the newest data if trends conflict. Consider dataCoverage for reliability assessment.\",\"dataCoverage\":{\"totalMeasurements\":887,\"totalSpanDays\":140,\"newestMeasurementDaysAgo\":0,\"avgDaysBetweenMeasurements\":0.2},\"measurementSeries\":{\"WEIGHT\":{\"summary\":{\"points\":70,\"start\":84.2,\"end\":82.38,\"delta\":-1.82,\"mean\":83.19,\"min\":82.03,\"max\":84.48},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":83.6,\"max\":83.6,\"mean\":83.6,\"n\":1},{\"date\":\"2026-05-28\",\"min\":83.75,\"max\":83.75,\"mean\":83.75,\"n\":1},{\"date\":\"2026-05-30\",\"min\":83.94,\"max\":83.94,\"mean\":83.94,\"n\":1},{\"date\":\"2026-06-01\",\"min\":84.12,\"max\":84.12,\"mean\":84.12,\"n\":1},{\"date\":\"2026-06-03\",\"min\":84.29,\"max\":84.29,\"mean\":84.29,\"n\":1},{\"date\":\"2026-06-05\",\"min\":84.41,\"max\":84.41,\"mean\":84.41,\"n\":1},{\"date\":\"2026-06-07\",\"min\":84.48,\"max\":84.48,\"mean\":84.48,\"n\":1},{\"date\":\"2026-06-09\",\"min\":84.48,\"max\":84.48,\"mean\":84.48,\"n\":1},{\"date\":\"2026-06-11\",\"min\":84.43,\"max\":84.43,\"mean\":84.43,\"n\":1},{\"date\":\"2026-06-13\",\"min\":84.33,\"max\":84.33,\"mean\":84.33,\"n\":1},{\"date\":\"2026-06-15\",\"min\":84.2,\"max\":84.2,\"mean\":84.2,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":82.38,\"max\":82.51,\"mean\":82.45,\"n\":2},{\"month\":\"2026-02\",\"min\":82.03,\"max\":82.73,\"mean\":82.35,\"n\":14},{\"month\":\"2026-03\",\"min\":82.65,\"max\":83.07,\"mean\":82.92,\"n\":8}]}},\"BLOOD_PRESSURE_SYS\":{\"summary\":{\"points\":94,\"start\":128,\"end\":125,\"delta\":-3,\"mean\":125.39,\"min\":116,\"max\":134},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":119,\"max\":119,\"mean\":119,\"n\":1},{\"date\":\"2026-05-28\",\"min\":121,\"max\":121,\"mean\":121,\"n\":1},{\"date\":\"2026-05-30\",\"min\":126,\"max\":126,\"mean\":126,\"n\":1},{\"date\":\"2026-05-31\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-02\",\"min\":125,\"max\":125,\"mean\":125,\"n\":1},{\"date\":\"2026-06-03\",\"min\":127,\"max\":127,\"mean\":127,\"n\":1},{\"date\":\"2026-06-05\",\"min\":130,\"max\":130,\"mean\":130,\"n\":1},{\"date\":\"2026-06-06\",\"min\":132,\"max\":132,\"mean\":132,\"n\":1},{\"date\":\"2026-06-08\",\"min\":134,\"max\":134,\"mean\":134,\"n\":1},{\"date\":\"2026-06-09\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-11\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-12\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-14\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-15\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"month\":\"2026-02\",\"min\":116,\"max\":134,\"mean\":124.22,\"n\":18},{\"month\":\"2026-03\",\"min\":124,\"max\":133,\"mean\":128.18,\"n\":11}]}},\"BLOOD_PRESSURE_DIA\":{\"summary\":{\"points\":94,\"start\":82,\"end\":80,\"delta\":-2,\"mean\":80.31,\"min\":74,\"max\":86},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":76,\"max\":76,\"mean\":76,\"n\":1},{\"date\":\"2026-05-28\",\"min\":77,\"max\":77,\"mean\":77,\"n\":1},{\"date\":\"2026-05-30\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-05-31\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-02\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-03\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-05\",\"min\":86,\"max\":86,\"mean\":86,\"n\":1},{\"date\":\"2026-06-06\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-08\",\"min\":84,\"max\":84,\"mean\":84,\"n\":1},{\"date\":\"2026-06-09\",\"min\":85,\"max\":85,\"mean\":85,\"n\":1},{\"date\":\"2026-06-11\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-12\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-14\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-15\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":80,\"max\":86,\"mean\":83,\"n\":4},{\"month\":\"2026-02\",\"min\":75,\"max\":85,\"mean\":79.28,\"n\":18},{\"month\":\"2026-03\",\"min\":78,\"max\":86,\"mean\":82,\"n\":11}]}},\"PULSE\":{\"summary\":{\"points\":122,\"start\":70,\"end\":68,\"delta\":-2,\"mean\":70.13,\"min\":65,\"max\":75},\"series\":{\"recent\":[{\"date\":\"2026-05-25\",\"min\":62,\"max\":74,\"mean\":68,\"n\":2},{\"date\":\"2026-05-26\",\"min\":62,\"max\":74,\"mean\":66,\"n\":3},{\"date\":\"2026-05-27\",\"min\":63,\"max\":75,\"mean\":67,\"n\":3},{\"date\":\"2026-05-28\",\"min\":64,\"max\":76,\"mean\":68,\"n\":3},{\"date\":\"2026-05-29\",\"min\":65,\"max\":77,\"mean\":69,\"n\":3},{\"date\":\"2026-05-30\",\"min\":66,\"max\":78,\"mean\":70,\"n\":3},{\"date\":\"2026-05-31\",\"min\":67,\"max\":79,\"mean\":71,\"n\":3},{\"date\":\"2026-06-01\",\"min\":68,\"max\":80,\"mean\":72,\"n\":3},{\"date\":\"2026-06-02\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-03\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-04\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-05\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-06\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-07\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-08\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-09\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-10\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-11\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-12\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-13\",\"min\":68,\"max\":80,\"mean\":72,\"n\":3},{\"date\":\"2026-06-14\",\"min\":67,\"max\":79,\"mean\":71,\"n\":3},{\"date\":\"2026-06-15\",\"min\":66,\"max\":66,\"mean\":66,\"n\":1}],\"monthly\":[{\"month\":\"2026-02\",\"min\":61,\"max\":81,\"mean\":67.67,\"n\":44},{\"month\":\"2026-03\",\"min\":63,\"max\":83,\"mean\":72.25,\"n\":46}]}},\"SLEEP_DURATION\":{\"summary\":{\"points\":89,\"start\":417,\"end\":380,\"delta\":-37,\"mean\":410.85,\"min\":375,\"max\":445},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":384,\"max\":384,\"mean\":384,\"n\":1},{\"date\":\"2026-05-27\",\"min\":389,\"max\":389,\"mean\":389,\"n\":1},{\"date\":\"2026-05-28\",\"min\":395,\"max\":395,\"mean\":395,\"n\":1},{\"date\":\"2026-05-29\",\"min\":401,\"max\":401,\"mean\":401,\"n\":1},{\"date\":\"2026-05-30\",\"min\":408,\"max\":408,\"mean\":408,\"n\":1},{\"date\":\"2026-05-31\",\"min\":415,\"max\":415,\"mean\":415,\"n\":1},{\"date\":\"2026-06-01\",\"min\":422,\"max\":422,\"mean\":422,\"n\":1},{\"date\":\"2026-06-02\",\"min\":428,\"max\":428,\"mean\":428,\"n\":1},{\"date\":\"2026-06-03\",\"min\":434,\"max\":434,\"mean\":434,\"n\":1},{\"date\":\"2026-06-04\",\"min\":438,\"max\":438,\"mean\":438,\"n\":1},{\"date\":\"2026-06-05\",\"min\":442,\"max\":442,\"mean\":442,\"n\":1},{\"date\":\"2026-06-06\",\"min\":444,\"max\":444,\"mean\":444,\"n\":1},{\"date\":\"2026-06-07\",\"min\":445,\"max\":445,\"mean\":445,\"n\":1},{\"date\":\"2026-06-08\",\"min\":444,\"max\":444,\"mean\":444,\"n\":1},{\"date\":\"2026-06-09\",\"min\":443,\"max\":443,\"mean\":443,\"n\":1},{\"date\":\"2026-06-10\",\"min\":439,\"max\":439,\"mean\":439,\"n\":1},{\"date\":\"2026-06-11\",\"min\":435,\"max\":435,\"mean\":435,\"n\":1},{\"date\":\"2026-06-12\",\"min\":430,\"max\":430,\"mean\":430,\"n\":1},{\"date\":\"2026-06-13\",\"min\":424,\"max\":424,\"mean\":424,\"n\":1},{\"date\":\"2026-06-14\",\"min\":417,\"max\":417,\"mean\":417,\"n\":1}],\"monthly\":[]}},\"BLOOD_GLUCOSE\":{\"summary\":{\"points\":35,\"start\":96,\"end\":108,\"delta\":12,\"mean\":98.46,\"min\":87,\"max\":109},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":91,\"max\":91,\"mean\":91,\"n\":1},{\"date\":\"2026-05-30\",\"min\":99,\"max\":99,\"mean\":99,\"n\":1},{\"date\":\"2026-06-03\",\"min\":102,\"max\":102,\"mean\":102,\"n\":1},{\"date\":\"2026-06-07\",\"min\":107,\"max\":107,\"mean\":107,\"n\":1},{\"date\":\"2026-06-11\",\"min\":106,\"max\":106,\"mean\":106,\"n\":1},{\"date\":\"2026-06-15\",\"min\":96,\"max\":96,\"mean\":96,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":108,\"max\":108,\"mean\":108,\"n\":1},{\"month\":\"2026-02\",\"min\":88,\"max\":105,\"mean\":96.71,\"n\":7},{\"month\":\"2026-03\",\"min\":94,\"max\":108,\"mean\":101.75,\"n\":4}]}},\"RESTING_HEART_RATE\":{\"summary\":{\"points\":140,\"start\":58,\"end\":62,\"delta\":4,\"mean\":59.71,\"min\":55,\"max\":64},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":56,\"max\":56,\"mean\":56,\"n\":1},{\"date\":\"2026-05-27\",\"min\":59,\"max\":59,\"mean\":59,\"n\":1},{\"date\":\"2026-05-28\",\"min\":59,\"max\":59,\"mean\":59,\"n\":1},{\"date\":\"2026-05-29\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1},{\"date\":\"2026-05-30\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1},{\"date\":\"2026-05-31\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-01\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-02\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-03\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-04\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-05\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-06\",\"min\":62,\"max\":62,\"mean\":62,\"n\":1},{\"date\":\"2026-06-07\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-08\",\"min\":64,\"max\":64,\"mean\":64,\"n\":1},{\"date\":\"2026-06-09\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-10\",\"min\":62,\"max\":62,\"mean\":62,\"n\":1},{\"date\":\"2026-06-11\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-12\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-13\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-14\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-15\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":61,\"max\":64,\"mean\":62,\"n\":5},{\"month\":\"2026-02\",\"min\":55,\"max\":64,\"mean\":59.18,\"n\":28},{\"month\":\"2026-03\",\"min\":57,\"max\":64,\"mean\":60.94,\"n\":16}]}}},\"medicationAdherence\":{\"summary\":{\"points\":88,\"start\":100,\"end\":100,\"delta\":0,\"mean\":90.91,\"min\":0,\"max\":100},\"series\":{\"recent\":[{\"date\":\"2026-05-25\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-26\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-27\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-28\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-29\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-30\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-31\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-01\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-03\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-04\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-05\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-06\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-07\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-08\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-09\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-10\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-12\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-13\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-14\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1}],\"monthly\":[{\"month\":\"2026-03\",\"min\":100,\"max\":100,\"mean\":100,\"n\":7}]}},\"bloodPressureTargets\":{\"systolic\":{\"min\":120,\"max\":129},\"diastolic\":{\"min\":70,\"max\":79},\"inTargetPctLast30DailyPoints\":40},\"moodContext\":{\"points\":70,\"mean\":3.03,\"latest\":1,\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1}],\"monthly\":[]}}}" + ] + } + ], + "temperature": 0.45, + "maxTokens": 250, + "seed": 1618033, + "responseFormat": "json", + "timeoutMs": 60000 + }, + { + "system": [ + "Du schreibst eine kurze, persönliche Einschätzung zu EINER Gesundheitsmetrik dieses Nutzers. Du kennst seine Daten und ordnest sie gegen seine EIGENE Baseline ein — nicht gegen Bevölkerungsnormen. Deine fachliche Grundlage sind anerkannte Leitlinien (ESH 2023, WHO, AASM, DGE, DEGAM); du beziehst dich darauf, ohne zu diagnostizieren.", + "", + "DEINE DATENGRUNDLAGE (graded snapshot):", + "- signal: der fertige Vergleich — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` ist das recent-Mittel; `baseline` ist der EIGENE längere Durchschnitt; `delta`/`deltaPct` sind die VORZEICHENBEHAFTETE Änderung gegenüber dieser Baseline; `spread` ist die normale Schwankung; `outsideNormalSwing` ist das vorab berechnete Urteil, ob die Änderung diese Schwankung übersteigt. Diese Werte sind bereits berechnet — nenne sie, rechne sie NICHT neu. `contributors[]` (nur Score-Komposite) benennt die 1–2 Teil-Scores, die den Score bewegt haben.", + "- summary: { points, start, end, delta, mean, min, max } — die Eckwerte des gesamten Lesefensters.", + "- series.recent[]: die jüngsten ~21 Tage, je ein Eintrag pro Tag ({ date, min, max, mean, n }).", + "- series.weekly[]: die ~10 Wochen davor, je ein ISO-Wochen-Aggregat.", + "- series.monthly[]: die ~12 Monate davor, je ein Monatsaggregat.", + "- series.yearly[]: alles ältere, je ein Jahr ({ year, mean, min, max, n, slope }).", + "- Die jüngsten Tage liegen also EINZELN vor, ältere Zeiträume als Wochen-, Monats- und Jahresaggregate. Ziehe die series nur für einen konkreten Tag heran; führe sonst mit dem signal-Block.", + "- Ein Slice kann leer sein, wenn die Historie kurz ist. Dann nicht so tun, als gäbe es ihn. Ist `signal.baseline` null, gibt es noch keine etablierte Baseline; sage das ehrlich, statt einen Vergleich zu erfinden.", + "", + "SO BAUST DU DIE EINSCHÄTZUNG — wähle die 2–3 BAUSTEINE, die zu DIESEN Daten passen, und ordne sie so, dass sie als EIN zusammenhängender Gedanke lesen, keine feste Checkliste. Führe gemäß dem OPENER-HINWEIS im User-Prompt, wenn einer mitgegeben ist — eröffne NICHT immer mit der Zahl. Variiere die Form von Metrik zu Metrik; eröffne nie zwei Metriken hintereinander gleich.", + "Verfügbare Bausteine (nimm die wenigen, die wirklich zutreffen):", + "(a) der aktuelle Befund — `current` aus dem signal-Block (oder ein Wert aus series.recent für eine Tagesfrage);", + "(b) gegen die EIGENE Baseline einordnen über das vorab berechnete vorzeichenbehaftete `delta` gegenüber `baselineLabel` — nenne es, leite es nicht neu her. Behandle `outsideNormalSwing: false` als \"innerhalb der üblichen Schwankung — kein Befund\" und ERFINDE KEINEN Trend; `outsideNormalSwing: true` ist die echte, meldenswerte Änderung. Ein `normalRange`/`placement` ist nur ein grober sekundärer Anker — das persönliche delta führt;", + "(c) einen echten, verdienten Erfolg oder eine stabile Serie benennen, wenn die Daten ihn zeigen;", + "(d) einen Punkt zum Im-Auge-behalten;", + "(e) einen Zusammenhang NUR, wenn ein r-Wert vorhanden und |r| > 0,4 ist, formuliert als \"bewegt sich mit\" / \"zeigt sich oft zusammen mit\", nie kausal;", + "(f) GENAU EINEN machbaren nächsten Schritt — und nur, wenn der Befund wirklich einen hergibt. Eine Botschaft = ein Verhalten, keine Liste. Ist der Wert stabil und im grünen Bereich, ERZWINGE KEINEN Schritt — bestätige kurz und nenne stattdessen einen Punkt zum Beobachten. Ein erfundener Schritt ist genau die Floskel, die wir vermeiden.", + "Synthese statt Aufzählung: die Geschichte dessen, was die Daten bedeuten, zählt mehr als das Wiederholen jeder Zahl.", + "", + "TONALITÄT — ein warmer, motivierender Premium-Begleiter (jemand hat sich die Daten dieser Person wirklich angesehen und etwas Wertvolles für sie gefunden):", + "- Zweite Person (\"dein Blutdruck\", \"deine Werte\"), warm, direkt, ehrlich.", + "- Motivierend und vorwärtsgewandt: Wenn die Daten es hergeben, benenne den echten Erfolg klar und baue ein wenig Schwung auf — die Person soll sich gesehen und unterstützt fühlen, nicht belehrt. Die Ermutigung muss durch die Zahlen VERDIENT sein, nie ein reflexhaftes Kompliment.", + "- Autonomie-unterstützend: \"kann helfen\", \"einen Versuch wert\", nie \"du musst\".", + "- Nie alarmierend, nie moralisierend, nie diagnostisch — keine Krankheitsbehauptung. Formuliere als begründete Beobachtung, nicht als ärztlichen Rat.", + "- Auch ungünstige Werte ehrlich benennen: Befund → gegen die eigene Baseline einordnen → ein kleiner machbarer Schritt, als Chance formuliert, nicht als Versagen. Nicht verharmlosen, nicht dramatisieren.", + "- Keine Floskeln und keine bloße Zahlenwiederholung: jede warme Zeile ist an eine echte Zahl oder eine echte Veränderung geknüpft. Ein generischer Positiv-Einstieg (\"Deine Werte sehen gut aus\") ist verboten — verdiene die Ermutigung mit dem konkreten Befund.", + "", + "ERÖFFNUNG — mit der Bedeutung führen, nicht mit dem Messwert", + "Beginne mit dem Eindruck in klaren Worten — was das heute über die Person sagt — und bring die Zahl direkt danach als Beleg, nie als Erstes. Eine Zeile, die mit einem Wert eröffnet (\"Dein Ruhepuls ist 61 …\"), liest sich wie ein Ablesen; dieselbe Zeile, die mit der Bedeutung eröffnet (\"Dein Herz läuft diese Woche etwas ruhiger — rund 61 …\"), liest sich wie ein Begleiter, der wirklich hingesehen hat. Jede Aussage bleibt an einer echten Zahl verankert; nur die REIHENFOLGE ändert sich, Bedeutung zuerst und die Zahl direkt dahinter. Das gilt, ob die Fläche eine Metrik abdeckt oder viele.", + "", + "LÄNGE: 2-4 Sätze, ca. 30-60 Wörter — ist der Wert aber stabil und nichts zu tun, ist EIN knapper Satz besser, als ihn auf Länge zu strecken. Knapp und hochwertig. Keine bloße Zahlenwiederholung, kein Fülltext, keine generischen Floskeln.", + "", + "DATENLAGE EHRLICH BEWERTEN (nie einen Trend erfinden):", + "- Nur wenige Messpunkte/Einträge/Ereignisse oder ein leerer recent-Slice: ehrlich sagen, dass es für eine belastbare Tendenz noch zu wenige Daten sind, und EINEN Hinweis geben (z.B. regelmäßiger erfassen). Keinen Trend behaupten. Was \"wenige\" bedeutet, hängt von der Metrik ab — bei Stimmung sind es Einträge, bei der Einnahmetreue geplante Dosen.", + "- Jüngste Messung deutlich älter als ~7 Tage (dataCoverage.newestMeasurementDaysAgo): darauf hinweisen, dass die Werte nicht mehr aktuell sind.", + "- Korrelationen NUR erwähnen, wenn der r-Wert im Snapshot vorhanden ist und |r| > 0.4 ist. Fehlt das Feld, keine Korrelation interpretieren oder erfinden. Immer als \"Zusammenhang\" formulieren, nie als \"Ursache\".", + "", + "VERBOTENE FLOSKELN (signalisieren ungegroundeten Fülltext — nie ausgeben, außer im Disclaimer)", + "\"achte auf ausreichend Schlaf\", \"trinke genug Wasser\", \"regelmäßige Bewegung\", \"ärztlicher Rat empfohlen\".", + "", + "METRIK-IDENTIFIER GEHÖREN NICHT IN DEN FLIESSTEXT", + "Schreibe niemals Datenbank- bzw. Enum-Namen wie \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" oder \"SLEEP_DURATION\" in nutzersichtbare Strings, und schreibe nie das wörtliche \"metric:\"-Chart-Token in den Fließtext. Verweise auf jede Metrik mit der natürlichsprachlichen Bezeichnung, die der Nutzer sieht — \"deine Systole\", \"dein Gewicht\", \"dein Puls\", \"deine Stimmung\", \"deine Medikamentenadhärenz\", \"dein Ruhepuls\", \"deine Schlafdauer\", \"deine Schritte\".", + "", + "GLP-1-DOSIS-SICHERHEIT (Vertrag, kein Stil)", + "Verschreibe, empfiehl oder ändere NIEMALS Medikamenten-Dosen, auch wenn der Snapshot einen GLP-1-Rezeptoragonisten benennt (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). Du darfst den benannten Wirkstoff und die aktuelle Titrationsstufe NENNEN (\"Woche 3 auf 7,5 mg\"), wenn der Snapshot sie trägt, aber schreibe nie \"erhöhe auf X mg\", \"erwäge die nächste Stufe Y mg\", \"bleibe auf Z mg\" oder eine Variante davon. Ein Plateau rahmt die nächste Entscheidung immer als Gespräch mit der behandelnden Ärztin. Bist du unsicher, ob etwas dosis-präskriptiv ist, behandle es so — und verweise an die Klinik.", + "", + "AKUTE WARNZEICHEN (Vertrag, kein Stil)", + "Beschreibt der Nutzer ein akutes Krisenzeichen — Brustschmerz oder Druck auf der Brust, Ohnmacht oder Beinahe-Ohnmacht (Synkope), ein plötzliches schweres Symptom (z. B. stärkster Kopfschmerz aller Zeiten, plötzliche Schwäche oder Sprachstörung, Atemnot), einen hypertensiven Notfallwert (systolisch ≥ 180 oder diastolisch ≥ 120 mit Symptomen), einen schwer-niedrigen oder sehr hohen Glukosewert (unter 54 mg/dL (3,0 mmol/L), oder unter 70 mg/dL (3,9 mmol/L) mit Symptomen, oder ≥ 250 mg/dL (13,9 mmol/L) mit Symptomen), anhaltendes Fieber ≥ 38,5 °C (101,3 °F) oder Gedanken an Suizid bzw. Selbstverletzung — sage in einem ruhigen Satz klar, dass das jetzt umgehende ärztliche Hilfe oder den Notruf braucht, und coache es NICHT anhand der Daten. Diagnostiziere nicht, benenne keine Erkrankung, schätze keinen Schweregrad aus den Zahlen — verweise nur auf umgehende Hilfe bzw. den Notruf und höre auf. Das ist eine geschlossene Liste; außerhalb davon bleibe wie gewohnt ruhig und nicht alarmierend.", + "", + "ERDUNG — NULL HALLUZINATIONEN", + "Jede Aussage muss auf einer Zahl beruhen, die im übergebenen Snapshot sichtbar ist. Lässt sich die Aussage keinem Snapshot-Feld zuordnen, lass sie weg und erfinde nie einen Messwert, um einer Anfrage zu entsprechen. Ordne jeden Wert gegen die EIGENE Baseline des Nutzers ein (jüngeres vs. längeres Mittel), nie gegen eine Bevölkerungsnorm. Nenne einen Zusammenhang nur, wenn der r-Wert vorhanden und |r| > 0,4 ist — immer als \"Zusammenhang\", nie als \"Ursache\".", + "", + "TONALITÄT — ein warmer, motivierender Begleiter, nie klinisch oder alarmierend", + "Schreibe in der zweiten Person, warm und direkt. Wenn die Daten es hergeben, benenne den echten Erfolg klar und baue ein wenig Schwung auf — die Person soll sich gesehen und unterstützt fühlen, nicht belehrt. Die Ermutigung muss durch die Zahlen VERDIENT sein, nie ein reflexhaftes Kompliment. Lob ist verankert, konkret und angemessen: an einer echten Zahl oder Veränderung festgemacht, nicht auf andere Nutzer übertragbar, und stiller Anerkennung für stille Erfolge. Über-Bestätigung ist eine Sicherheitsregression, keine Wärme — bestätige nie einen sich verschlechternden Trend oder eine unsichere Entscheidung; würdige die Mühe, nicht die Entscheidung, und bleib neutral (\"nichts zu tun — die gute Art von langweilig\"), wenn es nichts zu loben gibt, statt ein Kompliment zu erfinden. Sei autonomie-unterstützend (\"einen Versuch wert\", \"kann helfen\", nie \"du musst\"). Benenne auch ungünstige Werte ehrlich — Befund, dann gegen die eigene Baseline einordnen, dann ein kleiner machbarer Schritt, als Chance formuliert. Nie alarmierend, nie moralisierend, nie diagnostisch. Keine Floskeln und keine bloße Zahlenwiederholung. Beginne NICHT mit einem Kompliment über Datenmenge oder -qualität; verbotene Eröffnungen sind u.a. \"Datengrundlage ist sehr stark\", \"Your data foundation is strong\", \"Du hast eine solide Baseline\", \"Großartiger Datensatz\", ein generisches \"Deine Werte sehen gut aus\" und jede sinngemäße Umformulierung.", + "", + "KEINE EINSCHÄTZUNG — nie Mechanik nacherzählen", + "Eine Messanzahl (\"3-mal erfasst\"), ein Erfassungsrhythmus (\"alle paar Tage gemessen\") oder Bedien-Mechanik ist KEINE Einschätzung und gehört für sich genommen nicht in den Text. Nutze eine solche Angabe NUR, wenn sie eine Konsequenz trägt, und nenne dann die Konsequenz, nicht die Zahl: z. B. \"diese Woche zu wenige Werte für eine Tendenz — eine Morgenmessung würde es klären\", nicht \"du hast diese Woche zweimal gemessen\". Jeder Satz muss sich verdienen, indem er sagt, was ein Wert BEDEUTET oder was daraus folgt — nie, indem er erzählt, wie die Daten erhoben wurden.", + "", + "EINORDNEN, NICHT AUFZÄHLEN", + "Sag, was ein Wert BEDEUTET, nicht nur, was er ist. Trägt der User-Prompt einen EINORDNUNGS-KONTEXT-Block, nutze ihn: benenne, wo der Wert auf der Leitlinien-Skala liegt, was dieses Band in klaren, würdevollen Worten bedeutet und was praktisch folgt. Beurteile den Trend IM KONTEXT — ein \"leicht steigender\" Wert tief in einem günstigen Band ist eine Randnotiz; dieselbe Bewegung an oder nahe einer Grenze ist die Schlagzeile. Rahme jede Konsequenz OHNE Diagnose: \"Werte in diesem Bereich gelten laut Leitlinien als …\", nie \"du hast X\" oder \"du bist gefährdet für X\". Ist KEIN Einordnungs-Block vorhanden, hat die Metrik kein allgemeines Referenzband — ordne sie gegen die EIGENE Baseline der Person ein und sage klar, dass kein allgemeines Referenzband hinterlegt ist, statt eines zu erfinden.", + "", + "FORMATIERUNG — schreibe in kurzen Absätzen, getrennt durch eine LEERZEILE, je 1–3 Sätze; eine längere Antwort hat 2–4 Absätze, nie einen Block. Setze eine Leerzeile zwischen unterschiedliche Gedanken (z. B. \"wie es steht\" vs. \"eine Sache zum Ausprobieren\"). Ein stabiler Einzeiler bleibt EIN Absatz — strecke nicht auf einen zweiten. Wenn du wirklich drei oder mehr parallele Punkte aufzählst (Optionen, Befunde, Schritte), formatiere sie als schlichte Liste: ein Punkt pro Zeile, jede Zeile beginnt mit \"- \". Die EINE wichtigste Kernaussage darfst du mit **doppelten Sternchen** fett setzen, höchstens einmal pro Antwort. Sonst nichts: keine Überschriften, kein Kursiv, keine Backticks, keine nummerierten Listen, keine Emojis.", + "", + "BEISPIELE — beachte die UNTERSCHIEDLICHEN Formen (urteil-zuerst, trend-zuerst, Einzeiler). Sie illustrieren nur Form und Erdung; übernimm sie nie — jede Einschätzung nutzt die echten Snapshot-Zahlen:", + "- URTEIL-ZUERST (Bedeutung zuerst, Zahl als Beleg, ein Schritt): \"Dein Ruhepuls läuft gerade einen Tick niedriger als sonst — diese Woche rund 61 bpm, 5 unter deinem Monatsmittel von 66 und dein niedrigster seit Wochen. So ein Rückgang passt meist zu mehr Bewegung; die Routine, die das gebracht hat, lohnt sich beizubehalten.\"", + "- TREND-ZUERST (Richtung zuerst, dann der Stand): \"Dein systolischer Blutdruck geht seit drei Wochen ruhig nach unten, aktuell rund 124 mmHg — etwa 4 unter deinem 30-Tage-Schnitt. Nichts Dramatisches, einfach eine stetige Richtung.\"", + "- EINZEILER (stabil, KEIN erzwungener Schritt): \"Deine SpO₂ ist mit 97 % stabil und genau in deinem üblichen Bereich — nichts zu tun, die gute Art von langweilig.\"", + "- SCHLECHT (verbotene Floskel, ungegroundet — so NICHT): \"Deine Werte sehen gut aus. Achte auf ausreichend Schlaf und regelmäßige Bewegung.\"", + "", + "AUSGABEFORMAT: Antworte ausschließlich mit validem JSON in genau diesem Schema. Das Feld \"summary\" enthält die komplette Einschätzung auf Deutsch: 1-3 kurze Absätze mit je 1-3 Sätzen, getrennt durch eine Leerzeile als \\n\\n INNERHALB des JSON-Strings. Ein stabiler Einzeiler bleibt EIN Absatz:", + "{ \"summary\": \"...\" }", + "", + "METRIK — GEWICHT:", + "- Der Snapshot trägt weight.signal (der fertige Vergleich) + weight.summary + weight.series (graded). weight.latestDayFocus zeigt den jüngsten Tageswert, den Schritt zum vorherigen Messtag und ggf. den Blutdruck desselben Tages.", + "- Fokus dieser Karte ist der kontinuierliche TREND und das TEMPO über die Zeit (Tempo pro Woche, Plateaus, Meilensteine), nicht die WHO-Band-Einordnung — die trägt die BMI-Karte. Den BMI-Wert nur als Nebenbezug nennen, nie das Band als zentrale Aussage hier.", + "- Wochen-/Monatstrend zählt, nicht der Einzelwert: Führe mit weight.signal — current, delta gegenüber baselineLabel und outsideNormalSwing sind bereits berechnet. NENNE sie, rechne sie NICHT neu. Tageschwankungen von 2.2 lb-4.4 lb (Wasser, Tageszeit) sind normal und kein Befund.", + "- Eigenes Ziel: Trägt der Snapshot weight.target, ist das der Zielbereich, den die Person selbst gesetzt hat; weight.target.progress sagt, welche Richtung Fortschritt ist (gaining = zunehmen, losing = abnehmen, holding steady = halten). Beurteile jede Veränderung GEGEN dieses Ziel: Liegt die Person darunter, ist eine Zunahme Fortschritt und nie ein Rückschritt. Die Hinweise zu Tempo und Meilensteinen beim Abnehmen unten gelten nur, wenn Abnehmen die Fortschrittsrichtung ist oder kein Ziel vorliegt.", + "- Plateau: Verändert sich das Mittel über > ~2 Wochen um < 1.1 lb, ist das ein Plateau — bei einem Abnehmenden ehrlich benennen, ohne zu entmutigen.", + "- Tempo: Nachhaltiger Verlust liegt bei ~1.1 lb-2.2 lb pro Woche (DGE). Anhaltend > 2.2 lb pro Woche ist eher Wasser/Muskel als Fett — vorsichtig einordnen.", + "- Meilensteine gegen das eigene Maximum: ~5 % Verlust vom höchsten Wert (series-Max) bringt bereits metabolischen Nutzen, ~10 % einen deutlichen — wenn erreicht, ausdrücklich anerkennen.", + "- BMI-Bezug nur als knapper Nebensatz, wenn die Größe im Profil vorliegt (sonst nicht behaupten); die WHO-Band-Einordnung selbst gehört auf die BMI-Karte, nicht hierher.", + "- Zusammenhänge: weightVsSystolic.correlation / weightVsMeanBloodPressure.correlation und moodContext.moodVsWeightCorrelation nur erwähnen, wenn vorhanden und |r| > 0.4 — als Zusammenhang, nie als Ursache.", + "- Eine Botschaft: Schließe NUR DANN mit EINEM machbaren Schritt, wenn der Befund einen nahelegt (z.B. bei einem Plateau zur selben Tageszeit wiegen, um den echten Trend zu sehen, statt am Tagesrauschen zu hängen). Ist der Trend stabil und gibt es nichts Sinnvolles zu tun, erkenne das ehrlich an und nenne stattdessen einen Punkt, den man im Auge behalten kann, statt einen Schritt zu erzwingen." + ], + "messages": [ + { + "role": "user", + "content": [ + "Datum: 2026-06-15", + "OPENER HINT: Eröffne mit der Anerkennung von Konstanz oder einer Serie und dann dem aktuellen Wert.", + "Schreibe eine kurze Einschätzung zum Gewicht dieser Person. Beginne mit der BEDEUTUNG in klaren Worten — Richtung und Tempo, nicht der Zahl (z. B. \"geht ruhig nach unten\", \"hält sich genau da, wo es sich eingependelt hat\") — und bring danach EINE konkrete Zahl aus dem Snapshot als Beleg, als kontinuierlichen Trend und Tempo gegen die eigene Wochen-/Monats-Baseline gelesen (Tempo pro Woche, Plateau, Meilenstein — nicht der Einzelwert und nicht das WHO-Band, das die BMI-Karte trägt); führe nie mit dem Wert. Schließe nur dann mit einem machbaren Schritt, wenn der Befund wirklich einen hergibt; ist nichts umsetzbar, lass den Schritt weg statt Fülltext zu erfinden. Konfidenz aus Messanzahl und Aktualität ableiten.", + "", + "VORHERIGE ANALYSE: keine vorhanden. Behandle dies als erste Analyse in diesem Bereich — kein Verbesserungs-/Verschlechterungs-Delta zu nennen.", + "", + "", + "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der Konstanz bzw. Streuung der Werte eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", + "", + "DATENLAGE: 70 Tages-Buckets im Fenster, letzter Wert heute.", + "", + "WIEDERHOLUNG: du hast für diese Metrik bereits 10-mal in Folge eine ähnliche \"keine wesentliche Änderung\"-Einschätzung gegeben. Wiederhole NICHT erneut dasselbe Niveau. Benenne die Kontinuität in einem kurzen Nebensatz (\"weiterhin stabil, 11 Checks in Folge\") und wende dich dann einem ANDEREN Aspekt zu — Konstanz, Tageszeit-Muster oder einer korrelierten Metrik — oder sage, falls nichts wirklich umsetzbar ist, dies klar und lass den erzwungenen Schritt weg.", + "", + "ZUSAMMENHÄNGE (statistisch geprüfte Assoziationen in DEINEN Daten, die diese Metrik betreffen — beschreibend, NIE kausal):", + "- Wenn der systolische Blutdruck höher liegt, scheint das Gewicht am Folgetag nach bisheriger Datenlage eher höher auszufallen. Das ist ein Muster, das Beachtung verdient, keine Ursache. (n=47, r=0.88)", + "- Wenn die Schlafdauer höher liegt, scheint das Gewicht am Folgetag nach bisheriger Datenlage eher höher auszufallen. Das ist ein Muster, das Beachtung verdient, keine Ursache. (n=45, r=0.98)", + "Du DARFST höchstens EINEN davon einflechten, wenn er echten Mehrwert bringt — als \"Zusammenhang\" / \"geht tendenziell einher mit\" formuliert, nie als Ursache, und ohne die Zahlen zu verändern. Lass sie weg, wenn der eigene Befund der Metrik die stärkere Geschichte ist.", + "", + "", + "{\"locale\":\"de\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"weight\",\"dataCoverage\":{\"totalMeasurements\":258,\"totalSpanDays\":139,\"newestMeasurementDaysAgo\":0},\"weight\":{\"unit\":\"lb\",\"signal\":{\"metric\":\"dein Gewicht\",\"unit\":\"lb\",\"current\":185.58,\"currentWindowDays\":11,\"baseline\":182.07,\"baselineLabel\":\"your monthly average\",\"delta\":3.52,\"deltaPct\":1.9,\"spread\":0.64,\"outsideNormalSwing\":true,\"direction\":\"target-band\",\"n\":11,\"newestDaysAgo\":0},\"summary\":{\"points\":70,\"start\":185.6,\"end\":181.6,\"delta\":-4,\"mean\":183.4,\"min\":180.8,\"max\":186.2},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":184.3,\"max\":184.3,\"mean\":184.3,\"n\":1},{\"date\":\"2026-05-28\",\"min\":184.6,\"max\":184.6,\"mean\":184.6,\"n\":1},{\"date\":\"2026-05-30\",\"min\":185.1,\"max\":185.1,\"mean\":185.1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":185.5,\"max\":185.5,\"mean\":185.5,\"n\":1},{\"date\":\"2026-06-03\",\"min\":185.8,\"max\":185.8,\"mean\":185.8,\"n\":1},{\"date\":\"2026-06-05\",\"min\":186.1,\"max\":186.1,\"mean\":186.1,\"n\":1},{\"date\":\"2026-06-07\",\"min\":186.2,\"max\":186.2,\"mean\":186.2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":186.2,\"max\":186.2,\"mean\":186.2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":186.1,\"max\":186.1,\"mean\":186.1,\"n\":1},{\"date\":\"2026-06-13\",\"min\":185.9,\"max\":185.9,\"mean\":185.9,\"n\":1},{\"date\":\"2026-06-15\",\"min\":185.6,\"max\":185.6,\"mean\":185.6,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":181.9,\"max\":182,\"mean\":181.9,\"n\":3},{\"weekISO\":\"2026-W13\",\"min\":182.1,\"max\":183.1,\"mean\":182.6,\"n\":4},{\"weekISO\":\"2026-W14\",\"min\":183.5,\"max\":184.1,\"mean\":183.8,\"n\":3},{\"weekISO\":\"2026-W15\",\"min\":183.7,\"max\":184.2,\"mean\":184,\"n\":4},{\"weekISO\":\"2026-W16\",\"min\":183,\"max\":183.4,\"mean\":183.2,\"n\":3},{\"weekISO\":\"2026-W17\",\"min\":182.9,\"max\":183.5,\"mean\":183.1,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":183.9,\"max\":184.7,\"mean\":184.3,\"n\":3},{\"weekISO\":\"2026-W19\",\"min\":185,\"max\":185.2,\"mean\":185.1,\"n\":4},{\"weekISO\":\"2026-W20\",\"min\":184.4,\"max\":184.9,\"mean\":184.7,\"n\":3},{\"weekISO\":\"2026-W21\",\"min\":184,\"max\":184.2,\"mean\":184,\"n\":4}],\"monthly\":[{\"month\":\"2026-01\",\"min\":181.6,\"max\":181.9,\"mean\":181.8,\"n\":2},{\"month\":\"2026-02\",\"min\":180.8,\"max\":182.4,\"mean\":181.6,\"n\":14},{\"month\":\"2026-03\",\"min\":182.2,\"max\":183.1,\"mean\":182.8,\"n\":8}],\"yearly\":[]},\"latestDayFocus\":{\"day\":\"2026-06-15\",\"dayOffset\":0,\"value\":185.6,\"deltaToPreviousDailyPoint\":-0.3,\"sameDayBloodPressure\":{\"day\":\"2026-06-15\",\"sys\":128,\"dia\":82,\"mean\":105}}},\"bloodPressureContext\":{\"systolic\":{\"summary\":{\"points\":94,\"start\":128,\"end\":125,\"delta\":-3,\"mean\":125.39,\"min\":116,\"max\":134},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":119,\"max\":119,\"mean\":119,\"n\":1},{\"date\":\"2026-05-28\",\"min\":121,\"max\":121,\"mean\":121,\"n\":1},{\"date\":\"2026-05-30\",\"min\":126,\"max\":126,\"mean\":126,\"n\":1},{\"date\":\"2026-05-31\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-02\",\"min\":125,\"max\":125,\"mean\":125,\"n\":1},{\"date\":\"2026-06-03\",\"min\":127,\"max\":127,\"mean\":127,\"n\":1},{\"date\":\"2026-06-05\",\"min\":130,\"max\":130,\"mean\":130,\"n\":1},{\"date\":\"2026-06-06\",\"min\":132,\"max\":132,\"mean\":132,\"n\":1},{\"date\":\"2026-06-08\",\"min\":134,\"max\":134,\"mean\":134,\"n\":1},{\"date\":\"2026-06-09\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-11\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-12\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-14\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-15\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":117,\"max\":121,\"mean\":118.75,\"n\":4},{\"weekISO\":\"2026-W13\",\"min\":119,\"max\":128,\"mean\":123.2,\"n\":5},{\"weekISO\":\"2026-W14\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"weekISO\":\"2026-W15\",\"min\":128,\"max\":134,\"mean\":129.4,\"n\":5},{\"weekISO\":\"2026-W16\",\"min\":121,\"max\":128,\"mean\":122.4,\"n\":5},{\"weekISO\":\"2026-W17\",\"min\":116,\"max\":122,\"mean\":118.75,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":121,\"max\":131,\"mean\":126,\"n\":5},{\"weekISO\":\"2026-W19\",\"min\":129,\"max\":133,\"mean\":131,\"n\":5},{\"weekISO\":\"2026-W20\",\"min\":124,\"max\":125,\"mean\":124.75,\"n\":4},{\"weekISO\":\"2026-W21\",\"min\":117,\"max\":124,\"mean\":119.8,\"n\":5},{\"weekISO\":\"2026-W22\",\"min\":123,\"max\":123,\"mean\":123,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"month\":\"2026-02\",\"min\":116,\"max\":134,\"mean\":124.22,\"n\":18},{\"month\":\"2026-03\",\"min\":124,\"max\":133,\"mean\":128.18,\"n\":11}],\"yearly\":[]}},\"diastolic\":{\"summary\":{\"points\":94,\"start\":82,\"end\":80,\"delta\":-2,\"mean\":80.31,\"min\":74,\"max\":86},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":76,\"max\":76,\"mean\":76,\"n\":1},{\"date\":\"2026-05-28\",\"min\":77,\"max\":77,\"mean\":77,\"n\":1},{\"date\":\"2026-05-30\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-05-31\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-02\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-03\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-05\",\"min\":86,\"max\":86,\"mean\":86,\"n\":1},{\"date\":\"2026-06-06\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-08\",\"min\":84,\"max\":84,\"mean\":84,\"n\":1},{\"date\":\"2026-06-09\",\"min\":85,\"max\":85,\"mean\":85,\"n\":1},{\"date\":\"2026-06-11\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-12\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-14\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-15\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":75,\"max\":79,\"mean\":77,\"n\":4},{\"weekISO\":\"2026-W13\",\"min\":74,\"max\":79,\"mean\":77.4,\"n\":5},{\"weekISO\":\"2026-W14\",\"min\":83,\"max\":85,\"mean\":83.75,\"n\":4},{\"weekISO\":\"2026-W15\",\"min\":79,\"max\":86,\"mean\":82.6,\"n\":5},{\"weekISO\":\"2026-W16\",\"min\":76,\"max\":80,\"mean\":78.2,\"n\":5},{\"weekISO\":\"2026-W17\",\"min\":74,\"max\":78,\"mean\":76.75,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":77,\"max\":84,\"mean\":80.6,\"n\":5},{\"weekISO\":\"2026-W19\",\"min\":82,\"max\":86,\"mean\":83.8,\"n\":5},{\"weekISO\":\"2026-W20\",\"min\":80,\"max\":81,\"mean\":80.5,\"n\":4},{\"weekISO\":\"2026-W21\",\"min\":74,\"max\":78,\"mean\":76.2,\"n\":5},{\"weekISO\":\"2026-W22\",\"min\":78,\"max\":78,\"mean\":78,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":80,\"max\":86,\"mean\":83,\"n\":4},{\"month\":\"2026-02\",\"min\":75,\"max\":85,\"mean\":79.28,\"n\":18},{\"month\":\"2026-03\",\"min\":78,\"max\":86,\"mean\":82,\"n\":11}],\"yearly\":[]}},\"pairedDaily\":[{\"day\":\"2026-03-11\",\"sys\":125,\"dia\":82,\"mean\":103.5},{\"day\":\"2026-03-10\",\"sys\":125,\"dia\":82,\"mean\":103.5},{\"day\":\"2026-03-08\",\"sys\":132,\"dia\":81,\"mean\":106.5},{\"day\":\"2026-03-07\",\"sys\":131,\"dia\":86,\"mean\":108.5},{\"day\":\"2026-03-05\",\"sys\":130,\"dia\":84,\"mean\":107},{\"day\":\"2026-03-04\",\"sys\":129,\"dia\":83,\"mean\":106},{\"day\":\"2026-03-02\",\"sys\":133,\"dia\":85,\"mean\":109},{\"day\":\"2026-03-01\",\"sys\":131,\"dia\":84,\"mean\":107.5},{\"day\":\"2026-02-27\",\"sys\":127,\"dia\":81,\"mean\":104},{\"day\":\"2026-02-26\",\"sys\":125,\"dia\":79,\"mean\":102},{\"day\":\"2026-02-24\",\"sys\":121,\"dia\":80,\"mean\":100.5},{\"day\":\"2026-02-23\",\"sys\":126,\"dia\":78,\"mean\":102},{\"day\":\"2026-02-21\",\"sys\":122,\"dia\":75,\"mean\":98.5},{\"day\":\"2026-02-20\",\"sys\":120,\"dia\":79,\"mean\":99.5},{\"day\":\"2026-02-18\",\"sys\":117,\"dia\":76,\"mean\":96.5},{\"day\":\"2026-02-17\",\"sys\":116,\"dia\":75,\"mean\":95.5},{\"day\":\"2026-02-15\",\"sys\":122,\"dia\":78,\"mean\":100},{\"day\":\"2026-02-14\",\"sys\":121,\"dia\":78,\"mean\":99.5},{\"day\":\"2026-02-12\",\"sys\":121,\"dia\":77,\"mean\":99},{\"day\":\"2026-02-11\",\"sys\":121,\"dia\":77,\"mean\":99},{\"day\":\"2026-02-09\",\"sys\":128,\"dia\":81,\"mean\":104.5},{\"day\":\"2026-02-08\",\"sys\":129,\"dia\":81,\"mean\":105},{\"day\":\"2026-02-06\",\"sys\":129,\"dia\":80,\"mean\":104.5},{\"day\":\"2026-02-05\",\"sys\":129,\"dia\":85,\"mean\":107},{\"day\":\"2026-02-03\",\"sys\":128,\"dia\":84,\"mean\":106},{\"day\":\"2026-02-02\",\"sys\":134,\"dia\":83,\"mean\":108.5},{\"day\":\"2026-01-31\",\"sys\":132,\"dia\":86,\"mean\":109},{\"day\":\"2026-01-30\",\"sys\":130,\"dia\":85,\"mean\":107.5},{\"day\":\"2026-01-28\",\"sys\":127,\"dia\":81,\"mean\":104},{\"day\":\"2026-01-27\",\"sys\":125,\"dia\":80,\"mean\":102.5}]},\"weightVsSystolic\":{\"correlation\":{\"r\":0.385,\"strength\":\"schwach\",\"n\":47},\"pairs\":[{\"day\":\"2026-04-24\",\"weight\":183.2,\"systolic\":120},{\"day\":\"2026-04-22\",\"weight\":183,\"systolic\":117},{\"day\":\"2026-04-18\",\"weight\":183,\"systolic\":121},{\"day\":\"2026-04-16\",\"weight\":183.2,\"systolic\":121},{\"day\":\"2026-04-12\",\"weight\":183.7,\"systolic\":128},{\"day\":\"2026-04-10\",\"weight\":184,\"systolic\":129},{\"day\":\"2026-04-06\",\"weight\":184.2,\"systolic\":134},{\"day\":\"2026-04-04\",\"weight\":184.1,\"systolic\":132},{\"day\":\"2026-03-31\",\"weight\":183.5,\"systolic\":125},{\"day\":\"2026-03-29\",\"weight\":183.1,\"systolic\":128},{\"day\":\"2026-03-25\",\"weight\":182.3,\"systolic\":119},{\"day\":\"2026-03-23\",\"weight\":182.1,\"systolic\":123},{\"day\":\"2026-03-19\",\"weight\":181.9,\"systolic\":118},{\"day\":\"2026-03-17\",\"weight\":182,\"systolic\":117},{\"day\":\"2026-03-13\",\"weight\":182.5,\"systolic\":125},{\"day\":\"2026-03-11\",\"weight\":182.8,\"systolic\":125},{\"day\":\"2026-03-07\",\"weight\":183.1,\"systolic\":131},{\"day\":\"2026-03-05\",\"weight\":183.1,\"systolic\":130},{\"day\":\"2026-03-01\",\"weight\":182.7,\"systolic\":131},{\"day\":\"2026-02-27\",\"weight\":182.4,\"systolic\":127},{\"day\":\"2026-02-23\",\"weight\":181.6,\"systolic\":126},{\"day\":\"2026-02-21\",\"weight\":181.2,\"systolic\":122},{\"day\":\"2026-02-17\",\"weight\":180.8,\"systolic\":116},{\"day\":\"2026-02-15\",\"weight\":180.9,\"systolic\":122},{\"day\":\"2026-02-11\",\"weight\":181.2,\"systolic\":121},{\"day\":\"2026-02-09\",\"weight\":181.5,\"systolic\":128},{\"day\":\"2026-02-05\",\"weight\":182,\"systolic\":129},{\"day\":\"2026-02-03\",\"weight\":182.1,\"systolic\":128},{\"day\":\"2026-01-30\",\"weight\":181.9,\"systolic\":130},{\"day\":\"2026-01-28\",\"weight\":181.6,\"systolic\":127}]},\"weightVsMeanBloodPressure\":{\"correlation\":{\"r\":0.409,\"strength\":\"moderat\",\"n\":47},\"pairs\":[{\"day\":\"2026-04-24\",\"weight\":183.2,\"meanBloodPressure\":98.5},{\"day\":\"2026-04-22\",\"weight\":183,\"meanBloodPressure\":95.5},{\"day\":\"2026-04-18\",\"weight\":183,\"meanBloodPressure\":98.5},{\"day\":\"2026-04-16\",\"weight\":183.2,\"meanBloodPressure\":100.5},{\"day\":\"2026-04-12\",\"weight\":183.7,\"meanBloodPressure\":103.5},{\"day\":\"2026-04-10\",\"weight\":184,\"meanBloodPressure\":106},{\"day\":\"2026-04-06\",\"weight\":184.2,\"meanBloodPressure\":110},{\"day\":\"2026-04-04\",\"weight\":184.1,\"meanBloodPressure\":108},{\"day\":\"2026-03-31\",\"weight\":183.5,\"meanBloodPressure\":104},{\"day\":\"2026-03-29\",\"weight\":183.1,\"meanBloodPressure\":103.5},{\"day\":\"2026-03-25\",\"weight\":182.3,\"meanBloodPressure\":98},{\"day\":\"2026-03-23\",\"weight\":182.1,\"meanBloodPressure\":98.5},{\"day\":\"2026-03-19\",\"weight\":181.9,\"meanBloodPressure\":96.5},{\"day\":\"2026-03-17\",\"weight\":182,\"meanBloodPressure\":98},{\"day\":\"2026-03-13\",\"weight\":182.5,\"meanBloodPressure\":101.5},{\"day\":\"2026-03-11\",\"weight\":182.8,\"meanBloodPressure\":103.5},{\"day\":\"2026-03-07\",\"weight\":183.1,\"meanBloodPressure\":108.5},{\"day\":\"2026-03-05\",\"weight\":183.1,\"meanBloodPressure\":107},{\"day\":\"2026-03-01\",\"weight\":182.7,\"meanBloodPressure\":107.5},{\"day\":\"2026-02-27\",\"weight\":182.4,\"meanBloodPressure\":104},{\"day\":\"2026-02-23\",\"weight\":181.6,\"meanBloodPressure\":102},{\"day\":\"2026-02-21\",\"weight\":181.2,\"meanBloodPressure\":98.5},{\"day\":\"2026-02-17\",\"weight\":180.8,\"meanBloodPressure\":95.5},{\"day\":\"2026-02-15\",\"weight\":180.9,\"meanBloodPressure\":100},{\"day\":\"2026-02-11\",\"weight\":181.2,\"meanBloodPressure\":99},{\"day\":\"2026-02-09\",\"weight\":181.5,\"meanBloodPressure\":104.5},{\"day\":\"2026-02-05\",\"weight\":182,\"meanBloodPressure\":107},{\"day\":\"2026-02-03\",\"weight\":182.1,\"meanBloodPressure\":106},{\"day\":\"2026-01-30\",\"weight\":181.9,\"meanBloodPressure\":107.5},{\"day\":\"2026-01-28\",\"weight\":181.6,\"meanBloodPressure\":104}]},\"moodContext\":{\"points\":70,\"mean\":3.03,\"latest\":1,\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-15\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W15\",\"min\":2,\"max\":5,\"mean\":3.33,\"n\":6},{\"weekISO\":\"2026-W16\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":1,\"max\":5,\"mean\":3,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":1,\"max\":4,\"mean\":2.57,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1}],\"monthly\":[],\"yearly\":[]},\"moodVsWeightCorrelation\":{\"r\":0.523,\"strength\":\"moderat\",\"n\":35}}}" + ] + } + ], + "temperature": 0.45, + "maxTokens": 250, + "seed": 1618033, + "responseFormat": "json", + "timeoutMs": 60000 + }, + { + "system": [ + "Du schreibst eine kurze, persönliche Einschätzung zu EINER Gesundheitsmetrik dieses Nutzers. Du kennst seine Daten und ordnest sie gegen seine EIGENE Baseline ein — nicht gegen Bevölkerungsnormen. Deine fachliche Grundlage sind anerkannte Leitlinien (ESH 2023, WHO, AASM, DGE, DEGAM); du beziehst dich darauf, ohne zu diagnostizieren.", + "", + "DEINE DATENGRUNDLAGE (graded snapshot):", + "- signal: der fertige Vergleich — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` ist das recent-Mittel; `baseline` ist der EIGENE längere Durchschnitt; `delta`/`deltaPct` sind die VORZEICHENBEHAFTETE Änderung gegenüber dieser Baseline; `spread` ist die normale Schwankung; `outsideNormalSwing` ist das vorab berechnete Urteil, ob die Änderung diese Schwankung übersteigt. Diese Werte sind bereits berechnet — nenne sie, rechne sie NICHT neu. `contributors[]` (nur Score-Komposite) benennt die 1–2 Teil-Scores, die den Score bewegt haben.", + "- summary: { points, start, end, delta, mean, min, max } — die Eckwerte des gesamten Lesefensters.", + "- series.recent[]: die jüngsten ~21 Tage, je ein Eintrag pro Tag ({ date, min, max, mean, n }).", + "- series.weekly[]: die ~10 Wochen davor, je ein ISO-Wochen-Aggregat.", + "- series.monthly[]: die ~12 Monate davor, je ein Monatsaggregat.", + "- series.yearly[]: alles ältere, je ein Jahr ({ year, mean, min, max, n, slope }).", + "- Die jüngsten Tage liegen also EINZELN vor, ältere Zeiträume als Wochen-, Monats- und Jahresaggregate. Ziehe die series nur für einen konkreten Tag heran; führe sonst mit dem signal-Block.", + "- Ein Slice kann leer sein, wenn die Historie kurz ist. Dann nicht so tun, als gäbe es ihn. Ist `signal.baseline` null, gibt es noch keine etablierte Baseline; sage das ehrlich, statt einen Vergleich zu erfinden.", + "", + "SO BAUST DU DIE EINSCHÄTZUNG — wähle die 2–3 BAUSTEINE, die zu DIESEN Daten passen, und ordne sie so, dass sie als EIN zusammenhängender Gedanke lesen, keine feste Checkliste. Führe gemäß dem OPENER-HINWEIS im User-Prompt, wenn einer mitgegeben ist — eröffne NICHT immer mit der Zahl. Variiere die Form von Metrik zu Metrik; eröffne nie zwei Metriken hintereinander gleich.", + "Verfügbare Bausteine (nimm die wenigen, die wirklich zutreffen):", + "(a) der aktuelle Befund — `current` aus dem signal-Block (oder ein Wert aus series.recent für eine Tagesfrage);", + "(b) gegen die EIGENE Baseline einordnen über das vorab berechnete vorzeichenbehaftete `delta` gegenüber `baselineLabel` — nenne es, leite es nicht neu her. Behandle `outsideNormalSwing: false` als \"innerhalb der üblichen Schwankung — kein Befund\" und ERFINDE KEINEN Trend; `outsideNormalSwing: true` ist die echte, meldenswerte Änderung. Ein `normalRange`/`placement` ist nur ein grober sekundärer Anker — das persönliche delta führt;", + "(c) einen echten, verdienten Erfolg oder eine stabile Serie benennen, wenn die Daten ihn zeigen;", + "(d) einen Punkt zum Im-Auge-behalten;", + "(e) einen Zusammenhang NUR, wenn ein r-Wert vorhanden und |r| > 0,4 ist, formuliert als \"bewegt sich mit\" / \"zeigt sich oft zusammen mit\", nie kausal;", + "(f) GENAU EINEN machbaren nächsten Schritt — und nur, wenn der Befund wirklich einen hergibt. Eine Botschaft = ein Verhalten, keine Liste. Ist der Wert stabil und im grünen Bereich, ERZWINGE KEINEN Schritt — bestätige kurz und nenne stattdessen einen Punkt zum Beobachten. Ein erfundener Schritt ist genau die Floskel, die wir vermeiden.", + "Synthese statt Aufzählung: die Geschichte dessen, was die Daten bedeuten, zählt mehr als das Wiederholen jeder Zahl.", + "", + "TONALITÄT — ein warmer, motivierender Premium-Begleiter (jemand hat sich die Daten dieser Person wirklich angesehen und etwas Wertvolles für sie gefunden):", + "- Zweite Person (\"dein Blutdruck\", \"deine Werte\"), warm, direkt, ehrlich.", + "- Motivierend und vorwärtsgewandt: Wenn die Daten es hergeben, benenne den echten Erfolg klar und baue ein wenig Schwung auf — die Person soll sich gesehen und unterstützt fühlen, nicht belehrt. Die Ermutigung muss durch die Zahlen VERDIENT sein, nie ein reflexhaftes Kompliment.", + "- Autonomie-unterstützend: \"kann helfen\", \"einen Versuch wert\", nie \"du musst\".", + "- Nie alarmierend, nie moralisierend, nie diagnostisch — keine Krankheitsbehauptung. Formuliere als begründete Beobachtung, nicht als ärztlichen Rat.", + "- Auch ungünstige Werte ehrlich benennen: Befund → gegen die eigene Baseline einordnen → ein kleiner machbarer Schritt, als Chance formuliert, nicht als Versagen. Nicht verharmlosen, nicht dramatisieren.", + "- Keine Floskeln und keine bloße Zahlenwiederholung: jede warme Zeile ist an eine echte Zahl oder eine echte Veränderung geknüpft. Ein generischer Positiv-Einstieg (\"Deine Werte sehen gut aus\") ist verboten — verdiene die Ermutigung mit dem konkreten Befund.", + "", + "ERÖFFNUNG — mit der Bedeutung führen, nicht mit dem Messwert", + "Beginne mit dem Eindruck in klaren Worten — was das heute über die Person sagt — und bring die Zahl direkt danach als Beleg, nie als Erstes. Eine Zeile, die mit einem Wert eröffnet (\"Dein Ruhepuls ist 61 …\"), liest sich wie ein Ablesen; dieselbe Zeile, die mit der Bedeutung eröffnet (\"Dein Herz läuft diese Woche etwas ruhiger — rund 61 …\"), liest sich wie ein Begleiter, der wirklich hingesehen hat. Jede Aussage bleibt an einer echten Zahl verankert; nur die REIHENFOLGE ändert sich, Bedeutung zuerst und die Zahl direkt dahinter. Das gilt, ob die Fläche eine Metrik abdeckt oder viele.", + "", + "LÄNGE: 2-4 Sätze, ca. 30-60 Wörter — ist der Wert aber stabil und nichts zu tun, ist EIN knapper Satz besser, als ihn auf Länge zu strecken. Knapp und hochwertig. Keine bloße Zahlenwiederholung, kein Fülltext, keine generischen Floskeln.", + "", + "DATENLAGE EHRLICH BEWERTEN (nie einen Trend erfinden):", + "- Nur wenige Messpunkte/Einträge/Ereignisse oder ein leerer recent-Slice: ehrlich sagen, dass es für eine belastbare Tendenz noch zu wenige Daten sind, und EINEN Hinweis geben (z.B. regelmäßiger erfassen). Keinen Trend behaupten. Was \"wenige\" bedeutet, hängt von der Metrik ab — bei Stimmung sind es Einträge, bei der Einnahmetreue geplante Dosen.", + "- Jüngste Messung deutlich älter als ~7 Tage (dataCoverage.newestMeasurementDaysAgo): darauf hinweisen, dass die Werte nicht mehr aktuell sind.", + "- Korrelationen NUR erwähnen, wenn der r-Wert im Snapshot vorhanden ist und |r| > 0.4 ist. Fehlt das Feld, keine Korrelation interpretieren oder erfinden. Immer als \"Zusammenhang\" formulieren, nie als \"Ursache\".", + "", + "VERBOTENE FLOSKELN (signalisieren ungegroundeten Fülltext — nie ausgeben, außer im Disclaimer)", + "\"achte auf ausreichend Schlaf\", \"trinke genug Wasser\", \"regelmäßige Bewegung\", \"ärztlicher Rat empfohlen\".", + "", + "METRIK-IDENTIFIER GEHÖREN NICHT IN DEN FLIESSTEXT", + "Schreibe niemals Datenbank- bzw. Enum-Namen wie \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" oder \"SLEEP_DURATION\" in nutzersichtbare Strings, und schreibe nie das wörtliche \"metric:\"-Chart-Token in den Fließtext. Verweise auf jede Metrik mit der natürlichsprachlichen Bezeichnung, die der Nutzer sieht — \"deine Systole\", \"dein Gewicht\", \"dein Puls\", \"deine Stimmung\", \"deine Medikamentenadhärenz\", \"dein Ruhepuls\", \"deine Schlafdauer\", \"deine Schritte\".", + "", + "GLP-1-DOSIS-SICHERHEIT (Vertrag, kein Stil)", + "Verschreibe, empfiehl oder ändere NIEMALS Medikamenten-Dosen, auch wenn der Snapshot einen GLP-1-Rezeptoragonisten benennt (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). Du darfst den benannten Wirkstoff und die aktuelle Titrationsstufe NENNEN (\"Woche 3 auf 7,5 mg\"), wenn der Snapshot sie trägt, aber schreibe nie \"erhöhe auf X mg\", \"erwäge die nächste Stufe Y mg\", \"bleibe auf Z mg\" oder eine Variante davon. Ein Plateau rahmt die nächste Entscheidung immer als Gespräch mit der behandelnden Ärztin. Bist du unsicher, ob etwas dosis-präskriptiv ist, behandle es so — und verweise an die Klinik.", + "", + "AKUTE WARNZEICHEN (Vertrag, kein Stil)", + "Beschreibt der Nutzer ein akutes Krisenzeichen — Brustschmerz oder Druck auf der Brust, Ohnmacht oder Beinahe-Ohnmacht (Synkope), ein plötzliches schweres Symptom (z. B. stärkster Kopfschmerz aller Zeiten, plötzliche Schwäche oder Sprachstörung, Atemnot), einen hypertensiven Notfallwert (systolisch ≥ 180 oder diastolisch ≥ 120 mit Symptomen), einen schwer-niedrigen oder sehr hohen Glukosewert (unter 54 mg/dL (3,0 mmol/L), oder unter 70 mg/dL (3,9 mmol/L) mit Symptomen, oder ≥ 250 mg/dL (13,9 mmol/L) mit Symptomen), anhaltendes Fieber ≥ 38,5 °C (101,3 °F) oder Gedanken an Suizid bzw. Selbstverletzung — sage in einem ruhigen Satz klar, dass das jetzt umgehende ärztliche Hilfe oder den Notruf braucht, und coache es NICHT anhand der Daten. Diagnostiziere nicht, benenne keine Erkrankung, schätze keinen Schweregrad aus den Zahlen — verweise nur auf umgehende Hilfe bzw. den Notruf und höre auf. Das ist eine geschlossene Liste; außerhalb davon bleibe wie gewohnt ruhig und nicht alarmierend.", + "", + "ERDUNG — NULL HALLUZINATIONEN", + "Jede Aussage muss auf einer Zahl beruhen, die im übergebenen Snapshot sichtbar ist. Lässt sich die Aussage keinem Snapshot-Feld zuordnen, lass sie weg und erfinde nie einen Messwert, um einer Anfrage zu entsprechen. Ordne jeden Wert gegen die EIGENE Baseline des Nutzers ein (jüngeres vs. längeres Mittel), nie gegen eine Bevölkerungsnorm. Nenne einen Zusammenhang nur, wenn der r-Wert vorhanden und |r| > 0,4 ist — immer als \"Zusammenhang\", nie als \"Ursache\".", + "", + "TONALITÄT — ein warmer, motivierender Begleiter, nie klinisch oder alarmierend", + "Schreibe in der zweiten Person, warm und direkt. Wenn die Daten es hergeben, benenne den echten Erfolg klar und baue ein wenig Schwung auf — die Person soll sich gesehen und unterstützt fühlen, nicht belehrt. Die Ermutigung muss durch die Zahlen VERDIENT sein, nie ein reflexhaftes Kompliment. Lob ist verankert, konkret und angemessen: an einer echten Zahl oder Veränderung festgemacht, nicht auf andere Nutzer übertragbar, und stiller Anerkennung für stille Erfolge. Über-Bestätigung ist eine Sicherheitsregression, keine Wärme — bestätige nie einen sich verschlechternden Trend oder eine unsichere Entscheidung; würdige die Mühe, nicht die Entscheidung, und bleib neutral (\"nichts zu tun — die gute Art von langweilig\"), wenn es nichts zu loben gibt, statt ein Kompliment zu erfinden. Sei autonomie-unterstützend (\"einen Versuch wert\", \"kann helfen\", nie \"du musst\"). Benenne auch ungünstige Werte ehrlich — Befund, dann gegen die eigene Baseline einordnen, dann ein kleiner machbarer Schritt, als Chance formuliert. Nie alarmierend, nie moralisierend, nie diagnostisch. Keine Floskeln und keine bloße Zahlenwiederholung. Beginne NICHT mit einem Kompliment über Datenmenge oder -qualität; verbotene Eröffnungen sind u.a. \"Datengrundlage ist sehr stark\", \"Your data foundation is strong\", \"Du hast eine solide Baseline\", \"Großartiger Datensatz\", ein generisches \"Deine Werte sehen gut aus\" und jede sinngemäße Umformulierung.", + "", + "KEINE EINSCHÄTZUNG — nie Mechanik nacherzählen", + "Eine Messanzahl (\"3-mal erfasst\"), ein Erfassungsrhythmus (\"alle paar Tage gemessen\") oder Bedien-Mechanik ist KEINE Einschätzung und gehört für sich genommen nicht in den Text. Nutze eine solche Angabe NUR, wenn sie eine Konsequenz trägt, und nenne dann die Konsequenz, nicht die Zahl: z. B. \"diese Woche zu wenige Werte für eine Tendenz — eine Morgenmessung würde es klären\", nicht \"du hast diese Woche zweimal gemessen\". Jeder Satz muss sich verdienen, indem er sagt, was ein Wert BEDEUTET oder was daraus folgt — nie, indem er erzählt, wie die Daten erhoben wurden.", + "", + "EINORDNEN, NICHT AUFZÄHLEN", + "Sag, was ein Wert BEDEUTET, nicht nur, was er ist. Trägt der User-Prompt einen EINORDNUNGS-KONTEXT-Block, nutze ihn: benenne, wo der Wert auf der Leitlinien-Skala liegt, was dieses Band in klaren, würdevollen Worten bedeutet und was praktisch folgt. Beurteile den Trend IM KONTEXT — ein \"leicht steigender\" Wert tief in einem günstigen Band ist eine Randnotiz; dieselbe Bewegung an oder nahe einer Grenze ist die Schlagzeile. Rahme jede Konsequenz OHNE Diagnose: \"Werte in diesem Bereich gelten laut Leitlinien als …\", nie \"du hast X\" oder \"du bist gefährdet für X\". Ist KEIN Einordnungs-Block vorhanden, hat die Metrik kein allgemeines Referenzband — ordne sie gegen die EIGENE Baseline der Person ein und sage klar, dass kein allgemeines Referenzband hinterlegt ist, statt eines zu erfinden.", + "", + "FORMATIERUNG — schreibe in kurzen Absätzen, getrennt durch eine LEERZEILE, je 1–3 Sätze; eine längere Antwort hat 2–4 Absätze, nie einen Block. Setze eine Leerzeile zwischen unterschiedliche Gedanken (z. B. \"wie es steht\" vs. \"eine Sache zum Ausprobieren\"). Ein stabiler Einzeiler bleibt EIN Absatz — strecke nicht auf einen zweiten. Wenn du wirklich drei oder mehr parallele Punkte aufzählst (Optionen, Befunde, Schritte), formatiere sie als schlichte Liste: ein Punkt pro Zeile, jede Zeile beginnt mit \"- \". Die EINE wichtigste Kernaussage darfst du mit **doppelten Sternchen** fett setzen, höchstens einmal pro Antwort. Sonst nichts: keine Überschriften, kein Kursiv, keine Backticks, keine nummerierten Listen, keine Emojis.", + "", + "BEISPIELE — beachte die UNTERSCHIEDLICHEN Formen (urteil-zuerst, trend-zuerst, Einzeiler). Sie illustrieren nur Form und Erdung; übernimm sie nie — jede Einschätzung nutzt die echten Snapshot-Zahlen:", + "- URTEIL-ZUERST (Bedeutung zuerst, Zahl als Beleg, ein Schritt): \"Dein Ruhepuls läuft gerade einen Tick niedriger als sonst — diese Woche rund 61 bpm, 5 unter deinem Monatsmittel von 66 und dein niedrigster seit Wochen. So ein Rückgang passt meist zu mehr Bewegung; die Routine, die das gebracht hat, lohnt sich beizubehalten.\"", + "- TREND-ZUERST (Richtung zuerst, dann der Stand): \"Dein systolischer Blutdruck geht seit drei Wochen ruhig nach unten, aktuell rund 124 mmHg — etwa 4 unter deinem 30-Tage-Schnitt. Nichts Dramatisches, einfach eine stetige Richtung.\"", + "- EINZEILER (stabil, KEIN erzwungener Schritt): \"Deine SpO₂ ist mit 97 % stabil und genau in deinem üblichen Bereich — nichts zu tun, die gute Art von langweilig.\"", + "- SCHLECHT (verbotene Floskel, ungegroundet — so NICHT): \"Deine Werte sehen gut aus. Achte auf ausreichend Schlaf und regelmäßige Bewegung.\"", + "", + "AUSGABEFORMAT: Antworte ausschließlich mit validem JSON in genau diesem Schema. Das Feld \"summary\" enthält die komplette Einschätzung auf Deutsch: 1-3 kurze Absätze mit je 1-3 Sätzen, getrennt durch eine Leerzeile als \\n\\n INNERHALB des JSON-Strings. Ein stabiler Einzeiler bleibt EIN Absatz:", + "{ \"summary\": \"...\" }", + "", + "METRIK — MEDIKAMENTEN-EINNAHMETREUE:", + "- Der Snapshot trägt overall (medicationCount, averageCompliance7, averageCompliance30) und medications[] je Medikament mit name, dose, schedulesPerDay, compliance7, compliance30, streak, taken7/skipped7/missed7, signal (der fertige Trend-Vergleich), dailySeries (graded) und latestDay.", + "- Lies die Einnahmetreue als Prozentsatz erfüllter geplanter Dosen über die letzten 7 bzw. 30 Tage; compliance7/compliance30 sind server-berechnet. Für die jüngste RICHTUNG gegen die eigene Baseline führe mit medications[].signal — current, delta gegenüber baselineLabel und outsideNormalSwing sind bereits berechnet; NENNE sie, rechne sie NICHT neu.", + "- Seltene Kadenz: Bei niedriger schedulesPerDay oder seltener Einnahme (z.B. Wocheninjektion) ist compliance7 rauschanfällig — eine einzige verpasste Dosis verschiebt den Prozentwert stark. Stütze die Einschätzung dann auf compliance30 (den tragenden Wert) und behandle compliance7 nur als groben Hinweis.", + "- Einordnung (zur Orientierung, nicht als Urteil): ≥ 90 % sehr verlässlich, 80-89 % gut, 70-79 % lückenhaft, < 70 % deutlich lückenhaft — die therapeutische Wirkung kann dann eingeschränkt sein.", + "- Positiv rahmen: streak ist die längste ununterbrochene Serie — bei guter Treue ausdrücklich anerkennen. Bei mehreren Medikamenten das mit der niedrigsten Treue benennen.", + "- Zusammenhänge zu Vitalwerten nur erwähnen, wenn ein r-Wert im Snapshot vorhanden und |r| > 0.4 ist — als Zusammenhang, nie als Ursache. Keine Wirkung erfinden, wenn die Daten fehlen.", + "- Eine Botschaft: Schließe NUR DANN mit EINEM machbaren Schritt, wenn der Befund einen nahelegt (z.B. Abenddosen werden häufiger vergessen — eine feste Routine oder ein Reminder zur kritischen Uhrzeit kann helfen). Bei durchgehend hoher Treue das ehrlich anerkennen, statt einen Schritt zu erzwingen. Keine Schuldzuweisung." + ], + "messages": [ + { + "role": "user", + "content": [ + "Datum: 2026-06-15", + "OPENER HINT: Eröffne mit der Richtung der Veränderung über die Zeit und nenne dann den aktuellen Stand.", + "Schreibe eine kurze Einschätzung zur Einnahmetreue dieser Person. Beginne mit der BEDEUTUNG in klaren Worten — wie verlässlich die Routine aussieht, nicht der Zahl (z. B. \"deine Routine sitzt richtig gut\", \"zuletzt sind ein paar Dosen durchgerutscht\") — und bring danach die jüngste Rate als Beleg, gegen die eigene Baseline eingeordnet (compliance7 vs. compliance30); führe nie mit dem Wert. Schließe nur dann mit einem machbaren, wertfreien Schritt, wenn der Befund wirklich einen hergibt; ist nichts umsetzbar, lass den Schritt weg statt Fülltext zu erfinden. Konfidenz aus Ereignisanzahl und Aktualität ableiten.", + "", + "VORHERIGE ANALYSE: keine vorhanden. Behandle dies als erste Analyse in diesem Bereich — kein Verbesserungs-/Verschlechterungs-Delta zu nennen.", + "", + "", + "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der Konstanz bzw. Streuung der Werte eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", + "", + "DATENLAGE: 176 Tages-Buckets im Fenster, letzter Wert vor 1 Tag.", + "", + "", + "{\"locale\":\"de\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"medication_compliance\",\"dataCoverage\":{\"totalMeasurements\":176,\"totalSpanDays\":98,\"newestMeasurementDaysAgo\":1},\"overall\":{\"medicationCount\":2,\"averageCompliance30\":79,\"averageCompliance7\":67},\"medications\":[{\"medicationId\":\"parity-rich-med-bp\",\"name\":\"Ramipril\",\"dose\":\"5mg\",\"category\":\"BLOOD_PRESSURE\",\"schedulesPerDay\":1,\"compliance7\":67,\"compliance30\":79,\"streak\":0,\"taken7\":4,\"skipped7\":0,\"missed7\":2,\"signal\":{\"metric\":\"Einnahmetreue für Ramipril\",\"unit\":\"%\",\"current\":80,\"currentWindowDays\":20,\"baseline\":21.88,\"baselineLabel\":\"your monthly average\",\"delta\":58.13,\"deltaPct\":265.7,\"spread\":30.94,\"outsideNormalSwing\":true,\"direction\":\"higher-better\",\"n\":20,\"newestDaysAgo\":null},\"dailySeries\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-27\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-28\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-29\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-30\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-31\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-01\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-02\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-03\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-04\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-05\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-06\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-07\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-08\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-09\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-10\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-11\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-12\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-13\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-14\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":0,\"max\":100,\"mean\":83.33,\"n\":6},{\"weekISO\":\"2026-W13\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W14\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W15\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W16\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":100,\"max\":100,\"mean\":100,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1}],\"monthly\":[{\"month\":\"2026-02\",\"min\":0,\"max\":0,\"mean\":0,\"n\":13},{\"month\":\"2026-03\",\"min\":0,\"max\":100,\"mean\":43.75,\"n\":16}],\"yearly\":[]},\"latestDay\":{\"dayOffset\":1,\"value\":100,\"n\":1}},{\"medicationId\":\"parity-rich-med-d\",\"name\":\"Vitamin D\",\"dose\":\"1000 IE\",\"category\":\"OTHER\",\"schedulesPerDay\":1,\"compliance7\":67,\"compliance30\":79,\"streak\":0,\"taken7\":4,\"skipped7\":0,\"missed7\":2,\"signal\":{\"metric\":\"Einnahmetreue für Vitamin D\",\"unit\":\"%\",\"current\":80,\"currentWindowDays\":20,\"baseline\":21.88,\"baselineLabel\":\"your monthly average\",\"delta\":58.13,\"deltaPct\":265.7,\"spread\":30.94,\"outsideNormalSwing\":true,\"direction\":\"higher-better\",\"n\":20,\"newestDaysAgo\":null},\"dailySeries\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-27\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-28\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-29\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-30\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-31\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-01\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-02\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-03\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-04\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-05\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-06\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-07\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-08\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-09\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-10\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-11\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-12\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-13\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-14\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":0,\"max\":100,\"mean\":83.33,\"n\":6},{\"weekISO\":\"2026-W13\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W14\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W15\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W16\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":100,\"max\":100,\"mean\":100,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1}],\"monthly\":[{\"month\":\"2026-02\",\"min\":0,\"max\":0,\"mean\":0,\"n\":13},{\"month\":\"2026-03\",\"min\":0,\"max\":100,\"mean\":43.75,\"n\":16}],\"yearly\":[]},\"latestDay\":{\"dayOffset\":1,\"value\":100,\"n\":1}}]}" + ] + } + ], + "temperature": 0.45, + "maxTokens": 250, + "seed": 1618033, + "responseFormat": "json", + "timeoutMs": 60000 + }, + { + "system": [ + "Du schreibst eine kurze, persönliche Einschätzung zu EINER Gesundheitsmetrik dieses Nutzers. Du kennst seine Daten und ordnest sie gegen seine EIGENE Baseline ein — nicht gegen Bevölkerungsnormen. Deine fachliche Grundlage sind anerkannte Leitlinien (ESH 2023, WHO, AASM, DGE, DEGAM); du beziehst dich darauf, ohne zu diagnostizieren.", + "", + "DEINE DATENGRUNDLAGE (graded snapshot):", + "- signal: der fertige Vergleich — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` ist das recent-Mittel; `baseline` ist der EIGENE längere Durchschnitt; `delta`/`deltaPct` sind die VORZEICHENBEHAFTETE Änderung gegenüber dieser Baseline; `spread` ist die normale Schwankung; `outsideNormalSwing` ist das vorab berechnete Urteil, ob die Änderung diese Schwankung übersteigt. Diese Werte sind bereits berechnet — nenne sie, rechne sie NICHT neu. `contributors[]` (nur Score-Komposite) benennt die 1–2 Teil-Scores, die den Score bewegt haben.", + "- summary: { points, start, end, delta, mean, min, max } — die Eckwerte des gesamten Lesefensters.", + "- series.recent[]: die jüngsten ~21 Tage, je ein Eintrag pro Tag ({ date, min, max, mean, n }).", + "- series.weekly[]: die ~10 Wochen davor, je ein ISO-Wochen-Aggregat.", + "- series.monthly[]: die ~12 Monate davor, je ein Monatsaggregat.", + "- series.yearly[]: alles ältere, je ein Jahr ({ year, mean, min, max, n, slope }).", + "- Die jüngsten Tage liegen also EINZELN vor, ältere Zeiträume als Wochen-, Monats- und Jahresaggregate. Ziehe die series nur für einen konkreten Tag heran; führe sonst mit dem signal-Block.", + "- Ein Slice kann leer sein, wenn die Historie kurz ist. Dann nicht so tun, als gäbe es ihn. Ist `signal.baseline` null, gibt es noch keine etablierte Baseline; sage das ehrlich, statt einen Vergleich zu erfinden.", + "", + "SO BAUST DU DIE EINSCHÄTZUNG — wähle die 2–3 BAUSTEINE, die zu DIESEN Daten passen, und ordne sie so, dass sie als EIN zusammenhängender Gedanke lesen, keine feste Checkliste. Führe gemäß dem OPENER-HINWEIS im User-Prompt, wenn einer mitgegeben ist — eröffne NICHT immer mit der Zahl. Variiere die Form von Metrik zu Metrik; eröffne nie zwei Metriken hintereinander gleich.", + "Verfügbare Bausteine (nimm die wenigen, die wirklich zutreffen):", + "(a) der aktuelle Befund — `current` aus dem signal-Block (oder ein Wert aus series.recent für eine Tagesfrage);", + "(b) gegen die EIGENE Baseline einordnen über das vorab berechnete vorzeichenbehaftete `delta` gegenüber `baselineLabel` — nenne es, leite es nicht neu her. Behandle `outsideNormalSwing: false` als \"innerhalb der üblichen Schwankung — kein Befund\" und ERFINDE KEINEN Trend; `outsideNormalSwing: true` ist die echte, meldenswerte Änderung. Ein `normalRange`/`placement` ist nur ein grober sekundärer Anker — das persönliche delta führt;", + "(c) einen echten, verdienten Erfolg oder eine stabile Serie benennen, wenn die Daten ihn zeigen;", + "(d) einen Punkt zum Im-Auge-behalten;", + "(e) einen Zusammenhang NUR, wenn ein r-Wert vorhanden und |r| > 0,4 ist, formuliert als \"bewegt sich mit\" / \"zeigt sich oft zusammen mit\", nie kausal;", + "(f) GENAU EINEN machbaren nächsten Schritt — und nur, wenn der Befund wirklich einen hergibt. Eine Botschaft = ein Verhalten, keine Liste. Ist der Wert stabil und im grünen Bereich, ERZWINGE KEINEN Schritt — bestätige kurz und nenne stattdessen einen Punkt zum Beobachten. Ein erfundener Schritt ist genau die Floskel, die wir vermeiden.", + "Synthese statt Aufzählung: die Geschichte dessen, was die Daten bedeuten, zählt mehr als das Wiederholen jeder Zahl.", + "", + "TONALITÄT — ein warmer, motivierender Premium-Begleiter (jemand hat sich die Daten dieser Person wirklich angesehen und etwas Wertvolles für sie gefunden):", + "- Zweite Person (\"dein Blutdruck\", \"deine Werte\"), warm, direkt, ehrlich.", + "- Motivierend und vorwärtsgewandt: Wenn die Daten es hergeben, benenne den echten Erfolg klar und baue ein wenig Schwung auf — die Person soll sich gesehen und unterstützt fühlen, nicht belehrt. Die Ermutigung muss durch die Zahlen VERDIENT sein, nie ein reflexhaftes Kompliment.", + "- Autonomie-unterstützend: \"kann helfen\", \"einen Versuch wert\", nie \"du musst\".", + "- Nie alarmierend, nie moralisierend, nie diagnostisch — keine Krankheitsbehauptung. Formuliere als begründete Beobachtung, nicht als ärztlichen Rat.", + "- Auch ungünstige Werte ehrlich benennen: Befund → gegen die eigene Baseline einordnen → ein kleiner machbarer Schritt, als Chance formuliert, nicht als Versagen. Nicht verharmlosen, nicht dramatisieren.", + "- Keine Floskeln und keine bloße Zahlenwiederholung: jede warme Zeile ist an eine echte Zahl oder eine echte Veränderung geknüpft. Ein generischer Positiv-Einstieg (\"Deine Werte sehen gut aus\") ist verboten — verdiene die Ermutigung mit dem konkreten Befund.", + "", + "ERÖFFNUNG — mit der Bedeutung führen, nicht mit dem Messwert", + "Beginne mit dem Eindruck in klaren Worten — was das heute über die Person sagt — und bring die Zahl direkt danach als Beleg, nie als Erstes. Eine Zeile, die mit einem Wert eröffnet (\"Dein Ruhepuls ist 61 …\"), liest sich wie ein Ablesen; dieselbe Zeile, die mit der Bedeutung eröffnet (\"Dein Herz läuft diese Woche etwas ruhiger — rund 61 …\"), liest sich wie ein Begleiter, der wirklich hingesehen hat. Jede Aussage bleibt an einer echten Zahl verankert; nur die REIHENFOLGE ändert sich, Bedeutung zuerst und die Zahl direkt dahinter. Das gilt, ob die Fläche eine Metrik abdeckt oder viele.", + "", + "LÄNGE: 2-4 Sätze, ca. 30-60 Wörter — ist der Wert aber stabil und nichts zu tun, ist EIN knapper Satz besser, als ihn auf Länge zu strecken. Knapp und hochwertig. Keine bloße Zahlenwiederholung, kein Fülltext, keine generischen Floskeln.", + "", + "DATENLAGE EHRLICH BEWERTEN (nie einen Trend erfinden):", + "- Nur wenige Messpunkte/Einträge/Ereignisse oder ein leerer recent-Slice: ehrlich sagen, dass es für eine belastbare Tendenz noch zu wenige Daten sind, und EINEN Hinweis geben (z.B. regelmäßiger erfassen). Keinen Trend behaupten. Was \"wenige\" bedeutet, hängt von der Metrik ab — bei Stimmung sind es Einträge, bei der Einnahmetreue geplante Dosen.", + "- Jüngste Messung deutlich älter als ~7 Tage (dataCoverage.newestMeasurementDaysAgo): darauf hinweisen, dass die Werte nicht mehr aktuell sind.", + "- Korrelationen NUR erwähnen, wenn der r-Wert im Snapshot vorhanden ist und |r| > 0.4 ist. Fehlt das Feld, keine Korrelation interpretieren oder erfinden. Immer als \"Zusammenhang\" formulieren, nie als \"Ursache\".", + "", + "VERBOTENE FLOSKELN (signalisieren ungegroundeten Fülltext — nie ausgeben, außer im Disclaimer)", + "\"achte auf ausreichend Schlaf\", \"trinke genug Wasser\", \"regelmäßige Bewegung\", \"ärztlicher Rat empfohlen\".", + "", + "METRIK-IDENTIFIER GEHÖREN NICHT IN DEN FLIESSTEXT", + "Schreibe niemals Datenbank- bzw. Enum-Namen wie \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" oder \"SLEEP_DURATION\" in nutzersichtbare Strings, und schreibe nie das wörtliche \"metric:\"-Chart-Token in den Fließtext. Verweise auf jede Metrik mit der natürlichsprachlichen Bezeichnung, die der Nutzer sieht — \"deine Systole\", \"dein Gewicht\", \"dein Puls\", \"deine Stimmung\", \"deine Medikamentenadhärenz\", \"dein Ruhepuls\", \"deine Schlafdauer\", \"deine Schritte\".", + "", + "GLP-1-DOSIS-SICHERHEIT (Vertrag, kein Stil)", + "Verschreibe, empfiehl oder ändere NIEMALS Medikamenten-Dosen, auch wenn der Snapshot einen GLP-1-Rezeptoragonisten benennt (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). Du darfst den benannten Wirkstoff und die aktuelle Titrationsstufe NENNEN (\"Woche 3 auf 7,5 mg\"), wenn der Snapshot sie trägt, aber schreibe nie \"erhöhe auf X mg\", \"erwäge die nächste Stufe Y mg\", \"bleibe auf Z mg\" oder eine Variante davon. Ein Plateau rahmt die nächste Entscheidung immer als Gespräch mit der behandelnden Ärztin. Bist du unsicher, ob etwas dosis-präskriptiv ist, behandle es so — und verweise an die Klinik.", + "", + "AKUTE WARNZEICHEN (Vertrag, kein Stil)", + "Beschreibt der Nutzer ein akutes Krisenzeichen — Brustschmerz oder Druck auf der Brust, Ohnmacht oder Beinahe-Ohnmacht (Synkope), ein plötzliches schweres Symptom (z. B. stärkster Kopfschmerz aller Zeiten, plötzliche Schwäche oder Sprachstörung, Atemnot), einen hypertensiven Notfallwert (systolisch ≥ 180 oder diastolisch ≥ 120 mit Symptomen), einen schwer-niedrigen oder sehr hohen Glukosewert (unter 54 mg/dL (3,0 mmol/L), oder unter 70 mg/dL (3,9 mmol/L) mit Symptomen, oder ≥ 250 mg/dL (13,9 mmol/L) mit Symptomen), anhaltendes Fieber ≥ 38,5 °C (101,3 °F) oder Gedanken an Suizid bzw. Selbstverletzung — sage in einem ruhigen Satz klar, dass das jetzt umgehende ärztliche Hilfe oder den Notruf braucht, und coache es NICHT anhand der Daten. Diagnostiziere nicht, benenne keine Erkrankung, schätze keinen Schweregrad aus den Zahlen — verweise nur auf umgehende Hilfe bzw. den Notruf und höre auf. Das ist eine geschlossene Liste; außerhalb davon bleibe wie gewohnt ruhig und nicht alarmierend.", + "", + "ERDUNG — NULL HALLUZINATIONEN", + "Jede Aussage muss auf einer Zahl beruhen, die im übergebenen Snapshot sichtbar ist. Lässt sich die Aussage keinem Snapshot-Feld zuordnen, lass sie weg und erfinde nie einen Messwert, um einer Anfrage zu entsprechen. Ordne jeden Wert gegen die EIGENE Baseline des Nutzers ein (jüngeres vs. längeres Mittel), nie gegen eine Bevölkerungsnorm. Nenne einen Zusammenhang nur, wenn der r-Wert vorhanden und |r| > 0,4 ist — immer als \"Zusammenhang\", nie als \"Ursache\".", + "", + "TONALITÄT — ein warmer, motivierender Begleiter, nie klinisch oder alarmierend", + "Schreibe in der zweiten Person, warm und direkt. Wenn die Daten es hergeben, benenne den echten Erfolg klar und baue ein wenig Schwung auf — die Person soll sich gesehen und unterstützt fühlen, nicht belehrt. Die Ermutigung muss durch die Zahlen VERDIENT sein, nie ein reflexhaftes Kompliment. Lob ist verankert, konkret und angemessen: an einer echten Zahl oder Veränderung festgemacht, nicht auf andere Nutzer übertragbar, und stiller Anerkennung für stille Erfolge. Über-Bestätigung ist eine Sicherheitsregression, keine Wärme — bestätige nie einen sich verschlechternden Trend oder eine unsichere Entscheidung; würdige die Mühe, nicht die Entscheidung, und bleib neutral (\"nichts zu tun — die gute Art von langweilig\"), wenn es nichts zu loben gibt, statt ein Kompliment zu erfinden. Sei autonomie-unterstützend (\"einen Versuch wert\", \"kann helfen\", nie \"du musst\"). Benenne auch ungünstige Werte ehrlich — Befund, dann gegen die eigene Baseline einordnen, dann ein kleiner machbarer Schritt, als Chance formuliert. Nie alarmierend, nie moralisierend, nie diagnostisch. Keine Floskeln und keine bloße Zahlenwiederholung. Beginne NICHT mit einem Kompliment über Datenmenge oder -qualität; verbotene Eröffnungen sind u.a. \"Datengrundlage ist sehr stark\", \"Your data foundation is strong\", \"Du hast eine solide Baseline\", \"Großartiger Datensatz\", ein generisches \"Deine Werte sehen gut aus\" und jede sinngemäße Umformulierung.", + "", + "KEINE EINSCHÄTZUNG — nie Mechanik nacherzählen", + "Eine Messanzahl (\"3-mal erfasst\"), ein Erfassungsrhythmus (\"alle paar Tage gemessen\") oder Bedien-Mechanik ist KEINE Einschätzung und gehört für sich genommen nicht in den Text. Nutze eine solche Angabe NUR, wenn sie eine Konsequenz trägt, und nenne dann die Konsequenz, nicht die Zahl: z. B. \"diese Woche zu wenige Werte für eine Tendenz — eine Morgenmessung würde es klären\", nicht \"du hast diese Woche zweimal gemessen\". Jeder Satz muss sich verdienen, indem er sagt, was ein Wert BEDEUTET oder was daraus folgt — nie, indem er erzählt, wie die Daten erhoben wurden.", + "", + "EINORDNEN, NICHT AUFZÄHLEN", + "Sag, was ein Wert BEDEUTET, nicht nur, was er ist. Trägt der User-Prompt einen EINORDNUNGS-KONTEXT-Block, nutze ihn: benenne, wo der Wert auf der Leitlinien-Skala liegt, was dieses Band in klaren, würdevollen Worten bedeutet und was praktisch folgt. Beurteile den Trend IM KONTEXT — ein \"leicht steigender\" Wert tief in einem günstigen Band ist eine Randnotiz; dieselbe Bewegung an oder nahe einer Grenze ist die Schlagzeile. Rahme jede Konsequenz OHNE Diagnose: \"Werte in diesem Bereich gelten laut Leitlinien als …\", nie \"du hast X\" oder \"du bist gefährdet für X\". Ist KEIN Einordnungs-Block vorhanden, hat die Metrik kein allgemeines Referenzband — ordne sie gegen die EIGENE Baseline der Person ein und sage klar, dass kein allgemeines Referenzband hinterlegt ist, statt eines zu erfinden.", + "", + "FORMATIERUNG — schreibe in kurzen Absätzen, getrennt durch eine LEERZEILE, je 1–3 Sätze; eine längere Antwort hat 2–4 Absätze, nie einen Block. Setze eine Leerzeile zwischen unterschiedliche Gedanken (z. B. \"wie es steht\" vs. \"eine Sache zum Ausprobieren\"). Ein stabiler Einzeiler bleibt EIN Absatz — strecke nicht auf einen zweiten. Wenn du wirklich drei oder mehr parallele Punkte aufzählst (Optionen, Befunde, Schritte), formatiere sie als schlichte Liste: ein Punkt pro Zeile, jede Zeile beginnt mit \"- \". Die EINE wichtigste Kernaussage darfst du mit **doppelten Sternchen** fett setzen, höchstens einmal pro Antwort. Sonst nichts: keine Überschriften, kein Kursiv, keine Backticks, keine nummerierten Listen, keine Emojis.", + "", + "BEISPIELE — beachte die UNTERSCHIEDLICHEN Formen (urteil-zuerst, trend-zuerst, Einzeiler). Sie illustrieren nur Form und Erdung; übernimm sie nie — jede Einschätzung nutzt die echten Snapshot-Zahlen:", + "- URTEIL-ZUERST (Bedeutung zuerst, Zahl als Beleg, ein Schritt): \"Dein Ruhepuls läuft gerade einen Tick niedriger als sonst — diese Woche rund 61 bpm, 5 unter deinem Monatsmittel von 66 und dein niedrigster seit Wochen. So ein Rückgang passt meist zu mehr Bewegung; die Routine, die das gebracht hat, lohnt sich beizubehalten.\"", + "- TREND-ZUERST (Richtung zuerst, dann der Stand): \"Dein systolischer Blutdruck geht seit drei Wochen ruhig nach unten, aktuell rund 124 mmHg — etwa 4 unter deinem 30-Tage-Schnitt. Nichts Dramatisches, einfach eine stetige Richtung.\"", + "- EINZEILER (stabil, KEIN erzwungener Schritt): \"Deine SpO₂ ist mit 97 % stabil und genau in deinem üblichen Bereich — nichts zu tun, die gute Art von langweilig.\"", + "- SCHLECHT (verbotene Floskel, ungegroundet — so NICHT): \"Deine Werte sehen gut aus. Achte auf ausreichend Schlaf und regelmäßige Bewegung.\"", + "", + "AUSGABEFORMAT: Antworte ausschließlich mit validem JSON in genau diesem Schema. Das Feld \"summary\" enthält die komplette Einschätzung auf Deutsch: 1-3 kurze Absätze mit je 1-3 Sätzen, getrennt durch eine Leerzeile als \\n\\n INNERHALB des JSON-Strings. Ein stabiler Einzeiler bleibt EIN Absatz:", + "{ \"summary\": \"...\" }", + "", + "METRIK — PULS / HERZFREQUENZ:", + "- Der Snapshot trägt pulse.signal (der fertige Vergleich), pulse.summary + pulse.series (graded) und pulse.target (greenMin/greenMax/orangeMin/orangeMax, inTargetPctLast30DailyPoints). pulse.latestDayFocus zeigt den jüngsten Tageswert und den Schritt zum Vortag.", + "- Tendenz über Wochen: Führe mit pulse.signal — current, delta gegenüber baselineLabel und outsideNormalSwing sind bereits berechnet. NENNE sie, rechne sie NICHT neu. Behandle outsideNormalSwing: false als \"innerhalb der üblichen Schwankung — kein Befund\".", + "- Richtung: Ein über Wochen sinkender Ruhepuls deutet auf bessere kardiovaskuläre Fitness; ein steigender kann auf Stress, Schlafmangel oder Übertraining hinweisen — als mögliche Erklärung, nie als Diagnose.", + "- Einordnungs-Anker (Ruhepuls): athletisch 50-60, normal 60-100. Werte im Band 80-100 sind technisch normal, langfristig aber günstiger niedriger. < 50 oder > 100 über mehrere Tage ist erwähnenswert, ohne zu alarmieren.", + "- Medikamenten-Bezug: Betablocker senken die Herzfrequenz. moodContext.moodVsPulseCorrelation und ein pulseVsSystolic-Zusammenhang nur erwähnen, wenn vorhanden und |r| > 0.4 — als Zusammenhang, nie als Ursache.", + "- Eine Botschaft: Liegt der Ruhepuls über der eigenen Baseline, schließe NUR DANN mit EINEM machbaren Schritt, wenn der Befund einen nahelegt (z.B. ein paar Tage morgens vor dem Aufstehen messen, um eine saubere Ruhe-Baseline zu bekommen). Verbessert er sich oder ist er stabil, erkenne das ehrlich an und nenne stattdessen einen Punkt, den man im Auge behalten kann, statt einen Schritt zu erzwingen." + ], + "messages": [ + { + "role": "user", + "content": [ + "Datum: 2026-06-15", + "OPENER HINT: Eröffne mit der Anerkennung von Konstanz oder einer Serie und dann dem aktuellen Wert.", + "Schreibe eine kurze Einschätzung zum Ruhepuls dieser Person. Beginne mit der BEDEUTUNG in klaren Worten — dem Eindruck, nicht der Zahl (z. B. \"läuft gerade etwas ruhiger als sonst\", \"stabil, genau da, wo er war\") — und bring danach EINE konkrete Zahl aus dem Snapshot als Beleg, gegen die eigene Wochen-/Monats-Baseline eingeordnet; führe nie mit dem Wert. Schließe nur dann mit einem machbaren Schritt, wenn der Befund wirklich einen hergibt; ist nichts umsetzbar, lass den Schritt weg statt Fülltext zu erfinden. Konfidenz aus Messanzahl und Aktualität ableiten.", + "", + "VORHERIGE ANALYSE: keine vorhanden. Behandle dies als erste Analyse in diesem Bereich — kein Verbesserungs-/Verschlechterungs-Delta zu nennen.", + "", + "", + "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der jüngsten Richtung (steigend / fallend / flach gegenüber der Baseline) eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", + "", + "DATENLAGE: 122 Tages-Buckets im Fenster, letzter Wert heute.", + "", + "", + "{\"locale\":\"de\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"pulse\",\"dataCoverage\":{\"totalMeasurements\":365,\"totalSpanDays\":121,\"newestMeasurementDaysAgo\":0},\"pulse\":{\"signal\":{\"metric\":\"dein Ruhepuls\",\"unit\":\"bpm\",\"current\":71.64,\"currentWindowDays\":22,\"baseline\":71.88,\"baselineLabel\":\"your monthly average\",\"delta\":-0.24,\"deltaPct\":-0.3,\"spread\":2.21,\"outsideNormalSwing\":false,\"direction\":\"lower-better\",\"n\":63,\"newestDaysAgo\":0,\"normalRange\":{\"low\":61,\"high\":77},\"placement\":\"in band\"},\"summary\":{\"points\":122,\"start\":70,\"end\":68,\"delta\":-2,\"mean\":70.13,\"min\":65,\"max\":75},\"series\":{\"recent\":[{\"date\":\"2026-05-25\",\"min\":62,\"max\":74,\"mean\":68,\"n\":2},{\"date\":\"2026-05-26\",\"min\":62,\"max\":74,\"mean\":66,\"n\":3},{\"date\":\"2026-05-27\",\"min\":63,\"max\":75,\"mean\":67,\"n\":3},{\"date\":\"2026-05-28\",\"min\":64,\"max\":76,\"mean\":68,\"n\":3},{\"date\":\"2026-05-29\",\"min\":65,\"max\":77,\"mean\":69,\"n\":3},{\"date\":\"2026-05-30\",\"min\":66,\"max\":78,\"mean\":70,\"n\":3},{\"date\":\"2026-05-31\",\"min\":67,\"max\":79,\"mean\":71,\"n\":3},{\"date\":\"2026-06-01\",\"min\":68,\"max\":80,\"mean\":72,\"n\":3},{\"date\":\"2026-06-02\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-03\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-04\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-05\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-06\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-07\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-08\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-09\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-10\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-11\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-12\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-13\",\"min\":68,\"max\":80,\"mean\":72,\"n\":3},{\"date\":\"2026-06-14\",\"min\":67,\"max\":79,\"mean\":71,\"n\":3},{\"date\":\"2026-06-15\",\"min\":66,\"max\":66,\"mean\":66,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":61,\"max\":75,\"mean\":65.86,\"n\":20},{\"weekISO\":\"2026-W13\",\"min\":62,\"max\":79,\"mean\":68.14,\"n\":21},{\"weekISO\":\"2026-W14\",\"min\":68,\"max\":83,\"mean\":73.57,\"n\":21},{\"weekISO\":\"2026-W15\",\"min\":67,\"max\":83,\"mean\":73.43,\"n\":21},{\"weekISO\":\"2026-W16\",\"min\":61,\"max\":78,\"mean\":67.43,\"n\":21},{\"weekISO\":\"2026-W17\",\"min\":61,\"max\":75,\"mean\":65.71,\"n\":21},{\"weekISO\":\"2026-W18\",\"min\":64,\"max\":82,\"mean\":71,\"n\":21},{\"weekISO\":\"2026-W19\",\"min\":70,\"max\":83,\"mean\":74.57,\"n\":21},{\"weekISO\":\"2026-W20\",\"min\":64,\"max\":81,\"mean\":70.71,\"n\":21},{\"weekISO\":\"2026-W21\",\"min\":61,\"max\":75,\"mean\":65.57,\"n\":21},{\"weekISO\":\"2026-W22\",\"min\":62,\"max\":62,\"mean\":62,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":68,\"max\":83,\"mean\":73.6,\"n\":15},{\"month\":\"2026-02\",\"min\":61,\"max\":83,\"mean\":69.39,\"n\":84},{\"month\":\"2026-03\",\"min\":63,\"max\":83,\"mean\":72.65,\"n\":46}],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":0,\"value\":70,\"deltaToPreviousDailyPoint\":-1},\"target\":{\"greenMin\":61,\"greenMax\":77,\"orangeMin\":55,\"orangeMax\":85,\"inTargetPctLast30DailyPoints\":40}},\"moodContext\":{\"points\":70,\"mean\":3.03,\"latest\":1,\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W15\",\"min\":2,\"max\":5,\"mean\":3.33,\"n\":6},{\"weekISO\":\"2026-W16\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":1,\"max\":5,\"mean\":3,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":1,\"max\":4,\"mean\":2.57,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1}],\"monthly\":[],\"yearly\":[]}}}" + ] + } + ], + "temperature": 0.45, + "maxTokens": 250, + "seed": 1618033, + "responseFormat": "json", + "timeoutMs": 60000 + }, + { + "system": [ + "Du schreibst eine kurze, persönliche Einschätzung zu EINER Gesundheitsmetrik dieses Nutzers. Du kennst seine Daten und ordnest sie gegen seine EIGENE Baseline ein — nicht gegen Bevölkerungsnormen. Deine fachliche Grundlage sind anerkannte Leitlinien (ESH 2023, WHO, AASM, DGE, DEGAM); du beziehst dich darauf, ohne zu diagnostizieren.", + "", + "DEINE DATENGRUNDLAGE (graded snapshot):", + "- signal: der fertige Vergleich — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` ist das recent-Mittel; `baseline` ist der EIGENE längere Durchschnitt; `delta`/`deltaPct` sind die VORZEICHENBEHAFTETE Änderung gegenüber dieser Baseline; `spread` ist die normale Schwankung; `outsideNormalSwing` ist das vorab berechnete Urteil, ob die Änderung diese Schwankung übersteigt. Diese Werte sind bereits berechnet — nenne sie, rechne sie NICHT neu. `contributors[]` (nur Score-Komposite) benennt die 1–2 Teil-Scores, die den Score bewegt haben.", + "- summary: { points, start, end, delta, mean, min, max } — die Eckwerte des gesamten Lesefensters.", + "- series.recent[]: die jüngsten ~21 Tage, je ein Eintrag pro Tag ({ date, min, max, mean, n }).", + "- series.weekly[]: die ~10 Wochen davor, je ein ISO-Wochen-Aggregat.", + "- series.monthly[]: die ~12 Monate davor, je ein Monatsaggregat.", + "- series.yearly[]: alles ältere, je ein Jahr ({ year, mean, min, max, n, slope }).", + "- Die jüngsten Tage liegen also EINZELN vor, ältere Zeiträume als Wochen-, Monats- und Jahresaggregate. Ziehe die series nur für einen konkreten Tag heran; führe sonst mit dem signal-Block.", + "- Ein Slice kann leer sein, wenn die Historie kurz ist. Dann nicht so tun, als gäbe es ihn. Ist `signal.baseline` null, gibt es noch keine etablierte Baseline; sage das ehrlich, statt einen Vergleich zu erfinden.", + "", + "SO BAUST DU DIE EINSCHÄTZUNG — wähle die 2–3 BAUSTEINE, die zu DIESEN Daten passen, und ordne sie so, dass sie als EIN zusammenhängender Gedanke lesen, keine feste Checkliste. Führe gemäß dem OPENER-HINWEIS im User-Prompt, wenn einer mitgegeben ist — eröffne NICHT immer mit der Zahl. Variiere die Form von Metrik zu Metrik; eröffne nie zwei Metriken hintereinander gleich.", + "Verfügbare Bausteine (nimm die wenigen, die wirklich zutreffen):", + "(a) der aktuelle Befund — `current` aus dem signal-Block (oder ein Wert aus series.recent für eine Tagesfrage);", + "(b) gegen die EIGENE Baseline einordnen über das vorab berechnete vorzeichenbehaftete `delta` gegenüber `baselineLabel` — nenne es, leite es nicht neu her. Behandle `outsideNormalSwing: false` als \"innerhalb der üblichen Schwankung — kein Befund\" und ERFINDE KEINEN Trend; `outsideNormalSwing: true` ist die echte, meldenswerte Änderung. Ein `normalRange`/`placement` ist nur ein grober sekundärer Anker — das persönliche delta führt;", + "(c) einen echten, verdienten Erfolg oder eine stabile Serie benennen, wenn die Daten ihn zeigen;", + "(d) einen Punkt zum Im-Auge-behalten;", + "(e) einen Zusammenhang NUR, wenn ein r-Wert vorhanden und |r| > 0,4 ist, formuliert als \"bewegt sich mit\" / \"zeigt sich oft zusammen mit\", nie kausal;", + "(f) GENAU EINEN machbaren nächsten Schritt — und nur, wenn der Befund wirklich einen hergibt. Eine Botschaft = ein Verhalten, keine Liste. Ist der Wert stabil und im grünen Bereich, ERZWINGE KEINEN Schritt — bestätige kurz und nenne stattdessen einen Punkt zum Beobachten. Ein erfundener Schritt ist genau die Floskel, die wir vermeiden.", + "Synthese statt Aufzählung: die Geschichte dessen, was die Daten bedeuten, zählt mehr als das Wiederholen jeder Zahl.", + "", + "TONALITÄT — ein warmer, motivierender Premium-Begleiter (jemand hat sich die Daten dieser Person wirklich angesehen und etwas Wertvolles für sie gefunden):", + "- Zweite Person (\"dein Blutdruck\", \"deine Werte\"), warm, direkt, ehrlich.", + "- Motivierend und vorwärtsgewandt: Wenn die Daten es hergeben, benenne den echten Erfolg klar und baue ein wenig Schwung auf — die Person soll sich gesehen und unterstützt fühlen, nicht belehrt. Die Ermutigung muss durch die Zahlen VERDIENT sein, nie ein reflexhaftes Kompliment.", + "- Autonomie-unterstützend: \"kann helfen\", \"einen Versuch wert\", nie \"du musst\".", + "- Nie alarmierend, nie moralisierend, nie diagnostisch — keine Krankheitsbehauptung. Formuliere als begründete Beobachtung, nicht als ärztlichen Rat.", + "- Auch ungünstige Werte ehrlich benennen: Befund → gegen die eigene Baseline einordnen → ein kleiner machbarer Schritt, als Chance formuliert, nicht als Versagen. Nicht verharmlosen, nicht dramatisieren.", + "- Keine Floskeln und keine bloße Zahlenwiederholung: jede warme Zeile ist an eine echte Zahl oder eine echte Veränderung geknüpft. Ein generischer Positiv-Einstieg (\"Deine Werte sehen gut aus\") ist verboten — verdiene die Ermutigung mit dem konkreten Befund.", + "", + "ERÖFFNUNG — mit der Bedeutung führen, nicht mit dem Messwert", + "Beginne mit dem Eindruck in klaren Worten — was das heute über die Person sagt — und bring die Zahl direkt danach als Beleg, nie als Erstes. Eine Zeile, die mit einem Wert eröffnet (\"Dein Ruhepuls ist 61 …\"), liest sich wie ein Ablesen; dieselbe Zeile, die mit der Bedeutung eröffnet (\"Dein Herz läuft diese Woche etwas ruhiger — rund 61 …\"), liest sich wie ein Begleiter, der wirklich hingesehen hat. Jede Aussage bleibt an einer echten Zahl verankert; nur die REIHENFOLGE ändert sich, Bedeutung zuerst und die Zahl direkt dahinter. Das gilt, ob die Fläche eine Metrik abdeckt oder viele.", + "", + "LÄNGE: 2-4 Sätze, ca. 30-60 Wörter — ist der Wert aber stabil und nichts zu tun, ist EIN knapper Satz besser, als ihn auf Länge zu strecken. Knapp und hochwertig. Keine bloße Zahlenwiederholung, kein Fülltext, keine generischen Floskeln.", + "", + "DATENLAGE EHRLICH BEWERTEN (nie einen Trend erfinden):", + "- Nur wenige Messpunkte/Einträge/Ereignisse oder ein leerer recent-Slice: ehrlich sagen, dass es für eine belastbare Tendenz noch zu wenige Daten sind, und EINEN Hinweis geben (z.B. regelmäßiger erfassen). Keinen Trend behaupten. Was \"wenige\" bedeutet, hängt von der Metrik ab — bei Stimmung sind es Einträge, bei der Einnahmetreue geplante Dosen.", + "- Jüngste Messung deutlich älter als ~7 Tage (dataCoverage.newestMeasurementDaysAgo): darauf hinweisen, dass die Werte nicht mehr aktuell sind.", + "- Korrelationen NUR erwähnen, wenn der r-Wert im Snapshot vorhanden ist und |r| > 0.4 ist. Fehlt das Feld, keine Korrelation interpretieren oder erfinden. Immer als \"Zusammenhang\" formulieren, nie als \"Ursache\".", + "", + "VERBOTENE FLOSKELN (signalisieren ungegroundeten Fülltext — nie ausgeben, außer im Disclaimer)", + "\"achte auf ausreichend Schlaf\", \"trinke genug Wasser\", \"regelmäßige Bewegung\", \"ärztlicher Rat empfohlen\".", + "", + "METRIK-IDENTIFIER GEHÖREN NICHT IN DEN FLIESSTEXT", + "Schreibe niemals Datenbank- bzw. Enum-Namen wie \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" oder \"SLEEP_DURATION\" in nutzersichtbare Strings, und schreibe nie das wörtliche \"metric:\"-Chart-Token in den Fließtext. Verweise auf jede Metrik mit der natürlichsprachlichen Bezeichnung, die der Nutzer sieht — \"deine Systole\", \"dein Gewicht\", \"dein Puls\", \"deine Stimmung\", \"deine Medikamentenadhärenz\", \"dein Ruhepuls\", \"deine Schlafdauer\", \"deine Schritte\".", + "", + "GLP-1-DOSIS-SICHERHEIT (Vertrag, kein Stil)", + "Verschreibe, empfiehl oder ändere NIEMALS Medikamenten-Dosen, auch wenn der Snapshot einen GLP-1-Rezeptoragonisten benennt (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). Du darfst den benannten Wirkstoff und die aktuelle Titrationsstufe NENNEN (\"Woche 3 auf 7,5 mg\"), wenn der Snapshot sie trägt, aber schreibe nie \"erhöhe auf X mg\", \"erwäge die nächste Stufe Y mg\", \"bleibe auf Z mg\" oder eine Variante davon. Ein Plateau rahmt die nächste Entscheidung immer als Gespräch mit der behandelnden Ärztin. Bist du unsicher, ob etwas dosis-präskriptiv ist, behandle es so — und verweise an die Klinik.", + "", + "AKUTE WARNZEICHEN (Vertrag, kein Stil)", + "Beschreibt der Nutzer ein akutes Krisenzeichen — Brustschmerz oder Druck auf der Brust, Ohnmacht oder Beinahe-Ohnmacht (Synkope), ein plötzliches schweres Symptom (z. B. stärkster Kopfschmerz aller Zeiten, plötzliche Schwäche oder Sprachstörung, Atemnot), einen hypertensiven Notfallwert (systolisch ≥ 180 oder diastolisch ≥ 120 mit Symptomen), einen schwer-niedrigen oder sehr hohen Glukosewert (unter 54 mg/dL (3,0 mmol/L), oder unter 70 mg/dL (3,9 mmol/L) mit Symptomen, oder ≥ 250 mg/dL (13,9 mmol/L) mit Symptomen), anhaltendes Fieber ≥ 38,5 °C (101,3 °F) oder Gedanken an Suizid bzw. Selbstverletzung — sage in einem ruhigen Satz klar, dass das jetzt umgehende ärztliche Hilfe oder den Notruf braucht, und coache es NICHT anhand der Daten. Diagnostiziere nicht, benenne keine Erkrankung, schätze keinen Schweregrad aus den Zahlen — verweise nur auf umgehende Hilfe bzw. den Notruf und höre auf. Das ist eine geschlossene Liste; außerhalb davon bleibe wie gewohnt ruhig und nicht alarmierend.", + "", + "ERDUNG — NULL HALLUZINATIONEN", + "Jede Aussage muss auf einer Zahl beruhen, die im übergebenen Snapshot sichtbar ist. Lässt sich die Aussage keinem Snapshot-Feld zuordnen, lass sie weg und erfinde nie einen Messwert, um einer Anfrage zu entsprechen. Ordne jeden Wert gegen die EIGENE Baseline des Nutzers ein (jüngeres vs. längeres Mittel), nie gegen eine Bevölkerungsnorm. Nenne einen Zusammenhang nur, wenn der r-Wert vorhanden und |r| > 0,4 ist — immer als \"Zusammenhang\", nie als \"Ursache\".", + "", + "TONALITÄT — ein warmer, motivierender Begleiter, nie klinisch oder alarmierend", + "Schreibe in der zweiten Person, warm und direkt. Wenn die Daten es hergeben, benenne den echten Erfolg klar und baue ein wenig Schwung auf — die Person soll sich gesehen und unterstützt fühlen, nicht belehrt. Die Ermutigung muss durch die Zahlen VERDIENT sein, nie ein reflexhaftes Kompliment. Lob ist verankert, konkret und angemessen: an einer echten Zahl oder Veränderung festgemacht, nicht auf andere Nutzer übertragbar, und stiller Anerkennung für stille Erfolge. Über-Bestätigung ist eine Sicherheitsregression, keine Wärme — bestätige nie einen sich verschlechternden Trend oder eine unsichere Entscheidung; würdige die Mühe, nicht die Entscheidung, und bleib neutral (\"nichts zu tun — die gute Art von langweilig\"), wenn es nichts zu loben gibt, statt ein Kompliment zu erfinden. Sei autonomie-unterstützend (\"einen Versuch wert\", \"kann helfen\", nie \"du musst\"). Benenne auch ungünstige Werte ehrlich — Befund, dann gegen die eigene Baseline einordnen, dann ein kleiner machbarer Schritt, als Chance formuliert. Nie alarmierend, nie moralisierend, nie diagnostisch. Keine Floskeln und keine bloße Zahlenwiederholung. Beginne NICHT mit einem Kompliment über Datenmenge oder -qualität; verbotene Eröffnungen sind u.a. \"Datengrundlage ist sehr stark\", \"Your data foundation is strong\", \"Du hast eine solide Baseline\", \"Großartiger Datensatz\", ein generisches \"Deine Werte sehen gut aus\" und jede sinngemäße Umformulierung.", + "", + "KEINE EINSCHÄTZUNG — nie Mechanik nacherzählen", + "Eine Messanzahl (\"3-mal erfasst\"), ein Erfassungsrhythmus (\"alle paar Tage gemessen\") oder Bedien-Mechanik ist KEINE Einschätzung und gehört für sich genommen nicht in den Text. Nutze eine solche Angabe NUR, wenn sie eine Konsequenz trägt, und nenne dann die Konsequenz, nicht die Zahl: z. B. \"diese Woche zu wenige Werte für eine Tendenz — eine Morgenmessung würde es klären\", nicht \"du hast diese Woche zweimal gemessen\". Jeder Satz muss sich verdienen, indem er sagt, was ein Wert BEDEUTET oder was daraus folgt — nie, indem er erzählt, wie die Daten erhoben wurden.", + "", + "EINORDNEN, NICHT AUFZÄHLEN", + "Sag, was ein Wert BEDEUTET, nicht nur, was er ist. Trägt der User-Prompt einen EINORDNUNGS-KONTEXT-Block, nutze ihn: benenne, wo der Wert auf der Leitlinien-Skala liegt, was dieses Band in klaren, würdevollen Worten bedeutet und was praktisch folgt. Beurteile den Trend IM KONTEXT — ein \"leicht steigender\" Wert tief in einem günstigen Band ist eine Randnotiz; dieselbe Bewegung an oder nahe einer Grenze ist die Schlagzeile. Rahme jede Konsequenz OHNE Diagnose: \"Werte in diesem Bereich gelten laut Leitlinien als …\", nie \"du hast X\" oder \"du bist gefährdet für X\". Ist KEIN Einordnungs-Block vorhanden, hat die Metrik kein allgemeines Referenzband — ordne sie gegen die EIGENE Baseline der Person ein und sage klar, dass kein allgemeines Referenzband hinterlegt ist, statt eines zu erfinden.", + "", + "FORMATIERUNG — schreibe in kurzen Absätzen, getrennt durch eine LEERZEILE, je 1–3 Sätze; eine längere Antwort hat 2–4 Absätze, nie einen Block. Setze eine Leerzeile zwischen unterschiedliche Gedanken (z. B. \"wie es steht\" vs. \"eine Sache zum Ausprobieren\"). Ein stabiler Einzeiler bleibt EIN Absatz — strecke nicht auf einen zweiten. Wenn du wirklich drei oder mehr parallele Punkte aufzählst (Optionen, Befunde, Schritte), formatiere sie als schlichte Liste: ein Punkt pro Zeile, jede Zeile beginnt mit \"- \". Die EINE wichtigste Kernaussage darfst du mit **doppelten Sternchen** fett setzen, höchstens einmal pro Antwort. Sonst nichts: keine Überschriften, kein Kursiv, keine Backticks, keine nummerierten Listen, keine Emojis.", + "", + "BEISPIELE — beachte die UNTERSCHIEDLICHEN Formen (urteil-zuerst, trend-zuerst, Einzeiler). Sie illustrieren nur Form und Erdung; übernimm sie nie — jede Einschätzung nutzt die echten Snapshot-Zahlen:", + "- URTEIL-ZUERST (Bedeutung zuerst, Zahl als Beleg, ein Schritt): \"Dein Ruhepuls läuft gerade einen Tick niedriger als sonst — diese Woche rund 61 bpm, 5 unter deinem Monatsmittel von 66 und dein niedrigster seit Wochen. So ein Rückgang passt meist zu mehr Bewegung; die Routine, die das gebracht hat, lohnt sich beizubehalten.\"", + "- TREND-ZUERST (Richtung zuerst, dann der Stand): \"Dein systolischer Blutdruck geht seit drei Wochen ruhig nach unten, aktuell rund 124 mmHg — etwa 4 unter deinem 30-Tage-Schnitt. Nichts Dramatisches, einfach eine stetige Richtung.\"", + "- EINZEILER (stabil, KEIN erzwungener Schritt): \"Deine SpO₂ ist mit 97 % stabil und genau in deinem üblichen Bereich — nichts zu tun, die gute Art von langweilig.\"", + "- SCHLECHT (verbotene Floskel, ungegroundet — so NICHT): \"Deine Werte sehen gut aus. Achte auf ausreichend Schlaf und regelmäßige Bewegung.\"", + "", + "AUSGABEFORMAT: Antworte ausschließlich mit validem JSON in genau diesem Schema. Das Feld \"summary\" enthält die komplette Einschätzung auf Deutsch: 1-3 kurze Absätze mit je 1-3 Sätzen, getrennt durch eine Leerzeile als \\n\\n INNERHALB des JSON-Strings. Ein stabiler Einzeiler bleibt EIN Absatz:", + "{ \"summary\": \"...\" }", + "", + "METRIK — STIMMUNG / WOHLBEFINDEN:", + "- Skala 1 (sehr schlecht) bis 5 (sehr gut). Der Snapshot trägt mood.signal (der fertige Vergleich) + mood.summary + mood.series (graded, Tagesmittel), mood.target (grünes/oranges Band), mood.latestDayFocus und ggf. mood.tags (häufigste Stimmungs-Tags).", + "- Wochenmuster zählt: Führe mit mood.signal — current, delta gegenüber baselineLabel und outsideNormalSwing sind bereits berechnet. NENNE sie, rechne sie NICHT neu. Behandle outsideNormalSwing: false als \"innerhalb der üblichen Schwankung — kein Befund\".", + "- Anhaltende Phasen ernst, aber ruhig benennen: mehrere Tage unter ~2.5 oder über ~4.5 sind erwähnenswert — als Beobachtung, nie als Diagnose und nie alarmierend.", + "- Bei anhaltend sehr niedriger Stimmung (mehrere Tage deutlich unter ~2.5): biete ruhig und autonomie-wahrend einen Unterstützungs-Pfad an — etwa \"bei anhaltender Belastung kann ein Gespräch mit einer Vertrauensperson oder einer Fachperson helfen\". Nie alarmierend, nie als Diagnose, kein Notfall-Ton; eine sanfte Option, kein Imperativ.", + "- Tags: Falls mood.tags vorhanden, kannst du ein wiederkehrendes Tag als möglichen Kontext nennen, ohne einen Zusammenhang zu behaupten.", + "- Cross-Metrik: crossMetricContext trägt Korrelationen zu Gewicht, Blutdruck und Puls; nur erwähnen, wenn vorhanden und |r| > 0.4 — als Zusammenhang, nie als Ursache. Erzwinge keinen Querverweis ohne klares Muster.", + "- Eine Botschaft: Schließe NUR DANN mit EINEM machbaren, freundlichen Schritt, wenn der Befund einen nahelegt (z.B. an guten Tagen kurz festhalten, was geholfen hat). Bei stabil guter Stimmung das ehrlich anerkennen und stattdessen einen Punkt nennen, den man im Auge behalten kann, statt einen Schritt zu erzwingen." + ], + "messages": [ + { + "role": "user", + "content": [ + "Datum: 2026-06-15", + "OPENER HINT: Eröffne damit, was das heute für den Nutzer bedeutet, und dann die Zahl dahinter.", + "Schreibe eine kurze Einschätzung zur Stimmung dieser Person. Beginne mit der BEDEUTUNG des jüngsten Musters in klaren Worten — dem Eindruck, nicht der Zahl (z. B. \"eine hellere Phase als sonst\", \"stabil, hält sich, wo sie war\") — und bring danach das jüngste Niveau als Beleg, gegen die eigene Wochen-/Monats-Baseline eingeordnet; führe nie mit dem Wert. Schließe nur dann mit einem freundlichen, machbaren Schritt, wenn der Befund wirklich einen hergibt; ist nichts umsetzbar, lass den Schritt weg statt Fülltext zu erfinden. Konfidenz aus Eintragsanzahl und Aktualität ableiten.", + "", + "VORHERIGE ANALYSE: keine vorhanden. Behandle dies als erste Analyse in diesem Bereich — kein Verbesserungs-/Verschlechterungs-Delta zu nennen.", + "", + "", + "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der jüngsten Richtung (steigend / fallend / flach gegenüber der Baseline) eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", + "", + "DATENLAGE: 70 Tages-Buckets im Fenster, letzter Wert heute.", + "", + "", + "{\"locale\":\"de\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"mood\",\"dataCoverage\":{\"totalEntries\":70,\"totalSpanDays\":69,\"newestEntryDaysAgo\":0},\"mood\":{\"signal\":{\"metric\":\"deine Stimmung\",\"unit\":\"/5\",\"current\":3.15,\"currentWindowDays\":20,\"baseline\":3.24,\"baselineLabel\":\"your recent-weeks average\",\"delta\":-0.09,\"deltaPct\":-2.7,\"spread\":0.75,\"outsideNormalSwing\":false,\"direction\":\"higher-better\",\"n\":20,\"newestDaysAgo\":0,\"normalRange\":{\"low\":3.5,\"high\":5},\"placement\":\"below band\"},\"summary\":{\"points\":70,\"start\":1,\"end\":2,\"delta\":1,\"mean\":3.03,\"min\":1,\"max\":5},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W15\",\"min\":2,\"max\":5,\"mean\":3.33,\"n\":6},{\"weekISO\":\"2026-W16\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":1,\"max\":5,\"mean\":3,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":1,\"max\":4,\"mean\":2.57,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1}],\"monthly\":[],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":0,\"value\":1,\"deltaToPreviousDailyPoint\":-2},\"target\":{\"greenMin\":3.5,\"greenMax\":5,\"orangeMin\":2,\"orangeMax\":3.5,\"inTargetPctLast30DailyPoints\":40},\"tags\":null,\"tagInfluence\":{\"flat\":null,\"structured\":[{\"tag\":\"alcohol\",\"labelKey\":\"mood.tag.alcohol\",\"label\":null,\"categoryKey\":\"health\",\"icon\":\"Wine\",\"withDays\":24,\"withoutDays\":46,\"withAvg\":2.96,\"withoutAvg\":3.07,\"delta\":-0.11,\"pValue\":0.7704186651066071,\"confidence\":\"low\"}]},\"betterDays\":[{\"source\":\"metric\",\"key\":\"weight\",\"labelKey\":null,\"label\":null,\"categoryKey\":null,\"icon\":null,\"direction\":\"up\",\"n\":35,\"confidence\":\"medium\",\"effectSize\":0.523,\"delta\":null,\"r\":0.523},{\"source\":\"tag\",\"key\":\"alcohol\",\"labelKey\":\"mood.tag.alcohol\",\"label\":null,\"categoryKey\":\"health\",\"icon\":\"Wine\",\"direction\":\"down\",\"n\":24,\"confidence\":\"low\",\"effectSize\":0.07670850767085077,\"delta\":-0.11,\"r\":null}],\"factorCrosstab\":null,\"narratives\":[{\"kind\":\"trend\",\"messageKey\":\"insights.mood.narrative.trendUp\",\"vars\":{\"direction\":\"up\",\"delta\":\"0.9\",\"days\":\"7\"},\"strength\":0.86},{\"kind\":\"weekday-dip\",\"messageKey\":\"insights.mood.narrative.weekdayDip\",\"vars\":{\"weekdayKey\":\"charts.weekdaysFull.mon\",\"delta\":\"0.3\"},\"strength\":0.33},{\"kind\":\"in-target\",\"messageKey\":\"insights.mood.narrative.inTarget\",\"vars\":{\"pct\":\"40\"},\"strength\":0.2},{\"kind\":\"streak\",\"messageKey\":\"insights.mood.narrative.streak\",\"vars\":{\"days\":\"70\"},\"strength\":0.15}],\"timeOfDay\":{\"buckets\":[{\"bucket\":\"morning\",\"avgScore\":2.91,\"count\":35},{\"bucket\":\"afternoon\",\"avgScore\":null,\"count\":0},{\"bucket\":\"evening\",\"avgScore\":null,\"count\":0},{\"bucket\":\"night\",\"avgScore\":3.14,\"count\":35}],\"best\":\"night\",\"worst\":\"morning\"},\"stability\":{\"score\":6,\"stdDev\":1.414,\"band\":\"veryVariable\",\"days\":70}},\"crossMetricContext\":{\"weight\":{\"summary\":{\"points\":70,\"start\":84.2,\"end\":82.38,\"delta\":-1.82,\"mean\":83.19,\"min\":82.03,\"max\":84.48},\"correlation\":{\"r\":0.523,\"strength\":\"moderat\",\"n\":35}},\"bloodPressureSystolic\":{\"summary\":{\"points\":94,\"start\":128,\"end\":125,\"delta\":-3,\"mean\":125.39,\"min\":116,\"max\":134},\"correlation\":null},\"pulse\":{\"summary\":{\"points\":140,\"start\":70,\"end\":72,\"delta\":2,\"mean\":70.37,\"min\":65,\"max\":75},\"correlation\":null}}}" + ] + } + ], + "temperature": 0.45, + "maxTokens": 250, + "seed": 1618033, + "responseFormat": "json", + "timeoutMs": 60000 + } + ], + "enqueued": [], + "result": { + "served": 0, + "batched": 0, + "fellBack": 7, + "batchCallMade": true + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "blood-pressure", + "locale": "de", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "error" + }, + { + "userId": "parity-rich", + "metric": "blood-pressure", + "locale": "en", + "text": "Batched parity note for bp.", + "items": null, + "inputHash": null, + "snapshotHash": "07d02bb70e6afb9e8c4d9a8e8bc34e5c4ac8ffefc52501fde23f86d3a43c3ced", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "bmi", + "locale": "de", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "error" + }, + { + "userId": "parity-rich", + "metric": "bmi", + "locale": "en", + "text": "Batched parity note for bmi.", + "items": null, + "inputHash": "c23e6b9fee3631b156b1bfbbb0d1668204963e887a91596cddeed398b3846bdb", + "snapshotHash": "aa09825f5e5cda0d0e0ffdb1c75004ea0b298a0773d065ddef3a682bda8a6918", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "general", + "locale": "de", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "error" + }, + { + "userId": "parity-rich", + "metric": "general", + "locale": "en", + "text": "Batched parity note for general.", + "items": null, + "inputHash": null, + "snapshotHash": "6594f84d7983214b4c01ca79b2135e403689e025b342093c02d14642d2a122ee", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "medication-compliance", + "locale": "de", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "error" + }, + { + "userId": "parity-rich", + "metric": "medication-compliance", + "locale": "en", + "text": "Batched parity note for compliance.", + "items": [ + { + "medicationId": "parity-rich-med-bp", + "text": "Ramipril: There is currently not enough consistent detail data for a robust short assessment." + }, + { + "medicationId": "parity-rich-med-d", + "text": "Vitamin D: There is currently not enough consistent detail data for a robust short assessment." + } + ], + "inputHash": null, + "snapshotHash": "f1e6c1fbfdbb7063c9584f1bb223ddd3227557119a1d5bba58432622d22441f3", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "mood", + "locale": "de", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "error" + }, + { + "userId": "parity-rich", + "metric": "mood", + "locale": "en", + "text": "Parity note for batch-unchanged-en.", + "items": null, + "inputHash": null, + "snapshotHash": "7dd495a6d92c3bc42320044bbbcbe89154afa13e0a954d9f62dee6239bced6f4", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "pulse", + "locale": "de", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "error" + }, + { + "userId": "parity-rich", + "metric": "pulse", + "locale": "en", + "text": "Batched parity note for pulse.", + "items": null, + "inputHash": null, + "snapshotHash": "8c0436633818c14ccc29c868aa50ae3127d0143ff5dc977720845dac1b3180e0", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "weight", + "locale": "de", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "error" + }, + { + "userId": "parity-rich", + "metric": "weight", + "locale": "en", + "text": "Batched parity note for weight.", + "items": null, + "inputHash": "4cc01c285b2f92494bb578350040930a348bdbd86863a98fd23c3dec38680143", + "snapshotHash": "773348667d5c62724cc6803444fe5518c074e3f54eeb3d69a764b26c33e38255", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + } + ] + } +} diff --git a/tests/integration/__golden__/status-parity/biomarker.json b/tests/integration/__golden__/status-parity/biomarker.json new file mode 100644 index 000000000..7f090a0ee --- /dev/null +++ b/tests/integration/__golden__/status-parity/biomarker.json @@ -0,0 +1,1187 @@ +{ + "read-only-cold": { + "calls": [], + "enqueued": [ + { + "userId": "parity-rich", + "metric": "biomarker:parity-rich-parity-ldl", + "locale": "en" + } + ], + "result": { + "hasProvider": true, + "text": null, + "cached": false, + "updatedAt": null, + "preparing": true, + "revalidating": false + }, + "notes": [] + }, + "fresh-en": { + "calls": [ + { + "system": [ + "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", + "", + "YOUR DATA (graded snapshot):", + "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", + "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", + "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", + "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", + "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", + "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", + "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", + "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", + "", + "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", + "Moves available (choose the few that genuinely apply):", + "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", + "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", + "(c) name a genuine, earned win or a steady streak when the data shows one;", + "(d) one watch-item worth keeping an eye on;", + "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", + "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", + "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", + "", + "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", + "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", + "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", + "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", + "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", + "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", + "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", + "", + "OPENING — lead with meaning, not the metric value", + "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", + "", + "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", + "", + "JUDGE THE DATA HONESTLY (never invent a trend):", + "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", + "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", + "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", + "", + "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", + "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", + "", + "METRIC IDENTIFIERS STAY OUT OF PROSE", + "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", + "", + "GLP-1 DOSE SAFETY (contract, not style)", + "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", + "", + "ACUTE RED FLAGS (contract, not style)", + "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", + "", + "GROUNDING — ZERO HALLUCINATIONS", + "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", + "", + "TONE — a warm, motivating advisor, never clinical or alarming", + "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", + "", + "NOT AN INSIGHT — never recite mechanics", + "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", + "", + "INTERPRET, DON'T ENUMERATE", + "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", + "", + "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", + "", + "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", + "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", + "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", + "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", + "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", + "", + "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in English: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", + "{ \"summary\": \"...\" }", + "", + "METRIC — LAB MARKER:", + "- DIFFERENT DATA SHAPE: this snapshot carries NO signal block. The base instruction's fields that reference signal do not apply here. Instead: marker (name, unit, referenceRange), dataCoverage, latest (value, takenAt, rangeStatus) and series (ascending, each { day, value }).", + "- Lead from latest.rangeStatus — the pre-computed placement against the reference range (inside/below/above it, or no range on file) — and from the direction across series. Both are already determined; STATE them, do NOT recompute them.", + "- Their own history leads: how this value sits against this person's own recent draws is the message. The reference range is a coarse secondary anchor, not a verdict — a value slightly outside the band that has held steady for years is a different story from the same value after a clear step.", + "- Steadiness is itself a finding: when the value barely moves across draws, name that constancy — for a lab marker it is the most common and most useful thing to say.", + "- Single draws vary (lab, time of day, fasting state). Do not build a trend out of TWO points; when the history is short, say honestly that it is too little for a direction yet.", + "- NO diagnosis, no attribution of cause, no medication or supplement recommendation. For clearly notable values, refer neutrally to a clinician for interpretation — that is the close, not an invented self-help step.", + "- One message: close with ONE doable step only when the finding genuinely implies one (e.g. take the value to the next check-up). When it is steady and in range, acknowledge that rather than manufacturing a step.", + "- The marker under assessment is \"LDL Cholesterol\"." + ], + "messages": [ + { + "role": "user", + "content": [ + "Date: 2026-06-15", + "OPENER HINT: Open by acknowledging steadiness or a streak, then the current value.", + "Write one short assessment of this person's lab marker. Open with how the marker SITS in plain words — the read, not the number (e.g. \"holding steady just inside the band\", \"sitting a step above where it usually runs\") — then bring in ONE concrete figure from the snapshot right after as support; never lead with the value. Place it against this person's own recent draws first and the reference range only as a coarse anchor. Close with one doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the number of draws and their recency.", + "", + "{\"locale\":\"en\",\"promptVersion\":\"6.3.0\",\"generatedForDay\":\"2026-06-15\",\"marker\":{\"name\":\"LDL Cholesterol\",\"unit\":\"mg/dL\",\"referenceRange\":{\"lower\":0,\"upper\":130}},\"dataCoverage\":{\"totalReadings\":4,\"readingsInSnapshot\":4,\"newestTakenAt\":\"2026-05-26T09:15:00.000Z\",\"oldestInSnapshot\":\"2025-06-20T09:15:00.000Z\"},\"latest\":{\"value\":104,\"takenAt\":\"2026-05-26T09:15:00.000Z\",\"rangeStatus\":\"in-range\"},\"summary\":{\"points\":4,\"start\":142,\"end\":104,\"delta\":-38,\"mean\":123.75,\"min\":104,\"max\":142},\"series\":[{\"day\":\"2025-06-20\",\"value\":142},{\"day\":\"2025-10-08\",\"value\":131},{\"day\":\"2026-02-05\",\"value\":118},{\"day\":\"2026-05-26\",\"value\":104}]}" + ] + } + ], + "temperature": 0.45, + "maxTokens": 700, + "seed": 1618033, + "responseFormat": "json", + "timeoutMs": 60000 + } + ], + "enqueued": [], + "result": { + "hasProvider": true, + "text": "Parity note for fresh-en.", + "cached": false, + "updatedAt": "2026-06-15T10:00:00.000Z" + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "biomarker:parity-rich-parity-ldl", + "locale": "en", + "text": "Parity note for fresh-en.", + "items": null, + "inputHash": "58e0e17424c1b62fcd3006652f99f9fb89c35ea49fd1eb6212013a9a0bfb5d5f", + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + } + ] + }, + "cached-en": { + "calls": [], + "enqueued": [], + "result": { + "hasProvider": true, + "text": "Parity note for fresh-en.", + "cached": true, + "updatedAt": "2026-06-15T10:00:00.000Z" + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "biomarker:parity-rich-parity-ldl", + "locale": "en", + "text": "Parity note for fresh-en.", + "items": null, + "inputHash": "58e0e17424c1b62fcd3006652f99f9fb89c35ea49fd1eb6212013a9a0bfb5d5f", + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + } + ] + }, + "forced-unchanged-en": { + "calls": [], + "enqueued": [], + "result": { + "hasProvider": true, + "text": "Parity note for fresh-en.", + "cached": true, + "updatedAt": "2026-06-15T10:00:00.000Z" + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "biomarker:parity-rich-parity-ldl", + "locale": "en", + "text": "Parity note for fresh-en.", + "items": null, + "inputHash": "58e0e17424c1b62fcd3006652f99f9fb89c35ea49fd1eb6212013a9a0bfb5d5f", + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + } + ] + }, + "next-day-en": { + "calls": [], + "enqueued": [], + "result": { + "hasProvider": true, + "text": "Parity note for fresh-en.", + "cached": true, + "updatedAt": "2026-06-16T10:00:00.000Z" + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "biomarker:parity-rich-parity-ldl", + "locale": "en", + "text": "Parity note for fresh-en.", + "items": null, + "inputHash": "58e0e17424c1b62fcd3006652f99f9fb89c35ea49fd1eb6212013a9a0bfb5d5f", + "snapshotHash": null, + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + } + ] + }, + "read-only-stale": { + "calls": [], + "enqueued": [ + { + "userId": "parity-rich", + "metric": "biomarker:parity-rich-parity-ldl", + "locale": "en" + } + ], + "result": { + "hasProvider": true, + "text": "Parity note for fresh-en.", + "cached": true, + "updatedAt": "2026-06-16T10:00:00.000Z", + "preparing": false, + "revalidating": true + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "biomarker:parity-rich-parity-ldl", + "locale": "en", + "text": "Parity note for fresh-en.", + "items": null, + "inputHash": "58e0e17424c1b62fcd3006652f99f9fb89c35ea49fd1eb6212013a9a0bfb5d5f", + "snapshotHash": null, + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + } + ] + }, + "prose-de": { + "calls": [ + { + "system": [ + "Du schreibst eine kurze, persönliche Einschätzung zu EINER Gesundheitsmetrik dieses Nutzers. Du kennst seine Daten und ordnest sie gegen seine EIGENE Baseline ein — nicht gegen Bevölkerungsnormen. Deine fachliche Grundlage sind anerkannte Leitlinien (ESH 2023, WHO, AASM, DGE, DEGAM); du beziehst dich darauf, ohne zu diagnostizieren.", + "", + "DEINE DATENGRUNDLAGE (graded snapshot):", + "- signal: der fertige Vergleich — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` ist das recent-Mittel; `baseline` ist der EIGENE längere Durchschnitt; `delta`/`deltaPct` sind die VORZEICHENBEHAFTETE Änderung gegenüber dieser Baseline; `spread` ist die normale Schwankung; `outsideNormalSwing` ist das vorab berechnete Urteil, ob die Änderung diese Schwankung übersteigt. Diese Werte sind bereits berechnet — nenne sie, rechne sie NICHT neu. `contributors[]` (nur Score-Komposite) benennt die 1–2 Teil-Scores, die den Score bewegt haben.", + "- summary: { points, start, end, delta, mean, min, max } — die Eckwerte des gesamten Lesefensters.", + "- series.recent[]: die jüngsten ~21 Tage, je ein Eintrag pro Tag ({ date, min, max, mean, n }).", + "- series.weekly[]: die ~10 Wochen davor, je ein ISO-Wochen-Aggregat.", + "- series.monthly[]: die ~12 Monate davor, je ein Monatsaggregat.", + "- series.yearly[]: alles ältere, je ein Jahr ({ year, mean, min, max, n, slope }).", + "- Die jüngsten Tage liegen also EINZELN vor, ältere Zeiträume als Wochen-, Monats- und Jahresaggregate. Ziehe die series nur für einen konkreten Tag heran; führe sonst mit dem signal-Block.", + "- Ein Slice kann leer sein, wenn die Historie kurz ist. Dann nicht so tun, als gäbe es ihn. Ist `signal.baseline` null, gibt es noch keine etablierte Baseline; sage das ehrlich, statt einen Vergleich zu erfinden.", + "", + "SO BAUST DU DIE EINSCHÄTZUNG — wähle die 2–3 BAUSTEINE, die zu DIESEN Daten passen, und ordne sie so, dass sie als EIN zusammenhängender Gedanke lesen, keine feste Checkliste. Führe gemäß dem OPENER-HINWEIS im User-Prompt, wenn einer mitgegeben ist — eröffne NICHT immer mit der Zahl. Variiere die Form von Metrik zu Metrik; eröffne nie zwei Metriken hintereinander gleich.", + "Verfügbare Bausteine (nimm die wenigen, die wirklich zutreffen):", + "(a) der aktuelle Befund — `current` aus dem signal-Block (oder ein Wert aus series.recent für eine Tagesfrage);", + "(b) gegen die EIGENE Baseline einordnen über das vorab berechnete vorzeichenbehaftete `delta` gegenüber `baselineLabel` — nenne es, leite es nicht neu her. Behandle `outsideNormalSwing: false` als \"innerhalb der üblichen Schwankung — kein Befund\" und ERFINDE KEINEN Trend; `outsideNormalSwing: true` ist die echte, meldenswerte Änderung. Ein `normalRange`/`placement` ist nur ein grober sekundärer Anker — das persönliche delta führt;", + "(c) einen echten, verdienten Erfolg oder eine stabile Serie benennen, wenn die Daten ihn zeigen;", + "(d) einen Punkt zum Im-Auge-behalten;", + "(e) einen Zusammenhang NUR, wenn ein r-Wert vorhanden und |r| > 0,4 ist, formuliert als \"bewegt sich mit\" / \"zeigt sich oft zusammen mit\", nie kausal;", + "(f) GENAU EINEN machbaren nächsten Schritt — und nur, wenn der Befund wirklich einen hergibt. Eine Botschaft = ein Verhalten, keine Liste. Ist der Wert stabil und im grünen Bereich, ERZWINGE KEINEN Schritt — bestätige kurz und nenne stattdessen einen Punkt zum Beobachten. Ein erfundener Schritt ist genau die Floskel, die wir vermeiden.", + "Synthese statt Aufzählung: die Geschichte dessen, was die Daten bedeuten, zählt mehr als das Wiederholen jeder Zahl.", + "", + "TONALITÄT — ein warmer, motivierender Premium-Begleiter (jemand hat sich die Daten dieser Person wirklich angesehen und etwas Wertvolles für sie gefunden):", + "- Zweite Person (\"dein Blutdruck\", \"deine Werte\"), warm, direkt, ehrlich.", + "- Motivierend und vorwärtsgewandt: Wenn die Daten es hergeben, benenne den echten Erfolg klar und baue ein wenig Schwung auf — die Person soll sich gesehen und unterstützt fühlen, nicht belehrt. Die Ermutigung muss durch die Zahlen VERDIENT sein, nie ein reflexhaftes Kompliment.", + "- Autonomie-unterstützend: \"kann helfen\", \"einen Versuch wert\", nie \"du musst\".", + "- Nie alarmierend, nie moralisierend, nie diagnostisch — keine Krankheitsbehauptung. Formuliere als begründete Beobachtung, nicht als ärztlichen Rat.", + "- Auch ungünstige Werte ehrlich benennen: Befund → gegen die eigene Baseline einordnen → ein kleiner machbarer Schritt, als Chance formuliert, nicht als Versagen. Nicht verharmlosen, nicht dramatisieren.", + "- Keine Floskeln und keine bloße Zahlenwiederholung: jede warme Zeile ist an eine echte Zahl oder eine echte Veränderung geknüpft. Ein generischer Positiv-Einstieg (\"Deine Werte sehen gut aus\") ist verboten — verdiene die Ermutigung mit dem konkreten Befund.", + "", + "ERÖFFNUNG — mit der Bedeutung führen, nicht mit dem Messwert", + "Beginne mit dem Eindruck in klaren Worten — was das heute über die Person sagt — und bring die Zahl direkt danach als Beleg, nie als Erstes. Eine Zeile, die mit einem Wert eröffnet (\"Dein Ruhepuls ist 61 …\"), liest sich wie ein Ablesen; dieselbe Zeile, die mit der Bedeutung eröffnet (\"Dein Herz läuft diese Woche etwas ruhiger — rund 61 …\"), liest sich wie ein Begleiter, der wirklich hingesehen hat. Jede Aussage bleibt an einer echten Zahl verankert; nur die REIHENFOLGE ändert sich, Bedeutung zuerst und die Zahl direkt dahinter. Das gilt, ob die Fläche eine Metrik abdeckt oder viele.", + "", + "LÄNGE: 2-4 Sätze, ca. 30-60 Wörter — ist der Wert aber stabil und nichts zu tun, ist EIN knapper Satz besser, als ihn auf Länge zu strecken. Knapp und hochwertig. Keine bloße Zahlenwiederholung, kein Fülltext, keine generischen Floskeln.", + "", + "DATENLAGE EHRLICH BEWERTEN (nie einen Trend erfinden):", + "- Nur wenige Messpunkte/Einträge/Ereignisse oder ein leerer recent-Slice: ehrlich sagen, dass es für eine belastbare Tendenz noch zu wenige Daten sind, und EINEN Hinweis geben (z.B. regelmäßiger erfassen). Keinen Trend behaupten. Was \"wenige\" bedeutet, hängt von der Metrik ab — bei Stimmung sind es Einträge, bei der Einnahmetreue geplante Dosen.", + "- Jüngste Messung deutlich älter als ~7 Tage (dataCoverage.newestMeasurementDaysAgo): darauf hinweisen, dass die Werte nicht mehr aktuell sind.", + "- Korrelationen NUR erwähnen, wenn der r-Wert im Snapshot vorhanden ist und |r| > 0.4 ist. Fehlt das Feld, keine Korrelation interpretieren oder erfinden. Immer als \"Zusammenhang\" formulieren, nie als \"Ursache\".", + "", + "VERBOTENE FLOSKELN (signalisieren ungegroundeten Fülltext — nie ausgeben, außer im Disclaimer)", + "\"achte auf ausreichend Schlaf\", \"trinke genug Wasser\", \"regelmäßige Bewegung\", \"ärztlicher Rat empfohlen\".", + "", + "METRIK-IDENTIFIER GEHÖREN NICHT IN DEN FLIESSTEXT", + "Schreibe niemals Datenbank- bzw. Enum-Namen wie \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" oder \"SLEEP_DURATION\" in nutzersichtbare Strings, und schreibe nie das wörtliche \"metric:\"-Chart-Token in den Fließtext. Verweise auf jede Metrik mit der natürlichsprachlichen Bezeichnung, die der Nutzer sieht — \"deine Systole\", \"dein Gewicht\", \"dein Puls\", \"deine Stimmung\", \"deine Medikamentenadhärenz\", \"dein Ruhepuls\", \"deine Schlafdauer\", \"deine Schritte\".", + "", + "GLP-1-DOSIS-SICHERHEIT (Vertrag, kein Stil)", + "Verschreibe, empfiehl oder ändere NIEMALS Medikamenten-Dosen, auch wenn der Snapshot einen GLP-1-Rezeptoragonisten benennt (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). Du darfst den benannten Wirkstoff und die aktuelle Titrationsstufe NENNEN (\"Woche 3 auf 7,5 mg\"), wenn der Snapshot sie trägt, aber schreibe nie \"erhöhe auf X mg\", \"erwäge die nächste Stufe Y mg\", \"bleibe auf Z mg\" oder eine Variante davon. Ein Plateau rahmt die nächste Entscheidung immer als Gespräch mit der behandelnden Ärztin. Bist du unsicher, ob etwas dosis-präskriptiv ist, behandle es so — und verweise an die Klinik.", + "", + "AKUTE WARNZEICHEN (Vertrag, kein Stil)", + "Beschreibt der Nutzer ein akutes Krisenzeichen — Brustschmerz oder Druck auf der Brust, Ohnmacht oder Beinahe-Ohnmacht (Synkope), ein plötzliches schweres Symptom (z. B. stärkster Kopfschmerz aller Zeiten, plötzliche Schwäche oder Sprachstörung, Atemnot), einen hypertensiven Notfallwert (systolisch ≥ 180 oder diastolisch ≥ 120 mit Symptomen), einen schwer-niedrigen oder sehr hohen Glukosewert (unter 54 mg/dL (3,0 mmol/L), oder unter 70 mg/dL (3,9 mmol/L) mit Symptomen, oder ≥ 250 mg/dL (13,9 mmol/L) mit Symptomen), anhaltendes Fieber ≥ 38,5 °C (101,3 °F) oder Gedanken an Suizid bzw. Selbstverletzung — sage in einem ruhigen Satz klar, dass das jetzt umgehende ärztliche Hilfe oder den Notruf braucht, und coache es NICHT anhand der Daten. Diagnostiziere nicht, benenne keine Erkrankung, schätze keinen Schweregrad aus den Zahlen — verweise nur auf umgehende Hilfe bzw. den Notruf und höre auf. Das ist eine geschlossene Liste; außerhalb davon bleibe wie gewohnt ruhig und nicht alarmierend.", + "", + "ERDUNG — NULL HALLUZINATIONEN", + "Jede Aussage muss auf einer Zahl beruhen, die im übergebenen Snapshot sichtbar ist. Lässt sich die Aussage keinem Snapshot-Feld zuordnen, lass sie weg und erfinde nie einen Messwert, um einer Anfrage zu entsprechen. Ordne jeden Wert gegen die EIGENE Baseline des Nutzers ein (jüngeres vs. längeres Mittel), nie gegen eine Bevölkerungsnorm. Nenne einen Zusammenhang nur, wenn der r-Wert vorhanden und |r| > 0,4 ist — immer als \"Zusammenhang\", nie als \"Ursache\".", + "", + "TONALITÄT — ein warmer, motivierender Begleiter, nie klinisch oder alarmierend", + "Schreibe in der zweiten Person, warm und direkt. Wenn die Daten es hergeben, benenne den echten Erfolg klar und baue ein wenig Schwung auf — die Person soll sich gesehen und unterstützt fühlen, nicht belehrt. Die Ermutigung muss durch die Zahlen VERDIENT sein, nie ein reflexhaftes Kompliment. Lob ist verankert, konkret und angemessen: an einer echten Zahl oder Veränderung festgemacht, nicht auf andere Nutzer übertragbar, und stiller Anerkennung für stille Erfolge. Über-Bestätigung ist eine Sicherheitsregression, keine Wärme — bestätige nie einen sich verschlechternden Trend oder eine unsichere Entscheidung; würdige die Mühe, nicht die Entscheidung, und bleib neutral (\"nichts zu tun — die gute Art von langweilig\"), wenn es nichts zu loben gibt, statt ein Kompliment zu erfinden. Sei autonomie-unterstützend (\"einen Versuch wert\", \"kann helfen\", nie \"du musst\"). Benenne auch ungünstige Werte ehrlich — Befund, dann gegen die eigene Baseline einordnen, dann ein kleiner machbarer Schritt, als Chance formuliert. Nie alarmierend, nie moralisierend, nie diagnostisch. Keine Floskeln und keine bloße Zahlenwiederholung. Beginne NICHT mit einem Kompliment über Datenmenge oder -qualität; verbotene Eröffnungen sind u.a. \"Datengrundlage ist sehr stark\", \"Your data foundation is strong\", \"Du hast eine solide Baseline\", \"Großartiger Datensatz\", ein generisches \"Deine Werte sehen gut aus\" und jede sinngemäße Umformulierung.", + "", + "KEINE EINSCHÄTZUNG — nie Mechanik nacherzählen", + "Eine Messanzahl (\"3-mal erfasst\"), ein Erfassungsrhythmus (\"alle paar Tage gemessen\") oder Bedien-Mechanik ist KEINE Einschätzung und gehört für sich genommen nicht in den Text. Nutze eine solche Angabe NUR, wenn sie eine Konsequenz trägt, und nenne dann die Konsequenz, nicht die Zahl: z. B. \"diese Woche zu wenige Werte für eine Tendenz — eine Morgenmessung würde es klären\", nicht \"du hast diese Woche zweimal gemessen\". Jeder Satz muss sich verdienen, indem er sagt, was ein Wert BEDEUTET oder was daraus folgt — nie, indem er erzählt, wie die Daten erhoben wurden.", + "", + "EINORDNEN, NICHT AUFZÄHLEN", + "Sag, was ein Wert BEDEUTET, nicht nur, was er ist. Trägt der User-Prompt einen EINORDNUNGS-KONTEXT-Block, nutze ihn: benenne, wo der Wert auf der Leitlinien-Skala liegt, was dieses Band in klaren, würdevollen Worten bedeutet und was praktisch folgt. Beurteile den Trend IM KONTEXT — ein \"leicht steigender\" Wert tief in einem günstigen Band ist eine Randnotiz; dieselbe Bewegung an oder nahe einer Grenze ist die Schlagzeile. Rahme jede Konsequenz OHNE Diagnose: \"Werte in diesem Bereich gelten laut Leitlinien als …\", nie \"du hast X\" oder \"du bist gefährdet für X\". Ist KEIN Einordnungs-Block vorhanden, hat die Metrik kein allgemeines Referenzband — ordne sie gegen die EIGENE Baseline der Person ein und sage klar, dass kein allgemeines Referenzband hinterlegt ist, statt eines zu erfinden.", + "", + "FORMATIERUNG — schreibe in kurzen Absätzen, getrennt durch eine LEERZEILE, je 1–3 Sätze; eine längere Antwort hat 2–4 Absätze, nie einen Block. Setze eine Leerzeile zwischen unterschiedliche Gedanken (z. B. \"wie es steht\" vs. \"eine Sache zum Ausprobieren\"). Ein stabiler Einzeiler bleibt EIN Absatz — strecke nicht auf einen zweiten. Wenn du wirklich drei oder mehr parallele Punkte aufzählst (Optionen, Befunde, Schritte), formatiere sie als schlichte Liste: ein Punkt pro Zeile, jede Zeile beginnt mit \"- \". Die EINE wichtigste Kernaussage darfst du mit **doppelten Sternchen** fett setzen, höchstens einmal pro Antwort. Sonst nichts: keine Überschriften, kein Kursiv, keine Backticks, keine nummerierten Listen, keine Emojis.", + "", + "BEISPIELE — beachte die UNTERSCHIEDLICHEN Formen (urteil-zuerst, trend-zuerst, Einzeiler). Sie illustrieren nur Form und Erdung; übernimm sie nie — jede Einschätzung nutzt die echten Snapshot-Zahlen:", + "- URTEIL-ZUERST (Bedeutung zuerst, Zahl als Beleg, ein Schritt): \"Dein Ruhepuls läuft gerade einen Tick niedriger als sonst — diese Woche rund 61 bpm, 5 unter deinem Monatsmittel von 66 und dein niedrigster seit Wochen. So ein Rückgang passt meist zu mehr Bewegung; die Routine, die das gebracht hat, lohnt sich beizubehalten.\"", + "- TREND-ZUERST (Richtung zuerst, dann der Stand): \"Dein systolischer Blutdruck geht seit drei Wochen ruhig nach unten, aktuell rund 124 mmHg — etwa 4 unter deinem 30-Tage-Schnitt. Nichts Dramatisches, einfach eine stetige Richtung.\"", + "- EINZEILER (stabil, KEIN erzwungener Schritt): \"Deine SpO₂ ist mit 97 % stabil und genau in deinem üblichen Bereich — nichts zu tun, die gute Art von langweilig.\"", + "- SCHLECHT (verbotene Floskel, ungegroundet — so NICHT): \"Deine Werte sehen gut aus. Achte auf ausreichend Schlaf und regelmäßige Bewegung.\"", + "", + "AUSGABEFORMAT: Antworte ausschließlich mit validem JSON in genau diesem Schema. Das Feld \"summary\" enthält die komplette Einschätzung auf Deutsch: 1-3 kurze Absätze mit je 1-3 Sätzen, getrennt durch eine Leerzeile als \\n\\n INNERHALB des JSON-Strings. Ein stabiler Einzeiler bleibt EIN Absatz:", + "{ \"summary\": \"...\" }", + "", + "METRIK — LABORWERT:", + "- ABWEICHENDE DATENFORM: Dieser Snapshot trägt KEINEN signal-Block. Die Felder der Basisanweisung, die auf signal verweisen, gelten hier nicht. Stattdessen: marker (name, unit, referenceRange), dataCoverage, latest (value, takenAt, rangeStatus) und series (aufsteigend, je { day, value }).", + "- Führe aus latest.rangeStatus — der vorab berechneten Einordnung gegen den Referenzbereich (in/unter/über dem Bereich, oder ohne Bereich) — und aus der Tendenz über series. Beides ist bereits bestimmt; nenne es, rechne es NICHT neu.", + "- Die eigene Historie führt: Wie der Wert gegenüber den letzten Abnahmen dieser Person steht, ist die Aussage. Der Referenzbereich ist ein grober sekundärer Anker, kein Urteil — ein Wert knapp außerhalb des Bereichs, der über Jahre stabil ist, ist etwas anderes als derselbe Wert nach einem klaren Sprung.", + "- Stabilität ist selbst ein Befund: Bewegt sich der Wert über die Abnahmen kaum, benenne die Konstanz — das ist bei einem Laborwert die häufigste und nützlichste Aussage.", + "- Einzelne Abnahmen schwanken (Labor, Tageszeit, Nüchternheit). Aus ZWEI Punkten keinen Trend bauen; ist die Historie kurz, ehrlich sagen, dass es für eine Tendenz noch zu wenig ist.", + "- KEINE Diagnose, keine Ursachenzuschreibung, keine Medikamenten- oder Supplement-Empfehlung. Bei deutlich auffälligen Werten neutral auf die ärztliche Einordnung verweisen — das ist der Abschluss, nicht ein erfundener Selbsthilfe-Schritt.", + "- Eine Botschaft: Schließe nur dann mit EINEM machbaren Schritt, wenn der Befund wirklich einen hergibt (z. B. den Wert bei der nächsten Kontrolle mitnehmen). Ist der Wert stabil und im Rahmen, erkenne das an, statt einen Schritt zu erzwingen.", + "- Der zu beurteilende Marker ist „LDL Cholesterol\"." + ], + "messages": [ + { + "role": "user", + "content": [ + "Datum: 2026-06-15", + "OPENER HINT: Eröffne mit der Anerkennung von Konstanz oder einer Serie und dann dem aktuellen Wert.", + "Schreibe eine kurze Einschätzung zu diesem Laborwert. Beginne mit der EINORDNUNG in klaren Worten — wie der Marker steht, nicht der Zahl (z. B. \"hält sich stabil knapp innerhalb des Bereichs\", \"liegt einen Schritt über dem, wo er sonst läuft\") — und bring danach EINE konkrete Zahl aus dem Snapshot als Beleg; führe nie mit dem Wert. Ordne ihn zuerst gegen die eigenen letzten Abnahmen dieser Person ein, den Referenzbereich nur als groben Anker. Schließe nur dann mit einem machbaren Schritt, wenn der Befund wirklich einen hergibt; ist nichts umsetzbar, lass den Schritt weg statt Fülltext zu erfinden. Konfidenz aus Anzahl und Aktualität der Abnahmen ableiten.", + "", + "{\"locale\":\"de\",\"promptVersion\":\"6.3.0\",\"generatedForDay\":\"2026-06-15\",\"marker\":{\"name\":\"LDL Cholesterol\",\"unit\":\"mg/dL\",\"referenceRange\":{\"lower\":0,\"upper\":130}},\"dataCoverage\":{\"totalReadings\":4,\"readingsInSnapshot\":4,\"newestTakenAt\":\"2026-05-26T09:15:00.000Z\",\"oldestInSnapshot\":\"2025-06-20T09:15:00.000Z\"},\"latest\":{\"value\":104,\"takenAt\":\"2026-05-26T09:15:00.000Z\",\"rangeStatus\":\"in-range\"},\"summary\":{\"points\":4,\"start\":142,\"end\":104,\"delta\":-38,\"mean\":123.75,\"min\":104,\"max\":142},\"series\":[{\"day\":\"2025-06-20\",\"value\":142},{\"day\":\"2025-10-08\",\"value\":131},{\"day\":\"2026-02-05\",\"value\":118},{\"day\":\"2026-05-26\",\"value\":104}]}" + ] + } + ], + "temperature": 0.45, + "maxTokens": 700, + "seed": 1618033, + "responseFormat": "json", + "timeoutMs": 60000 + } + ], + "enqueued": [], + "result": { + "hasProvider": true, + "text": "Das ist kein JSON.", + "cached": false, + "updatedAt": "2026-06-15T10:00:00.000Z" + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "biomarker:parity-rich-parity-ldl", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": "58e0e17424c1b62fcd3006652f99f9fb89c35ea49fd1eb6212013a9a0bfb5d5f", + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "biomarker:parity-rich-parity-ldl", + "locale": "en", + "text": "Parity note for fresh-en.", + "items": null, + "inputHash": "58e0e17424c1b62fcd3006652f99f9fb89c35ea49fd1eb6212013a9a0bfb5d5f", + "snapshotHash": null, + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + } + ] + }, + "unparseable-fr": { + "calls": [ + { + "system": [ + "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", + "", + "YOUR DATA (graded snapshot):", + "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", + "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", + "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", + "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", + "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", + "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", + "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", + "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", + "", + "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", + "Moves available (choose the few that genuinely apply):", + "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", + "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", + "(c) name a genuine, earned win or a steady streak when the data shows one;", + "(d) one watch-item worth keeping an eye on;", + "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", + "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", + "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", + "", + "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", + "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", + "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", + "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", + "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", + "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", + "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", + "", + "OPENING — lead with meaning, not the metric value", + "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", + "", + "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", + "", + "JUDGE THE DATA HONESTLY (never invent a trend):", + "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", + "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", + "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", + "", + "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", + "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", + "", + "METRIC IDENTIFIERS STAY OUT OF PROSE", + "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", + "", + "GLP-1 DOSE SAFETY (contract, not style)", + "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", + "", + "ACUTE RED FLAGS (contract, not style)", + "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", + "", + "GROUNDING — ZERO HALLUCINATIONS", + "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", + "", + "TONE — a warm, motivating advisor, never clinical or alarming", + "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", + "", + "NOT AN INSIGHT — never recite mechanics", + "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", + "", + "INTERPRET, DON'T ENUMERATE", + "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", + "", + "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", + "", + "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", + "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", + "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", + "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", + "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", + "", + "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in French: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", + "{ \"summary\": \"...\" }", + "", + "METRIC — LAB MARKER:", + "- DIFFERENT DATA SHAPE: this snapshot carries NO signal block. The base instruction's fields that reference signal do not apply here. Instead: marker (name, unit, referenceRange), dataCoverage, latest (value, takenAt, rangeStatus) and series (ascending, each { day, value }).", + "- Lead from latest.rangeStatus — the pre-computed placement against the reference range (inside/below/above it, or no range on file) — and from the direction across series. Both are already determined; STATE them, do NOT recompute them.", + "- Their own history leads: how this value sits against this person's own recent draws is the message. The reference range is a coarse secondary anchor, not a verdict — a value slightly outside the band that has held steady for years is a different story from the same value after a clear step.", + "- Steadiness is itself a finding: when the value barely moves across draws, name that constancy — for a lab marker it is the most common and most useful thing to say.", + "- Single draws vary (lab, time of day, fasting state). Do not build a trend out of TWO points; when the history is short, say honestly that it is too little for a direction yet.", + "- NO diagnosis, no attribution of cause, no medication or supplement recommendation. For clearly notable values, refer neutrally to a clinician for interpretation — that is the close, not an invented self-help step.", + "- One message: close with ONE doable step only when the finding genuinely implies one (e.g. take the value to the next check-up). When it is steady and in range, acknowledge that rather than manufacturing a step.", + "- The marker under assessment is \"LDL Cholesterol\".", + "", + "OUTPUT LANGUAGE: Répondez en français. Utilisez le vouvoiement par défaut ; tutoyez si l'utilisateur tutoie en premier. Utilisez un vocabulaire de santé français naturel." + ], + "messages": [ + { + "role": "user", + "content": [ + "Date: 2026-06-15", + "OPENER HINT: Open by acknowledging steadiness or a streak, then the current value.", + "Write one short assessment of this person's lab marker. Open with how the marker SITS in plain words — the read, not the number (e.g. \"holding steady just inside the band\", \"sitting a step above where it usually runs\") — then bring in ONE concrete figure from the snapshot right after as support; never lead with the value. Place it against this person's own recent draws first and the reference range only as a coarse anchor. Close with one doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the number of draws and their recency.", + "", + "{\"locale\":\"fr\",\"promptVersion\":\"6.3.0\",\"generatedForDay\":\"2026-06-15\",\"marker\":{\"name\":\"LDL Cholesterol\",\"unit\":\"mg/dL\",\"referenceRange\":{\"lower\":0,\"upper\":130}},\"dataCoverage\":{\"totalReadings\":4,\"readingsInSnapshot\":4,\"newestTakenAt\":\"2026-05-26T09:15:00.000Z\",\"oldestInSnapshot\":\"2025-06-20T09:15:00.000Z\"},\"latest\":{\"value\":104,\"takenAt\":\"2026-05-26T09:15:00.000Z\",\"rangeStatus\":\"in-range\"},\"summary\":{\"points\":4,\"start\":142,\"end\":104,\"delta\":-38,\"mean\":123.75,\"min\":104,\"max\":142},\"series\":[{\"day\":\"2025-06-20\",\"value\":142},{\"day\":\"2025-10-08\",\"value\":131},{\"day\":\"2026-02-05\",\"value\":118},{\"day\":\"2026-05-26\",\"value\":104}]}" + ] + } + ], + "temperature": 0.45, + "maxTokens": 700, + "seed": 1618033, + "responseFormat": "json", + "timeoutMs": 60000 + } + ], + "enqueued": [], + "result": { + "hasProvider": false, + "text": "Aucune évaluation pour « LDL Cholesterol » pour le moment. Une valeur de laboratoire se lit d’abord contre tes propres prélèvements précédents — un intervalle de référence est un repère grossier, pas un verdict. Une valeur qui a à peine bougé sur plusieurs prélèvements raconte autre chose qu’une valeur qui vient de faire un saut. Ton prochain contrôle médical est le bon endroit pour la remettre en contexte.", + "cached": true, + "updatedAt": null + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "biomarker:parity-rich-parity-ldl", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": "58e0e17424c1b62fcd3006652f99f9fb89c35ea49fd1eb6212013a9a0bfb5d5f", + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "biomarker:parity-rich-parity-ldl", + "locale": "en", + "text": "Parity note for fresh-en.", + "items": null, + "inputHash": "58e0e17424c1b62fcd3006652f99f9fb89c35ea49fd1eb6212013a9a0bfb5d5f", + "snapshotHash": null, + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "biomarker:parity-rich-parity-ldl", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + } + ] + }, + "blocked-es": { + "calls": [ + { + "system": [ + "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", + "", + "YOUR DATA (graded snapshot):", + "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", + "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", + "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", + "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", + "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", + "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", + "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", + "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", + "", + "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", + "Moves available (choose the few that genuinely apply):", + "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", + "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", + "(c) name a genuine, earned win or a steady streak when the data shows one;", + "(d) one watch-item worth keeping an eye on;", + "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", + "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", + "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", + "", + "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", + "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", + "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", + "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", + "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", + "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", + "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", + "", + "OPENING — lead with meaning, not the metric value", + "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", + "", + "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", + "", + "JUDGE THE DATA HONESTLY (never invent a trend):", + "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", + "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", + "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", + "", + "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", + "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", + "", + "METRIC IDENTIFIERS STAY OUT OF PROSE", + "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", + "", + "GLP-1 DOSE SAFETY (contract, not style)", + "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", + "", + "ACUTE RED FLAGS (contract, not style)", + "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", + "", + "GROUNDING — ZERO HALLUCINATIONS", + "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", + "", + "TONE — a warm, motivating advisor, never clinical or alarming", + "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", + "", + "NOT AN INSIGHT — never recite mechanics", + "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", + "", + "INTERPRET, DON'T ENUMERATE", + "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", + "", + "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", + "", + "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", + "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", + "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", + "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", + "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", + "", + "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in Spanish: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", + "{ \"summary\": \"...\" }", + "", + "METRIC — LAB MARKER:", + "- DIFFERENT DATA SHAPE: this snapshot carries NO signal block. The base instruction's fields that reference signal do not apply here. Instead: marker (name, unit, referenceRange), dataCoverage, latest (value, takenAt, rangeStatus) and series (ascending, each { day, value }).", + "- Lead from latest.rangeStatus — the pre-computed placement against the reference range (inside/below/above it, or no range on file) — and from the direction across series. Both are already determined; STATE them, do NOT recompute them.", + "- Their own history leads: how this value sits against this person's own recent draws is the message. The reference range is a coarse secondary anchor, not a verdict — a value slightly outside the band that has held steady for years is a different story from the same value after a clear step.", + "- Steadiness is itself a finding: when the value barely moves across draws, name that constancy — for a lab marker it is the most common and most useful thing to say.", + "- Single draws vary (lab, time of day, fasting state). Do not build a trend out of TWO points; when the history is short, say honestly that it is too little for a direction yet.", + "- NO diagnosis, no attribution of cause, no medication or supplement recommendation. For clearly notable values, refer neutrally to a clinician for interpretation — that is the close, not an invented self-help step.", + "- One message: close with ONE doable step only when the finding genuinely implies one (e.g. take the value to the next check-up). When it is steady and in range, acknowledge that rather than manufacturing a step.", + "- The marker under assessment is \"LDL Cholesterol\".", + "", + "OUTPUT LANGUAGE: Responda en español (registro peninsular formal — usted). Use un vocabulario sanitario en español natural." + ], + "messages": [ + { + "role": "user", + "content": [ + "Date: 2026-06-15", + "OPENER HINT: Open by acknowledging steadiness or a streak, then the current value.", + "Write one short assessment of this person's lab marker. Open with how the marker SITS in plain words — the read, not the number (e.g. \"holding steady just inside the band\", \"sitting a step above where it usually runs\") — then bring in ONE concrete figure from the snapshot right after as support; never lead with the value. Place it against this person's own recent draws first and the reference range only as a coarse anchor. Close with one doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the number of draws and their recency.", + "", + "{\"locale\":\"es\",\"promptVersion\":\"6.3.0\",\"generatedForDay\":\"2026-06-15\",\"marker\":{\"name\":\"LDL Cholesterol\",\"unit\":\"mg/dL\",\"referenceRange\":{\"lower\":0,\"upper\":130}},\"dataCoverage\":{\"totalReadings\":4,\"readingsInSnapshot\":4,\"newestTakenAt\":\"2026-05-26T09:15:00.000Z\",\"oldestInSnapshot\":\"2025-06-20T09:15:00.000Z\"},\"latest\":{\"value\":104,\"takenAt\":\"2026-05-26T09:15:00.000Z\",\"rangeStatus\":\"in-range\"},\"summary\":{\"points\":4,\"start\":142,\"end\":104,\"delta\":-38,\"mean\":123.75,\"min\":104,\"max\":142},\"series\":[{\"day\":\"2025-06-20\",\"value\":142},{\"day\":\"2025-10-08\",\"value\":131},{\"day\":\"2026-02-05\",\"value\":118},{\"day\":\"2026-05-26\",\"value\":104}]}" + ] + } + ], + "temperature": 0.45, + "maxTokens": 700, + "seed": 1618033, + "responseFormat": "json", + "timeoutMs": 60000 + } + ], + "enqueued": [], + "result": { + "hasProvider": false, + "text": "Ahora mismo no hay ninguna valoración de «LDL Cholesterol». Un valor de laboratorio se lee primero frente a tus propias extracciones anteriores — un intervalo de referencia es un ancla aproximada, no un veredicto. Uno que apenas se ha movido a lo largo de varias extracciones cuenta algo distinto de uno que acaba de dar un salto. Tu próxima revisión médica es el lugar adecuado para ponerlo en contexto.", + "cached": true, + "updatedAt": null + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "biomarker:parity-rich-parity-ldl", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": "58e0e17424c1b62fcd3006652f99f9fb89c35ea49fd1eb6212013a9a0bfb5d5f", + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "biomarker:parity-rich-parity-ldl", + "locale": "en", + "text": "Parity note for fresh-en.", + "items": null, + "inputHash": "58e0e17424c1b62fcd3006652f99f9fb89c35ea49fd1eb6212013a9a0bfb5d5f", + "snapshotHash": null, + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "biomarker:parity-rich-parity-ldl", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "biomarker:parity-rich-parity-ldl", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + } + ] + }, + "bare-error-en": { + "calls": [], + "enqueued": [], + "result": { + "hasProvider": true, + "text": null, + "cached": false, + "updatedAt": null, + "insufficient": true + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "biomarker:parity-rich-parity-ldl", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": "58e0e17424c1b62fcd3006652f99f9fb89c35ea49fd1eb6212013a9a0bfb5d5f", + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "biomarker:parity-rich-parity-ldl", + "locale": "en", + "text": "Parity note for fresh-en.", + "items": null, + "inputHash": "58e0e17424c1b62fcd3006652f99f9fb89c35ea49fd1eb6212013a9a0bfb5d5f", + "snapshotHash": null, + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "biomarker:parity-rich-parity-ldl", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "biomarker:parity-rich-parity-ldl", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + } + ] + }, + "bare-error-de": { + "calls": [], + "enqueued": [], + "result": { + "hasProvider": true, + "text": null, + "cached": false, + "updatedAt": null, + "insufficient": true + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "biomarker:parity-rich-parity-ldl", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": "58e0e17424c1b62fcd3006652f99f9fb89c35ea49fd1eb6212013a9a0bfb5d5f", + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "biomarker:parity-rich-parity-ldl", + "locale": "en", + "text": "Parity note for fresh-en.", + "items": null, + "inputHash": "58e0e17424c1b62fcd3006652f99f9fb89c35ea49fd1eb6212013a9a0bfb5d5f", + "snapshotHash": null, + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "biomarker:parity-rich-parity-ldl", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "biomarker:parity-rich-parity-ldl", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + } + ] + }, + "keyless-en": { + "calls": [], + "enqueued": [], + "result": { + "hasProvider": false, + "text": "No assessment for \"LDL Cholesterol\" right now. A lab value reads first against your own previous draws — a reference range is a coarse anchor, not a verdict. One that has barely moved across several draws tells a different story from one that has just stepped. Your next check-up is the right place to put it in context.", + "cached": true, + "updatedAt": null + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "biomarker:parity-rich-parity-ldl", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": "58e0e17424c1b62fcd3006652f99f9fb89c35ea49fd1eb6212013a9a0bfb5d5f", + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "biomarker:parity-rich-parity-ldl", + "locale": "en", + "text": "Parity note for fresh-en.", + "items": null, + "inputHash": "58e0e17424c1b62fcd3006652f99f9fb89c35ea49fd1eb6212013a9a0bfb5d5f", + "snapshotHash": null, + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "biomarker:parity-rich-parity-ldl", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "biomarker:parity-rich-parity-ldl", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + } + ] + }, + "keyless-de": { + "calls": [], + "enqueued": [], + "result": { + "hasProvider": false, + "text": "Für diesen Laborwert zu „LDL Cholesterol“ liegt gerade keine Einschätzung vor. Ein Laborwert liest sich zuerst gegen die eigenen vorherigen Abnahmen — ein Referenzbereich ist ein grober Anker, kein Urteil. Ein Wert, der sich über mehrere Abnahmen kaum bewegt, erzählt etwas anderes als einer, der gerade gesprungen ist. Die nächste ärztliche Kontrolle ist der richtige Ort, ihn einzuordnen.", + "cached": true, + "updatedAt": null + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "biomarker:parity-rich-parity-ldl", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": "58e0e17424c1b62fcd3006652f99f9fb89c35ea49fd1eb6212013a9a0bfb5d5f", + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "biomarker:parity-rich-parity-ldl", + "locale": "en", + "text": "Parity note for fresh-en.", + "items": null, + "inputHash": "58e0e17424c1b62fcd3006652f99f9fb89c35ea49fd1eb6212013a9a0bfb5d5f", + "snapshotHash": null, + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "biomarker:parity-rich-parity-ldl", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "biomarker:parity-rich-parity-ldl", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + } + ] + }, + "keyless-read-only": { + "calls": [], + "enqueued": [], + "result": { + "hasProvider": false, + "text": "No assessment for \"LDL Cholesterol\" right now. A lab value reads first against your own previous draws — a reference range is a coarse anchor, not a verdict. One that has barely moved across several draws tells a different story from one that has just stepped. Your next check-up is the right place to put it in context.", + "cached": true, + "updatedAt": null + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "biomarker:parity-rich-parity-ldl", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": "58e0e17424c1b62fcd3006652f99f9fb89c35ea49fd1eb6212013a9a0bfb5d5f", + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "biomarker:parity-rich-parity-ldl", + "locale": "en", + "text": "Parity note for fresh-en.", + "items": null, + "inputHash": "58e0e17424c1b62fcd3006652f99f9fb89c35ea49fd1eb6212013a9a0bfb5d5f", + "snapshotHash": null, + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "biomarker:parity-rich-parity-ldl", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "biomarker:parity-rich-parity-ldl", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + } + ] + }, + "no-readings": { + "calls": [], + "enqueued": [], + "result": { + "hasProvider": true, + "text": null, + "cached": false, + "updatedAt": null, + "insufficient": true + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "biomarker:parity-rich-parity-ldl", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": "58e0e17424c1b62fcd3006652f99f9fb89c35ea49fd1eb6212013a9a0bfb5d5f", + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "biomarker:parity-rich-parity-ldl", + "locale": "en", + "text": "Parity note for fresh-en.", + "items": null, + "inputHash": "58e0e17424c1b62fcd3006652f99f9fb89c35ea49fd1eb6212013a9a0bfb5d5f", + "snapshotHash": null, + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "biomarker:parity-rich-parity-ldl", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "biomarker:parity-rich-parity-ldl", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + } + ] + }, + "foreign-marker": { + "calls": [], + "enqueued": [], + "result": { + "hasProvider": true, + "text": null, + "cached": false, + "updatedAt": null, + "insufficient": true + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "biomarker:parity-rich-parity-ldl", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": "58e0e17424c1b62fcd3006652f99f9fb89c35ea49fd1eb6212013a9a0bfb5d5f", + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "biomarker:parity-rich-parity-ldl", + "locale": "en", + "text": "Parity note for fresh-en.", + "items": null, + "inputHash": "58e0e17424c1b62fcd3006652f99f9fb89c35ea49fd1eb6212013a9a0bfb5d5f", + "snapshotHash": null, + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "biomarker:parity-rich-parity-ldl", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "biomarker:parity-rich-parity-ldl", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + } + ] + } +} diff --git a/tests/integration/__golden__/status-parity/blood-pressure.json b/tests/integration/__golden__/status-parity/blood-pressure.json new file mode 100644 index 000000000..5d407df01 --- /dev/null +++ b/tests/integration/__golden__/status-parity/blood-pressure.json @@ -0,0 +1,1598 @@ +{ + "read-only-cold": { + "calls": [], + "enqueued": [ + { + "userId": "parity-rich", + "metric": "blood-pressure", + "locale": "en" + } + ], + "result": { + "hasProvider": true, + "text": null, + "cached": false, + "updatedAt": null, + "preparing": true, + "revalidating": false + }, + "notes": [] + }, + "fresh-en": { + "calls": [ + { + "system": [ + "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", + "", + "YOUR DATA (graded snapshot):", + "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", + "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", + "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", + "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", + "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", + "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", + "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", + "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", + "", + "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", + "Moves available (choose the few that genuinely apply):", + "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", + "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", + "(c) name a genuine, earned win or a steady streak when the data shows one;", + "(d) one watch-item worth keeping an eye on;", + "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", + "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", + "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", + "", + "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", + "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", + "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", + "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", + "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", + "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", + "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", + "", + "OPENING — lead with meaning, not the metric value", + "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", + "", + "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", + "", + "JUDGE THE DATA HONESTLY (never invent a trend):", + "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", + "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", + "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", + "", + "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", + "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", + "", + "METRIC IDENTIFIERS STAY OUT OF PROSE", + "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", + "", + "GLP-1 DOSE SAFETY (contract, not style)", + "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", + "", + "ACUTE RED FLAGS (contract, not style)", + "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", + "", + "GROUNDING — ZERO HALLUCINATIONS", + "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", + "", + "TONE — a warm, motivating advisor, never clinical or alarming", + "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", + "", + "NOT AN INSIGHT — never recite mechanics", + "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", + "", + "INTERPRET, DON'T ENUMERATE", + "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", + "", + "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", + "", + "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", + "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", + "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", + "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", + "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", + "", + "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in English: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", + "{ \"summary\": \"...\" }", + "", + "METRIC — BLOOD PRESSURE:", + "- The snapshot carries systolic (bloodPressure.systolic) and diastolic (bloodPressure.diastolic), each with a signal (the finished comparison) + summary + graded series; judge both components TOGETHER, never in isolation.", + "- Trend over days/weeks: lead from bloodPressure.systolic.signal and bloodPressure.diastolic.signal — current, delta vs baselineLabel and outsideNormalSwing are already computed. STATE them, do NOT recompute. Treat outsideNormalSwing: false as \"inside the usual swing — not a finding\".", + "- Guideline anchors (ESH 2023), for placement, not diagnosis:", + " * Optimal < 120/80, normal 120-129/80-84, high-normal 130-139/85-89.", + " * Hypertension grade 1 from 140/90, grade 2 from 160/100, grade 3 from 180/110.", + " * Common treatment target for uncomplicated hypertension < 130/80 (age 18-69), < 140/80 (age ≥ 70).", + "- targets: if bloodPressure.targets is present, place values against the personal target band; inTargetPctLast30DailyPoints is the share of recent days in target.", + "- Medication link: bpMedications carries compliance7/compliance30. Mention bpMedicationContinuityVsSystolic.correlation and weightVsSystolic.correlation only when present and |r| > 0.4 — as an association, never a cause.", + "- Mood: mention moodContext.moodVsSystolicCorrelation only when present and |r| > 0.4.", + "- One message: if the values sit above the person's baseline, close with ONE doable step ONLY when the finding implies one (e.g. take a few readings at the same time of day for a few days, or — when adherence is patchy — make the medication routine more reliable). When values are stable and in target, say so honestly and name one thing worth keeping an eye on instead of manufacturing a step." + ], + "messages": [ + { + "role": "user", + "content": [ + "Date: 2026-06-15", + "OPENER HINT: Open with the overall read in plain words, then bring in the number as support — not number-first.", + "Write one short assessment of this person's blood pressure. Open with what the reading MEANS in plain words — the overall read, not the number (e.g. \"calm and right where you want it\", \"running a touch higher than your usual this week\") — then bring in the systolic/diastolic figures right after as support, judged as ONE pair and placed against their own weekly/monthly baseline; never lead with the numbers. Close with one doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the measurement count and recency.", + "", + "PREVIOUS ANALYSIS: none on file. Treat this as the user's first analysis for this domain — no improvement/regression delta to surface.", + "", + "", + "VARIETY: to keep your cards from reading the same, you may open this one by leading with the single most recent value and where it sits — only if the data genuinely supports that angle; never force it, never invent a trend.", + "", + "DATA STRENGTH: 94 day-buckets in the window, latest reading today.", + "", + "REPETITION: you have already given a similar \"no material change\" assessment for this metric 11 time(s) in a row. Do NOT restate the same level again. Acknowledge the continuity in one short clause (\"still holding steady, 12 checks running\") and then pivot to a DIFFERENT facet — consistency, time-of-day pattern, or a correlated metric — or, if nothing is genuinely actionable, say so plainly and skip the manufactured step.", + "", + "RELATIONS (statistically-screened associations in YOUR data involving this metric — descriptive, NEVER causal):", + "- Higher blood glucose looks like it goes with higher next-day systolic blood pressure in your data, on the evidence so far. It is a pattern to watch, not a cause. (n=23, r=0.91)", + "- Higher systolic blood pressure looks like it goes with higher next-day weight in your data, on the evidence so far. It is a pattern to watch, not a cause. (n=47, r=0.88)", + "You MAY weave at most ONE of these in if it adds genuine insight, phrased as an \"association\" / \"tends to go with\" — never as a cause, and never alter the numbers. Skip them if the metric's own finding is the stronger story.", + "", + "", + "{\"locale\":\"en\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"blood_pressure\",\"dataCoverage\":{\"totalMeasurements\":258,\"totalSpanDays\":139,\"newestMeasurementDaysAgo\":0},\"bloodPressure\":{\"systolic\":{\"signal\":{\"metric\":\"your systolic\",\"unit\":\"mmHg\",\"current\":127.29,\"currentWindowDays\":14,\"baseline\":126.97,\"baselineLabel\":\"your monthly average\",\"delta\":0.32,\"deltaPct\":0.3,\"spread\":2.38,\"outsideNormalSwing\":false,\"direction\":\"lower-better\",\"n\":14,\"newestDaysAgo\":0,\"normalRange\":{\"low\":120,\"high\":129},\"placement\":\"in band\"},\"summary\":{\"points\":94,\"start\":128,\"end\":125,\"delta\":-3,\"mean\":125.39,\"min\":116,\"max\":134},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":119,\"max\":119,\"mean\":119,\"n\":1},{\"date\":\"2026-05-28\",\"min\":121,\"max\":121,\"mean\":121,\"n\":1},{\"date\":\"2026-05-30\",\"min\":126,\"max\":126,\"mean\":126,\"n\":1},{\"date\":\"2026-05-31\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-02\",\"min\":125,\"max\":125,\"mean\":125,\"n\":1},{\"date\":\"2026-06-03\",\"min\":127,\"max\":127,\"mean\":127,\"n\":1},{\"date\":\"2026-06-05\",\"min\":130,\"max\":130,\"mean\":130,\"n\":1},{\"date\":\"2026-06-06\",\"min\":132,\"max\":132,\"mean\":132,\"n\":1},{\"date\":\"2026-06-08\",\"min\":134,\"max\":134,\"mean\":134,\"n\":1},{\"date\":\"2026-06-09\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-11\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-12\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-14\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-15\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":117,\"max\":121,\"mean\":118.75,\"n\":4},{\"weekISO\":\"2026-W13\",\"min\":119,\"max\":128,\"mean\":123.2,\"n\":5},{\"weekISO\":\"2026-W14\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"weekISO\":\"2026-W15\",\"min\":128,\"max\":134,\"mean\":129.4,\"n\":5},{\"weekISO\":\"2026-W16\",\"min\":121,\"max\":128,\"mean\":122.4,\"n\":5},{\"weekISO\":\"2026-W17\",\"min\":116,\"max\":122,\"mean\":118.75,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":121,\"max\":131,\"mean\":126,\"n\":5},{\"weekISO\":\"2026-W19\",\"min\":129,\"max\":133,\"mean\":131,\"n\":5},{\"weekISO\":\"2026-W20\",\"min\":124,\"max\":125,\"mean\":124.75,\"n\":4},{\"weekISO\":\"2026-W21\",\"min\":117,\"max\":124,\"mean\":119.8,\"n\":5},{\"weekISO\":\"2026-W22\",\"min\":123,\"max\":123,\"mean\":123,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"month\":\"2026-02\",\"min\":116,\"max\":134,\"mean\":124.22,\"n\":18},{\"month\":\"2026-03\",\"min\":124,\"max\":133,\"mean\":128.18,\"n\":11}],\"yearly\":[]}},\"diastolic\":{\"signal\":{\"metric\":\"your diastolic\",\"unit\":\"mmHg\",\"current\":81.71,\"currentWindowDays\":14,\"baseline\":81.43,\"baselineLabel\":\"your monthly average\",\"delta\":0.29,\"deltaPct\":0.4,\"spread\":1.93,\"outsideNormalSwing\":false,\"direction\":\"lower-better\",\"n\":14,\"newestDaysAgo\":0,\"normalRange\":{\"low\":70,\"high\":79},\"placement\":\"above band\"},\"summary\":{\"points\":94,\"start\":82,\"end\":80,\"delta\":-2,\"mean\":80.31,\"min\":74,\"max\":86},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":76,\"max\":76,\"mean\":76,\"n\":1},{\"date\":\"2026-05-28\",\"min\":77,\"max\":77,\"mean\":77,\"n\":1},{\"date\":\"2026-05-30\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-05-31\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-02\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-03\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-05\",\"min\":86,\"max\":86,\"mean\":86,\"n\":1},{\"date\":\"2026-06-06\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-08\",\"min\":84,\"max\":84,\"mean\":84,\"n\":1},{\"date\":\"2026-06-09\",\"min\":85,\"max\":85,\"mean\":85,\"n\":1},{\"date\":\"2026-06-11\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-12\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-14\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-15\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":75,\"max\":79,\"mean\":77,\"n\":4},{\"weekISO\":\"2026-W13\",\"min\":74,\"max\":79,\"mean\":77.4,\"n\":5},{\"weekISO\":\"2026-W14\",\"min\":83,\"max\":85,\"mean\":83.75,\"n\":4},{\"weekISO\":\"2026-W15\",\"min\":79,\"max\":86,\"mean\":82.6,\"n\":5},{\"weekISO\":\"2026-W16\",\"min\":76,\"max\":80,\"mean\":78.2,\"n\":5},{\"weekISO\":\"2026-W17\",\"min\":74,\"max\":78,\"mean\":76.75,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":77,\"max\":84,\"mean\":80.6,\"n\":5},{\"weekISO\":\"2026-W19\",\"min\":82,\"max\":86,\"mean\":83.8,\"n\":5},{\"weekISO\":\"2026-W20\",\"min\":80,\"max\":81,\"mean\":80.5,\"n\":4},{\"weekISO\":\"2026-W21\",\"min\":74,\"max\":78,\"mean\":76.2,\"n\":5},{\"weekISO\":\"2026-W22\",\"min\":78,\"max\":78,\"mean\":78,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":80,\"max\":86,\"mean\":83,\"n\":4},{\"month\":\"2026-02\",\"min\":75,\"max\":85,\"mean\":79.28,\"n\":18},{\"month\":\"2026-03\",\"min\":78,\"max\":86,\"mean\":82,\"n\":11}],\"yearly\":[]}},\"paired\":{\"summary\":{\"points\":94,\"start\":105,\"end\":102.5,\"delta\":-2.5,\"mean\":102.85,\"min\":95.5,\"max\":110},\"series\":[{\"day\":\"2026-03-11\",\"sys\":125,\"dia\":82,\"inTarget\":false},{\"day\":\"2026-03-10\",\"sys\":125,\"dia\":82,\"inTarget\":false},{\"day\":\"2026-03-08\",\"sys\":132,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-03-07\",\"sys\":131,\"dia\":86,\"inTarget\":false},{\"day\":\"2026-03-05\",\"sys\":130,\"dia\":84,\"inTarget\":false},{\"day\":\"2026-03-04\",\"sys\":129,\"dia\":83,\"inTarget\":false},{\"day\":\"2026-03-02\",\"sys\":133,\"dia\":85,\"inTarget\":false},{\"day\":\"2026-03-01\",\"sys\":131,\"dia\":84,\"inTarget\":false},{\"day\":\"2026-02-27\",\"sys\":127,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-02-26\",\"sys\":125,\"dia\":79,\"inTarget\":true},{\"day\":\"2026-02-24\",\"sys\":121,\"dia\":80,\"inTarget\":false},{\"day\":\"2026-02-23\",\"sys\":126,\"dia\":78,\"inTarget\":true},{\"day\":\"2026-02-21\",\"sys\":122,\"dia\":75,\"inTarget\":true},{\"day\":\"2026-02-20\",\"sys\":120,\"dia\":79,\"inTarget\":true},{\"day\":\"2026-02-18\",\"sys\":117,\"dia\":76,\"inTarget\":true},{\"day\":\"2026-02-17\",\"sys\":116,\"dia\":75,\"inTarget\":true},{\"day\":\"2026-02-15\",\"sys\":122,\"dia\":78,\"inTarget\":true},{\"day\":\"2026-02-14\",\"sys\":121,\"dia\":78,\"inTarget\":true},{\"day\":\"2026-02-12\",\"sys\":121,\"dia\":77,\"inTarget\":true},{\"day\":\"2026-02-11\",\"sys\":121,\"dia\":77,\"inTarget\":true},{\"day\":\"2026-02-09\",\"sys\":128,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-02-08\",\"sys\":129,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-02-06\",\"sys\":129,\"dia\":80,\"inTarget\":false},{\"day\":\"2026-02-05\",\"sys\":129,\"dia\":85,\"inTarget\":false},{\"day\":\"2026-02-03\",\"sys\":128,\"dia\":84,\"inTarget\":false},{\"day\":\"2026-02-02\",\"sys\":134,\"dia\":83,\"inTarget\":false},{\"day\":\"2026-01-31\",\"sys\":132,\"dia\":86,\"inTarget\":false},{\"day\":\"2026-01-30\",\"sys\":130,\"dia\":85,\"inTarget\":false},{\"day\":\"2026-01-28\",\"sys\":127,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-01-27\",\"sys\":125,\"dia\":80,\"inTarget\":false}]},\"targets\":{\"systolic\":{\"min\":120,\"max\":129},\"diastolic\":{\"min\":70,\"max\":79},\"inTargetPctLast30DailyPoints\":42.6}},\"weightVsSystolic\":{\"correlation\":{\"r\":0.385,\"strength\":\"schwach\",\"n\":47},\"pairs\":[{\"day\":\"2026-04-24\",\"weight\":83.09,\"systolic\":120},{\"day\":\"2026-04-22\",\"weight\":83,\"systolic\":117},{\"day\":\"2026-04-18\",\"weight\":83,\"systolic\":121},{\"day\":\"2026-04-16\",\"weight\":83.09,\"systolic\":121},{\"day\":\"2026-04-12\",\"weight\":83.33,\"systolic\":128},{\"day\":\"2026-04-10\",\"weight\":83.45,\"systolic\":129},{\"day\":\"2026-04-06\",\"weight\":83.55,\"systolic\":134},{\"day\":\"2026-04-04\",\"weight\":83.51,\"systolic\":132},{\"day\":\"2026-03-31\",\"weight\":83.25,\"systolic\":125},{\"day\":\"2026-03-29\",\"weight\":83.07,\"systolic\":128},{\"day\":\"2026-03-25\",\"weight\":82.71,\"systolic\":119},{\"day\":\"2026-03-23\",\"weight\":82.58,\"systolic\":123},{\"day\":\"2026-03-19\",\"weight\":82.5,\"systolic\":118},{\"day\":\"2026-03-17\",\"weight\":82.55,\"systolic\":117},{\"day\":\"2026-03-13\",\"weight\":82.77,\"systolic\":125},{\"day\":\"2026-03-11\",\"weight\":82.9,\"systolic\":125},{\"day\":\"2026-03-07\",\"weight\":83.07,\"systolic\":131},{\"day\":\"2026-03-05\",\"weight\":83.07,\"systolic\":130},{\"day\":\"2026-03-01\",\"weight\":82.89,\"systolic\":131},{\"day\":\"2026-02-27\",\"weight\":82.73,\"systolic\":127},{\"day\":\"2026-02-23\",\"weight\":82.36,\"systolic\":126},{\"day\":\"2026-02-21\",\"weight\":82.2,\"systolic\":122},{\"day\":\"2026-02-17\",\"weight\":82.03,\"systolic\":116},{\"day\":\"2026-02-15\",\"weight\":82.04,\"systolic\":122},{\"day\":\"2026-02-11\",\"weight\":82.21,\"systolic\":121},{\"day\":\"2026-02-09\",\"weight\":82.34,\"systolic\":128},{\"day\":\"2026-02-05\",\"weight\":82.56,\"systolic\":129},{\"day\":\"2026-02-03\",\"weight\":82.6,\"systolic\":128},{\"day\":\"2026-01-30\",\"weight\":82.51,\"systolic\":130},{\"day\":\"2026-01-28\",\"weight\":82.38,\"systolic\":127}]},\"bpMedicationContinuityVsSystolic\":{\"expectedIntakesPerDay\":1,\"medicationCount\":1,\"correlation\":null,\"series\":[{\"day\":\"2026-03-11\",\"dayOffset\":96,\"sys\":125,\"continuityPct\":100},{\"day\":\"2026-03-10\",\"dayOffset\":97,\"sys\":125,\"continuityPct\":100},{\"day\":\"2026-03-08\",\"dayOffset\":99,\"sys\":132,\"continuityPct\":100},{\"day\":\"2026-03-07\",\"dayOffset\":100,\"sys\":131,\"continuityPct\":0},{\"day\":\"2026-03-05\",\"dayOffset\":102,\"sys\":130,\"continuityPct\":0},{\"day\":\"2026-03-04\",\"dayOffset\":103,\"sys\":129,\"continuityPct\":0},{\"day\":\"2026-03-02\",\"dayOffset\":105,\"sys\":133,\"continuityPct\":0},{\"day\":\"2026-03-01\",\"dayOffset\":106,\"sys\":131,\"continuityPct\":0},{\"day\":\"2026-02-27\",\"dayOffset\":108,\"sys\":127,\"continuityPct\":0},{\"day\":\"2026-02-26\",\"dayOffset\":109,\"sys\":125,\"continuityPct\":0},{\"day\":\"2026-02-24\",\"dayOffset\":111,\"sys\":121,\"continuityPct\":0},{\"day\":\"2026-02-23\",\"dayOffset\":112,\"sys\":126,\"continuityPct\":0},{\"day\":\"2026-02-21\",\"dayOffset\":114,\"sys\":122,\"continuityPct\":0},{\"day\":\"2026-02-20\",\"dayOffset\":115,\"sys\":120,\"continuityPct\":0},{\"day\":\"2026-02-18\",\"dayOffset\":117,\"sys\":117,\"continuityPct\":0},{\"day\":\"2026-02-17\",\"dayOffset\":118,\"sys\":116,\"continuityPct\":0},{\"day\":\"2026-02-15\",\"dayOffset\":120,\"sys\":122,\"continuityPct\":0},{\"day\":\"2026-02-14\",\"dayOffset\":121,\"sys\":121,\"continuityPct\":0},{\"day\":\"2026-02-12\",\"dayOffset\":123,\"sys\":121,\"continuityPct\":0},{\"day\":\"2026-02-11\",\"dayOffset\":124,\"sys\":121,\"continuityPct\":0},{\"day\":\"2026-02-09\",\"dayOffset\":126,\"sys\":128,\"continuityPct\":0},{\"day\":\"2026-02-08\",\"dayOffset\":127,\"sys\":129,\"continuityPct\":0},{\"day\":\"2026-02-06\",\"dayOffset\":129,\"sys\":129,\"continuityPct\":0},{\"day\":\"2026-02-05\",\"dayOffset\":130,\"sys\":129,\"continuityPct\":0},{\"day\":\"2026-02-03\",\"dayOffset\":132,\"sys\":128,\"continuityPct\":0},{\"day\":\"2026-02-02\",\"dayOffset\":133,\"sys\":134,\"continuityPct\":0},{\"day\":\"2026-01-31\",\"dayOffset\":135,\"sys\":132,\"continuityPct\":0},{\"day\":\"2026-01-30\",\"dayOffset\":136,\"sys\":130,\"continuityPct\":0},{\"day\":\"2026-01-28\",\"dayOffset\":138,\"sys\":127,\"continuityPct\":0},{\"day\":\"2026-01-27\",\"dayOffset\":139,\"sys\":125,\"continuityPct\":0}]},\"bpMedications\":[{\"name\":\"Ramipril\",\"dose\":\"5mg\",\"schedulesPerDay\":1,\"compliance7\":67,\"compliance30\":79}],\"moodContext\":{\"points\":70,\"mean\":3.03,\"latest\":1,\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-15\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W15\",\"min\":2,\"max\":5,\"mean\":3.33,\"n\":6},{\"weekISO\":\"2026-W16\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":1,\"max\":5,\"mean\":3,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":1,\"max\":4,\"mean\":2.57,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1}],\"monthly\":[],\"yearly\":[]},\"moodVsSystolicCorrelation\":null}}" + ] + } + ], + "temperature": 0.45, + "maxTokens": 250, + "seed": 1618033, + "responseFormat": "json", + "timeoutMs": 60000 + } + ], + "enqueued": [], + "result": { + "hasProvider": true, + "text": "Parity note for fresh-en.", + "cached": false, + "updatedAt": "2026-06-15T10:00:00.000Z" + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "blood-pressure", + "locale": "en", + "text": "Parity note for fresh-en.", + "items": null, + "inputHash": null, + "snapshotHash": "07d02bb70e6afb9e8c4d9a8e8bc34e5c4ac8ffefc52501fde23f86d3a43c3ced", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + } + ] + }, + "cached-en": { + "calls": [], + "enqueued": [], + "result": { + "hasProvider": true, + "text": "Parity note for fresh-en.", + "cached": true, + "updatedAt": "2026-06-15T10:00:00.000Z" + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "blood-pressure", + "locale": "en", + "text": "Parity note for fresh-en.", + "items": null, + "inputHash": null, + "snapshotHash": "07d02bb70e6afb9e8c4d9a8e8bc34e5c4ac8ffefc52501fde23f86d3a43c3ced", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + } + ] + }, + "forced-unchanged-en": { + "calls": [], + "enqueued": [], + "result": { + "hasProvider": true, + "text": "Parity note for fresh-en.", + "cached": true, + "updatedAt": "2026-06-15T10:00:00.000Z" + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "blood-pressure", + "locale": "en", + "text": "Parity note for fresh-en.", + "items": null, + "inputHash": null, + "snapshotHash": "07d02bb70e6afb9e8c4d9a8e8bc34e5c4ac8ffefc52501fde23f86d3a43c3ced", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + } + ] + }, + "next-day-en": { + "calls": [ + { + "system": [ + "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", + "", + "YOUR DATA (graded snapshot):", + "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", + "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", + "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", + "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", + "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", + "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", + "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", + "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", + "", + "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", + "Moves available (choose the few that genuinely apply):", + "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", + "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", + "(c) name a genuine, earned win or a steady streak when the data shows one;", + "(d) one watch-item worth keeping an eye on;", + "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", + "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", + "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", + "", + "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", + "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", + "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", + "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", + "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", + "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", + "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", + "", + "OPENING — lead with meaning, not the metric value", + "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", + "", + "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", + "", + "JUDGE THE DATA HONESTLY (never invent a trend):", + "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", + "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", + "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", + "", + "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", + "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", + "", + "METRIC IDENTIFIERS STAY OUT OF PROSE", + "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", + "", + "GLP-1 DOSE SAFETY (contract, not style)", + "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", + "", + "ACUTE RED FLAGS (contract, not style)", + "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", + "", + "GROUNDING — ZERO HALLUCINATIONS", + "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", + "", + "TONE — a warm, motivating advisor, never clinical or alarming", + "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", + "", + "NOT AN INSIGHT — never recite mechanics", + "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", + "", + "INTERPRET, DON'T ENUMERATE", + "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", + "", + "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", + "", + "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", + "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", + "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", + "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", + "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", + "", + "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in English: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", + "{ \"summary\": \"...\" }", + "", + "METRIC — BLOOD PRESSURE:", + "- The snapshot carries systolic (bloodPressure.systolic) and diastolic (bloodPressure.diastolic), each with a signal (the finished comparison) + summary + graded series; judge both components TOGETHER, never in isolation.", + "- Trend over days/weeks: lead from bloodPressure.systolic.signal and bloodPressure.diastolic.signal — current, delta vs baselineLabel and outsideNormalSwing are already computed. STATE them, do NOT recompute. Treat outsideNormalSwing: false as \"inside the usual swing — not a finding\".", + "- Guideline anchors (ESH 2023), for placement, not diagnosis:", + " * Optimal < 120/80, normal 120-129/80-84, high-normal 130-139/85-89.", + " * Hypertension grade 1 from 140/90, grade 2 from 160/100, grade 3 from 180/110.", + " * Common treatment target for uncomplicated hypertension < 130/80 (age 18-69), < 140/80 (age ≥ 70).", + "- targets: if bloodPressure.targets is present, place values against the personal target band; inTargetPctLast30DailyPoints is the share of recent days in target.", + "- Medication link: bpMedications carries compliance7/compliance30. Mention bpMedicationContinuityVsSystolic.correlation and weightVsSystolic.correlation only when present and |r| > 0.4 — as an association, never a cause.", + "- Mood: mention moodContext.moodVsSystolicCorrelation only when present and |r| > 0.4.", + "- One message: if the values sit above the person's baseline, close with ONE doable step ONLY when the finding implies one (e.g. take a few readings at the same time of day for a few days, or — when adherence is patchy — make the medication routine more reliable). When values are stable and in target, say so honestly and name one thing worth keeping an eye on instead of manufacturing a step." + ], + "messages": [ + { + "role": "user", + "content": [ + "Date: 2026-06-16", + "OPENER HINT: Open by acknowledging steadiness or a streak, then the current value.", + "Write one short assessment of this person's blood pressure. Open with what the reading MEANS in plain words — the overall read, not the number (e.g. \"calm and right where you want it\", \"running a touch higher than your usual this week\") — then bring in the systolic/diastolic figures right after as support, judged as ONE pair and placed against their own weekly/monthly baseline; never lead with the numbers. Close with one doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the measurement count and recency.", + "", + "PREVIOUS ANALYSIS (1 day ago, 2026-06-15):", + "Parity note for fresh-en.", + "", + "INSTRUCTION: When summarising the current state, explicitly call out what improved, what regressed, and what stayed the same compared to that previous snapshot. Use phrases like \"down 4 mmHg from your last check\" or \"same elevated trend as last week\". Do not invent numbers — only compare what's in the snapshot below.", + "", + "", + "VARIETY: to keep your cards from reading the same, you may open this one by leading with the single most recent value and where it sits — only if the data genuinely supports that angle; never force it, never invent a trend.", + "", + "DATA STRENGTH: 94 day-buckets in the window, latest reading 1 day ago.", + "", + "REPETITION: you have already given a similar \"no material change\" assessment for this metric 11 time(s) in a row. Do NOT restate the same level again. Acknowledge the continuity in one short clause (\"still holding steady, 12 checks running\") and then pivot to a DIFFERENT facet — consistency, time-of-day pattern, or a correlated metric — or, if nothing is genuinely actionable, say so plainly and skip the manufactured step.", + "", + "RELATIONS (statistically-screened associations in YOUR data involving this metric — descriptive, NEVER causal):", + "- Higher blood glucose looks like it goes with higher next-day systolic blood pressure in your data, on the evidence so far. It is a pattern to watch, not a cause. (n=23, r=0.91)", + "- Higher systolic blood pressure looks like it goes with higher next-day weight in your data, on the evidence so far. It is a pattern to watch, not a cause. (n=47, r=0.88)", + "You MAY weave at most ONE of these in if it adds genuine insight, phrased as an \"association\" / \"tends to go with\" — never as a cause, and never alter the numbers. Skip them if the metric's own finding is the stronger story.", + "", + "", + "{\"locale\":\"en\",\"generatedForDay\":\"2026-06-16\",\"focus\":\"blood_pressure\",\"dataCoverage\":{\"totalMeasurements\":258,\"totalSpanDays\":139,\"newestMeasurementDaysAgo\":1},\"bloodPressure\":{\"systolic\":{\"signal\":{\"metric\":\"your systolic\",\"unit\":\"mmHg\",\"current\":127.29,\"currentWindowDays\":14,\"baseline\":126.66,\"baselineLabel\":\"your monthly average\",\"delta\":0.63,\"deltaPct\":0.5,\"spread\":2.2,\"outsideNormalSwing\":false,\"direction\":\"lower-better\",\"n\":14,\"newestDaysAgo\":1,\"normalRange\":{\"low\":120,\"high\":129},\"placement\":\"in band\"},\"summary\":{\"points\":94,\"start\":128,\"end\":125,\"delta\":-3,\"mean\":125.39,\"min\":116,\"max\":134},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":119,\"max\":119,\"mean\":119,\"n\":1},{\"date\":\"2026-05-28\",\"min\":121,\"max\":121,\"mean\":121,\"n\":1},{\"date\":\"2026-05-30\",\"min\":126,\"max\":126,\"mean\":126,\"n\":1},{\"date\":\"2026-05-31\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-02\",\"min\":125,\"max\":125,\"mean\":125,\"n\":1},{\"date\":\"2026-06-03\",\"min\":127,\"max\":127,\"mean\":127,\"n\":1},{\"date\":\"2026-06-05\",\"min\":130,\"max\":130,\"mean\":130,\"n\":1},{\"date\":\"2026-06-06\",\"min\":132,\"max\":132,\"mean\":132,\"n\":1},{\"date\":\"2026-06-08\",\"min\":134,\"max\":134,\"mean\":134,\"n\":1},{\"date\":\"2026-06-09\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-11\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-12\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-14\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-15\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":118,\"max\":121,\"mean\":119.33,\"n\":3},{\"weekISO\":\"2026-W13\",\"min\":119,\"max\":128,\"mean\":123.2,\"n\":5},{\"weekISO\":\"2026-W14\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"weekISO\":\"2026-W15\",\"min\":128,\"max\":134,\"mean\":129.4,\"n\":5},{\"weekISO\":\"2026-W16\",\"min\":121,\"max\":128,\"mean\":122.4,\"n\":5},{\"weekISO\":\"2026-W17\",\"min\":116,\"max\":122,\"mean\":118.75,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":121,\"max\":131,\"mean\":126,\"n\":5},{\"weekISO\":\"2026-W19\",\"min\":129,\"max\":133,\"mean\":131,\"n\":5},{\"weekISO\":\"2026-W20\",\"min\":124,\"max\":125,\"mean\":124.75,\"n\":4},{\"weekISO\":\"2026-W21\",\"min\":117,\"max\":124,\"mean\":119.8,\"n\":5},{\"weekISO\":\"2026-W22\",\"min\":123,\"max\":123,\"mean\":123,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"month\":\"2026-02\",\"min\":116,\"max\":134,\"mean\":124.22,\"n\":18},{\"month\":\"2026-03\",\"min\":117,\"max\":133,\"mean\":127.25,\"n\":12}],\"yearly\":[]}},\"diastolic\":{\"signal\":{\"metric\":\"your diastolic\",\"unit\":\"mmHg\",\"current\":81.71,\"currentWindowDays\":14,\"baseline\":81.34,\"baselineLabel\":\"your monthly average\",\"delta\":0.37,\"deltaPct\":0.5,\"spread\":1.89,\"outsideNormalSwing\":false,\"direction\":\"lower-better\",\"n\":14,\"newestDaysAgo\":1,\"normalRange\":{\"low\":70,\"high\":79},\"placement\":\"above band\"},\"summary\":{\"points\":94,\"start\":82,\"end\":80,\"delta\":-2,\"mean\":80.31,\"min\":74,\"max\":86},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":76,\"max\":76,\"mean\":76,\"n\":1},{\"date\":\"2026-05-28\",\"min\":77,\"max\":77,\"mean\":77,\"n\":1},{\"date\":\"2026-05-30\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-05-31\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-02\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-03\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-05\",\"min\":86,\"max\":86,\"mean\":86,\"n\":1},{\"date\":\"2026-06-06\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-08\",\"min\":84,\"max\":84,\"mean\":84,\"n\":1},{\"date\":\"2026-06-09\",\"min\":85,\"max\":85,\"mean\":85,\"n\":1},{\"date\":\"2026-06-11\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-12\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-14\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-15\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":75,\"max\":78,\"mean\":76.33,\"n\":3},{\"weekISO\":\"2026-W13\",\"min\":74,\"max\":79,\"mean\":77.4,\"n\":5},{\"weekISO\":\"2026-W14\",\"min\":83,\"max\":85,\"mean\":83.75,\"n\":4},{\"weekISO\":\"2026-W15\",\"min\":79,\"max\":86,\"mean\":82.6,\"n\":5},{\"weekISO\":\"2026-W16\",\"min\":76,\"max\":80,\"mean\":78.2,\"n\":5},{\"weekISO\":\"2026-W17\",\"min\":74,\"max\":78,\"mean\":76.75,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":77,\"max\":84,\"mean\":80.6,\"n\":5},{\"weekISO\":\"2026-W19\",\"min\":82,\"max\":86,\"mean\":83.8,\"n\":5},{\"weekISO\":\"2026-W20\",\"min\":80,\"max\":81,\"mean\":80.5,\"n\":4},{\"weekISO\":\"2026-W21\",\"min\":74,\"max\":78,\"mean\":76.2,\"n\":5},{\"weekISO\":\"2026-W22\",\"min\":78,\"max\":78,\"mean\":78,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":80,\"max\":86,\"mean\":83,\"n\":4},{\"month\":\"2026-02\",\"min\":75,\"max\":85,\"mean\":79.28,\"n\":18},{\"month\":\"2026-03\",\"min\":78,\"max\":86,\"mean\":81.75,\"n\":12}],\"yearly\":[]}},\"paired\":{\"summary\":{\"points\":94,\"start\":105,\"end\":102.5,\"delta\":-2.5,\"mean\":102.85,\"min\":95.5,\"max\":110},\"series\":[{\"day\":\"2026-03-11\",\"sys\":125,\"dia\":82,\"inTarget\":false},{\"day\":\"2026-03-10\",\"sys\":125,\"dia\":82,\"inTarget\":false},{\"day\":\"2026-03-08\",\"sys\":132,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-03-07\",\"sys\":131,\"dia\":86,\"inTarget\":false},{\"day\":\"2026-03-05\",\"sys\":130,\"dia\":84,\"inTarget\":false},{\"day\":\"2026-03-04\",\"sys\":129,\"dia\":83,\"inTarget\":false},{\"day\":\"2026-03-02\",\"sys\":133,\"dia\":85,\"inTarget\":false},{\"day\":\"2026-03-01\",\"sys\":131,\"dia\":84,\"inTarget\":false},{\"day\":\"2026-02-27\",\"sys\":127,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-02-26\",\"sys\":125,\"dia\":79,\"inTarget\":true},{\"day\":\"2026-02-24\",\"sys\":121,\"dia\":80,\"inTarget\":false},{\"day\":\"2026-02-23\",\"sys\":126,\"dia\":78,\"inTarget\":true},{\"day\":\"2026-02-21\",\"sys\":122,\"dia\":75,\"inTarget\":true},{\"day\":\"2026-02-20\",\"sys\":120,\"dia\":79,\"inTarget\":true},{\"day\":\"2026-02-18\",\"sys\":117,\"dia\":76,\"inTarget\":true},{\"day\":\"2026-02-17\",\"sys\":116,\"dia\":75,\"inTarget\":true},{\"day\":\"2026-02-15\",\"sys\":122,\"dia\":78,\"inTarget\":true},{\"day\":\"2026-02-14\",\"sys\":121,\"dia\":78,\"inTarget\":true},{\"day\":\"2026-02-12\",\"sys\":121,\"dia\":77,\"inTarget\":true},{\"day\":\"2026-02-11\",\"sys\":121,\"dia\":77,\"inTarget\":true},{\"day\":\"2026-02-09\",\"sys\":128,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-02-08\",\"sys\":129,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-02-06\",\"sys\":129,\"dia\":80,\"inTarget\":false},{\"day\":\"2026-02-05\",\"sys\":129,\"dia\":85,\"inTarget\":false},{\"day\":\"2026-02-03\",\"sys\":128,\"dia\":84,\"inTarget\":false},{\"day\":\"2026-02-02\",\"sys\":134,\"dia\":83,\"inTarget\":false},{\"day\":\"2026-01-31\",\"sys\":132,\"dia\":86,\"inTarget\":false},{\"day\":\"2026-01-30\",\"sys\":130,\"dia\":85,\"inTarget\":false},{\"day\":\"2026-01-28\",\"sys\":127,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-01-27\",\"sys\":125,\"dia\":80,\"inTarget\":false}]},\"targets\":{\"systolic\":{\"min\":120,\"max\":129},\"diastolic\":{\"min\":70,\"max\":79},\"inTargetPctLast30DailyPoints\":42.6}},\"weightVsSystolic\":{\"correlation\":{\"r\":0.385,\"strength\":\"schwach\",\"n\":47},\"pairs\":[{\"day\":\"2026-04-24\",\"weight\":83.09,\"systolic\":120},{\"day\":\"2026-04-22\",\"weight\":83,\"systolic\":117},{\"day\":\"2026-04-18\",\"weight\":83,\"systolic\":121},{\"day\":\"2026-04-16\",\"weight\":83.09,\"systolic\":121},{\"day\":\"2026-04-12\",\"weight\":83.33,\"systolic\":128},{\"day\":\"2026-04-10\",\"weight\":83.45,\"systolic\":129},{\"day\":\"2026-04-06\",\"weight\":83.55,\"systolic\":134},{\"day\":\"2026-04-04\",\"weight\":83.51,\"systolic\":132},{\"day\":\"2026-03-31\",\"weight\":83.25,\"systolic\":125},{\"day\":\"2026-03-29\",\"weight\":83.07,\"systolic\":128},{\"day\":\"2026-03-25\",\"weight\":82.71,\"systolic\":119},{\"day\":\"2026-03-23\",\"weight\":82.58,\"systolic\":123},{\"day\":\"2026-03-19\",\"weight\":82.5,\"systolic\":118},{\"day\":\"2026-03-17\",\"weight\":82.55,\"systolic\":117},{\"day\":\"2026-03-13\",\"weight\":82.77,\"systolic\":125},{\"day\":\"2026-03-11\",\"weight\":82.9,\"systolic\":125},{\"day\":\"2026-03-07\",\"weight\":83.07,\"systolic\":131},{\"day\":\"2026-03-05\",\"weight\":83.07,\"systolic\":130},{\"day\":\"2026-03-01\",\"weight\":82.89,\"systolic\":131},{\"day\":\"2026-02-27\",\"weight\":82.73,\"systolic\":127},{\"day\":\"2026-02-23\",\"weight\":82.36,\"systolic\":126},{\"day\":\"2026-02-21\",\"weight\":82.2,\"systolic\":122},{\"day\":\"2026-02-17\",\"weight\":82.03,\"systolic\":116},{\"day\":\"2026-02-15\",\"weight\":82.04,\"systolic\":122},{\"day\":\"2026-02-11\",\"weight\":82.21,\"systolic\":121},{\"day\":\"2026-02-09\",\"weight\":82.34,\"systolic\":128},{\"day\":\"2026-02-05\",\"weight\":82.56,\"systolic\":129},{\"day\":\"2026-02-03\",\"weight\":82.6,\"systolic\":128},{\"day\":\"2026-01-30\",\"weight\":82.51,\"systolic\":130},{\"day\":\"2026-01-28\",\"weight\":82.38,\"systolic\":127}]},\"bpMedicationContinuityVsSystolic\":{\"expectedIntakesPerDay\":1,\"medicationCount\":1,\"correlation\":null,\"series\":[{\"day\":\"2026-03-11\",\"dayOffset\":97,\"sys\":125,\"continuityPct\":100},{\"day\":\"2026-03-10\",\"dayOffset\":98,\"sys\":125,\"continuityPct\":100},{\"day\":\"2026-03-08\",\"dayOffset\":100,\"sys\":132,\"continuityPct\":100},{\"day\":\"2026-03-07\",\"dayOffset\":101,\"sys\":131,\"continuityPct\":0},{\"day\":\"2026-03-05\",\"dayOffset\":103,\"sys\":130,\"continuityPct\":0},{\"day\":\"2026-03-04\",\"dayOffset\":104,\"sys\":129,\"continuityPct\":0},{\"day\":\"2026-03-02\",\"dayOffset\":106,\"sys\":133,\"continuityPct\":0},{\"day\":\"2026-03-01\",\"dayOffset\":107,\"sys\":131,\"continuityPct\":0},{\"day\":\"2026-02-27\",\"dayOffset\":109,\"sys\":127,\"continuityPct\":0},{\"day\":\"2026-02-26\",\"dayOffset\":110,\"sys\":125,\"continuityPct\":0},{\"day\":\"2026-02-24\",\"dayOffset\":112,\"sys\":121,\"continuityPct\":0},{\"day\":\"2026-02-23\",\"dayOffset\":113,\"sys\":126,\"continuityPct\":0},{\"day\":\"2026-02-21\",\"dayOffset\":115,\"sys\":122,\"continuityPct\":0},{\"day\":\"2026-02-20\",\"dayOffset\":116,\"sys\":120,\"continuityPct\":0},{\"day\":\"2026-02-18\",\"dayOffset\":118,\"sys\":117,\"continuityPct\":0},{\"day\":\"2026-02-17\",\"dayOffset\":119,\"sys\":116,\"continuityPct\":0},{\"day\":\"2026-02-15\",\"dayOffset\":121,\"sys\":122,\"continuityPct\":0},{\"day\":\"2026-02-14\",\"dayOffset\":122,\"sys\":121,\"continuityPct\":0},{\"day\":\"2026-02-12\",\"dayOffset\":124,\"sys\":121,\"continuityPct\":0},{\"day\":\"2026-02-11\",\"dayOffset\":125,\"sys\":121,\"continuityPct\":0},{\"day\":\"2026-02-09\",\"dayOffset\":127,\"sys\":128,\"continuityPct\":0},{\"day\":\"2026-02-08\",\"dayOffset\":128,\"sys\":129,\"continuityPct\":0},{\"day\":\"2026-02-06\",\"dayOffset\":130,\"sys\":129,\"continuityPct\":0},{\"day\":\"2026-02-05\",\"dayOffset\":131,\"sys\":129,\"continuityPct\":0},{\"day\":\"2026-02-03\",\"dayOffset\":133,\"sys\":128,\"continuityPct\":0},{\"day\":\"2026-02-02\",\"dayOffset\":134,\"sys\":134,\"continuityPct\":0},{\"day\":\"2026-01-31\",\"dayOffset\":136,\"sys\":132,\"continuityPct\":0},{\"day\":\"2026-01-30\",\"dayOffset\":137,\"sys\":130,\"continuityPct\":0},{\"day\":\"2026-01-28\",\"dayOffset\":139,\"sys\":127,\"continuityPct\":0},{\"day\":\"2026-01-27\",\"dayOffset\":140,\"sys\":125,\"continuityPct\":0}]},\"bpMedications\":[{\"name\":\"Ramipril\",\"dose\":\"5mg\",\"schedulesPerDay\":1,\"compliance7\":50,\"compliance30\":76}],\"moodContext\":{\"points\":70,\"mean\":3.03,\"latest\":1,\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-15\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W15\",\"min\":2,\"max\":5,\"mean\":3.33,\"n\":6},{\"weekISO\":\"2026-W16\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":1,\"max\":5,\"mean\":3,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":1,\"max\":4,\"mean\":2.57,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":2,\"max\":5,\"mean\":3.5,\"n\":2}],\"monthly\":[],\"yearly\":[]},\"moodVsSystolicCorrelation\":null}}" + ] + } + ], + "temperature": 0.45, + "maxTokens": 250, + "seed": 1618033, + "responseFormat": "json", + "timeoutMs": 60000 + } + ], + "enqueued": [], + "result": { + "hasProvider": true, + "text": "Parity note for next-day-en.", + "cached": false, + "updatedAt": "2026-06-16T10:00:00.000Z" + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "blood-pressure", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "940846f9f59aeba3a96f517addf9d0af913b22b14ee8417d452e7cc8445520c7", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + } + ] + }, + "read-only-stale": { + "calls": [], + "enqueued": [ + { + "userId": "parity-rich", + "metric": "blood-pressure", + "locale": "en" + } + ], + "result": { + "hasProvider": true, + "text": "Parity note for next-day-en.", + "cached": true, + "updatedAt": "2026-06-16T10:00:00.000Z", + "preparing": false, + "revalidating": true + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "blood-pressure", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "940846f9f59aeba3a96f517addf9d0af913b22b14ee8417d452e7cc8445520c7", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + } + ] + }, + "prose-de": { + "calls": [ + { + "system": [ + "Du schreibst eine kurze, persönliche Einschätzung zu EINER Gesundheitsmetrik dieses Nutzers. Du kennst seine Daten und ordnest sie gegen seine EIGENE Baseline ein — nicht gegen Bevölkerungsnormen. Deine fachliche Grundlage sind anerkannte Leitlinien (ESH 2023, WHO, AASM, DGE, DEGAM); du beziehst dich darauf, ohne zu diagnostizieren.", + "", + "DEINE DATENGRUNDLAGE (graded snapshot):", + "- signal: der fertige Vergleich — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` ist das recent-Mittel; `baseline` ist der EIGENE längere Durchschnitt; `delta`/`deltaPct` sind die VORZEICHENBEHAFTETE Änderung gegenüber dieser Baseline; `spread` ist die normale Schwankung; `outsideNormalSwing` ist das vorab berechnete Urteil, ob die Änderung diese Schwankung übersteigt. Diese Werte sind bereits berechnet — nenne sie, rechne sie NICHT neu. `contributors[]` (nur Score-Komposite) benennt die 1–2 Teil-Scores, die den Score bewegt haben.", + "- summary: { points, start, end, delta, mean, min, max } — die Eckwerte des gesamten Lesefensters.", + "- series.recent[]: die jüngsten ~21 Tage, je ein Eintrag pro Tag ({ date, min, max, mean, n }).", + "- series.weekly[]: die ~10 Wochen davor, je ein ISO-Wochen-Aggregat.", + "- series.monthly[]: die ~12 Monate davor, je ein Monatsaggregat.", + "- series.yearly[]: alles ältere, je ein Jahr ({ year, mean, min, max, n, slope }).", + "- Die jüngsten Tage liegen also EINZELN vor, ältere Zeiträume als Wochen-, Monats- und Jahresaggregate. Ziehe die series nur für einen konkreten Tag heran; führe sonst mit dem signal-Block.", + "- Ein Slice kann leer sein, wenn die Historie kurz ist. Dann nicht so tun, als gäbe es ihn. Ist `signal.baseline` null, gibt es noch keine etablierte Baseline; sage das ehrlich, statt einen Vergleich zu erfinden.", + "", + "SO BAUST DU DIE EINSCHÄTZUNG — wähle die 2–3 BAUSTEINE, die zu DIESEN Daten passen, und ordne sie so, dass sie als EIN zusammenhängender Gedanke lesen, keine feste Checkliste. Führe gemäß dem OPENER-HINWEIS im User-Prompt, wenn einer mitgegeben ist — eröffne NICHT immer mit der Zahl. Variiere die Form von Metrik zu Metrik; eröffne nie zwei Metriken hintereinander gleich.", + "Verfügbare Bausteine (nimm die wenigen, die wirklich zutreffen):", + "(a) der aktuelle Befund — `current` aus dem signal-Block (oder ein Wert aus series.recent für eine Tagesfrage);", + "(b) gegen die EIGENE Baseline einordnen über das vorab berechnete vorzeichenbehaftete `delta` gegenüber `baselineLabel` — nenne es, leite es nicht neu her. Behandle `outsideNormalSwing: false` als \"innerhalb der üblichen Schwankung — kein Befund\" und ERFINDE KEINEN Trend; `outsideNormalSwing: true` ist die echte, meldenswerte Änderung. Ein `normalRange`/`placement` ist nur ein grober sekundärer Anker — das persönliche delta führt;", + "(c) einen echten, verdienten Erfolg oder eine stabile Serie benennen, wenn die Daten ihn zeigen;", + "(d) einen Punkt zum Im-Auge-behalten;", + "(e) einen Zusammenhang NUR, wenn ein r-Wert vorhanden und |r| > 0,4 ist, formuliert als \"bewegt sich mit\" / \"zeigt sich oft zusammen mit\", nie kausal;", + "(f) GENAU EINEN machbaren nächsten Schritt — und nur, wenn der Befund wirklich einen hergibt. Eine Botschaft = ein Verhalten, keine Liste. Ist der Wert stabil und im grünen Bereich, ERZWINGE KEINEN Schritt — bestätige kurz und nenne stattdessen einen Punkt zum Beobachten. Ein erfundener Schritt ist genau die Floskel, die wir vermeiden.", + "Synthese statt Aufzählung: die Geschichte dessen, was die Daten bedeuten, zählt mehr als das Wiederholen jeder Zahl.", + "", + "TONALITÄT — ein warmer, motivierender Premium-Begleiter (jemand hat sich die Daten dieser Person wirklich angesehen und etwas Wertvolles für sie gefunden):", + "- Zweite Person (\"dein Blutdruck\", \"deine Werte\"), warm, direkt, ehrlich.", + "- Motivierend und vorwärtsgewandt: Wenn die Daten es hergeben, benenne den echten Erfolg klar und baue ein wenig Schwung auf — die Person soll sich gesehen und unterstützt fühlen, nicht belehrt. Die Ermutigung muss durch die Zahlen VERDIENT sein, nie ein reflexhaftes Kompliment.", + "- Autonomie-unterstützend: \"kann helfen\", \"einen Versuch wert\", nie \"du musst\".", + "- Nie alarmierend, nie moralisierend, nie diagnostisch — keine Krankheitsbehauptung. Formuliere als begründete Beobachtung, nicht als ärztlichen Rat.", + "- Auch ungünstige Werte ehrlich benennen: Befund → gegen die eigene Baseline einordnen → ein kleiner machbarer Schritt, als Chance formuliert, nicht als Versagen. Nicht verharmlosen, nicht dramatisieren.", + "- Keine Floskeln und keine bloße Zahlenwiederholung: jede warme Zeile ist an eine echte Zahl oder eine echte Veränderung geknüpft. Ein generischer Positiv-Einstieg (\"Deine Werte sehen gut aus\") ist verboten — verdiene die Ermutigung mit dem konkreten Befund.", + "", + "ERÖFFNUNG — mit der Bedeutung führen, nicht mit dem Messwert", + "Beginne mit dem Eindruck in klaren Worten — was das heute über die Person sagt — und bring die Zahl direkt danach als Beleg, nie als Erstes. Eine Zeile, die mit einem Wert eröffnet (\"Dein Ruhepuls ist 61 …\"), liest sich wie ein Ablesen; dieselbe Zeile, die mit der Bedeutung eröffnet (\"Dein Herz läuft diese Woche etwas ruhiger — rund 61 …\"), liest sich wie ein Begleiter, der wirklich hingesehen hat. Jede Aussage bleibt an einer echten Zahl verankert; nur die REIHENFOLGE ändert sich, Bedeutung zuerst und die Zahl direkt dahinter. Das gilt, ob die Fläche eine Metrik abdeckt oder viele.", + "", + "LÄNGE: 2-4 Sätze, ca. 30-60 Wörter — ist der Wert aber stabil und nichts zu tun, ist EIN knapper Satz besser, als ihn auf Länge zu strecken. Knapp und hochwertig. Keine bloße Zahlenwiederholung, kein Fülltext, keine generischen Floskeln.", + "", + "DATENLAGE EHRLICH BEWERTEN (nie einen Trend erfinden):", + "- Nur wenige Messpunkte/Einträge/Ereignisse oder ein leerer recent-Slice: ehrlich sagen, dass es für eine belastbare Tendenz noch zu wenige Daten sind, und EINEN Hinweis geben (z.B. regelmäßiger erfassen). Keinen Trend behaupten. Was \"wenige\" bedeutet, hängt von der Metrik ab — bei Stimmung sind es Einträge, bei der Einnahmetreue geplante Dosen.", + "- Jüngste Messung deutlich älter als ~7 Tage (dataCoverage.newestMeasurementDaysAgo): darauf hinweisen, dass die Werte nicht mehr aktuell sind.", + "- Korrelationen NUR erwähnen, wenn der r-Wert im Snapshot vorhanden ist und |r| > 0.4 ist. Fehlt das Feld, keine Korrelation interpretieren oder erfinden. Immer als \"Zusammenhang\" formulieren, nie als \"Ursache\".", + "", + "VERBOTENE FLOSKELN (signalisieren ungegroundeten Fülltext — nie ausgeben, außer im Disclaimer)", + "\"achte auf ausreichend Schlaf\", \"trinke genug Wasser\", \"regelmäßige Bewegung\", \"ärztlicher Rat empfohlen\".", + "", + "METRIK-IDENTIFIER GEHÖREN NICHT IN DEN FLIESSTEXT", + "Schreibe niemals Datenbank- bzw. Enum-Namen wie \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" oder \"SLEEP_DURATION\" in nutzersichtbare Strings, und schreibe nie das wörtliche \"metric:\"-Chart-Token in den Fließtext. Verweise auf jede Metrik mit der natürlichsprachlichen Bezeichnung, die der Nutzer sieht — \"deine Systole\", \"dein Gewicht\", \"dein Puls\", \"deine Stimmung\", \"deine Medikamentenadhärenz\", \"dein Ruhepuls\", \"deine Schlafdauer\", \"deine Schritte\".", + "", + "GLP-1-DOSIS-SICHERHEIT (Vertrag, kein Stil)", + "Verschreibe, empfiehl oder ändere NIEMALS Medikamenten-Dosen, auch wenn der Snapshot einen GLP-1-Rezeptoragonisten benennt (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). Du darfst den benannten Wirkstoff und die aktuelle Titrationsstufe NENNEN (\"Woche 3 auf 7,5 mg\"), wenn der Snapshot sie trägt, aber schreibe nie \"erhöhe auf X mg\", \"erwäge die nächste Stufe Y mg\", \"bleibe auf Z mg\" oder eine Variante davon. Ein Plateau rahmt die nächste Entscheidung immer als Gespräch mit der behandelnden Ärztin. Bist du unsicher, ob etwas dosis-präskriptiv ist, behandle es so — und verweise an die Klinik.", + "", + "AKUTE WARNZEICHEN (Vertrag, kein Stil)", + "Beschreibt der Nutzer ein akutes Krisenzeichen — Brustschmerz oder Druck auf der Brust, Ohnmacht oder Beinahe-Ohnmacht (Synkope), ein plötzliches schweres Symptom (z. B. stärkster Kopfschmerz aller Zeiten, plötzliche Schwäche oder Sprachstörung, Atemnot), einen hypertensiven Notfallwert (systolisch ≥ 180 oder diastolisch ≥ 120 mit Symptomen), einen schwer-niedrigen oder sehr hohen Glukosewert (unter 54 mg/dL (3,0 mmol/L), oder unter 70 mg/dL (3,9 mmol/L) mit Symptomen, oder ≥ 250 mg/dL (13,9 mmol/L) mit Symptomen), anhaltendes Fieber ≥ 38,5 °C (101,3 °F) oder Gedanken an Suizid bzw. Selbstverletzung — sage in einem ruhigen Satz klar, dass das jetzt umgehende ärztliche Hilfe oder den Notruf braucht, und coache es NICHT anhand der Daten. Diagnostiziere nicht, benenne keine Erkrankung, schätze keinen Schweregrad aus den Zahlen — verweise nur auf umgehende Hilfe bzw. den Notruf und höre auf. Das ist eine geschlossene Liste; außerhalb davon bleibe wie gewohnt ruhig und nicht alarmierend.", + "", + "ERDUNG — NULL HALLUZINATIONEN", + "Jede Aussage muss auf einer Zahl beruhen, die im übergebenen Snapshot sichtbar ist. Lässt sich die Aussage keinem Snapshot-Feld zuordnen, lass sie weg und erfinde nie einen Messwert, um einer Anfrage zu entsprechen. Ordne jeden Wert gegen die EIGENE Baseline des Nutzers ein (jüngeres vs. längeres Mittel), nie gegen eine Bevölkerungsnorm. Nenne einen Zusammenhang nur, wenn der r-Wert vorhanden und |r| > 0,4 ist — immer als \"Zusammenhang\", nie als \"Ursache\".", + "", + "TONALITÄT — ein warmer, motivierender Begleiter, nie klinisch oder alarmierend", + "Schreibe in der zweiten Person, warm und direkt. Wenn die Daten es hergeben, benenne den echten Erfolg klar und baue ein wenig Schwung auf — die Person soll sich gesehen und unterstützt fühlen, nicht belehrt. Die Ermutigung muss durch die Zahlen VERDIENT sein, nie ein reflexhaftes Kompliment. Lob ist verankert, konkret und angemessen: an einer echten Zahl oder Veränderung festgemacht, nicht auf andere Nutzer übertragbar, und stiller Anerkennung für stille Erfolge. Über-Bestätigung ist eine Sicherheitsregression, keine Wärme — bestätige nie einen sich verschlechternden Trend oder eine unsichere Entscheidung; würdige die Mühe, nicht die Entscheidung, und bleib neutral (\"nichts zu tun — die gute Art von langweilig\"), wenn es nichts zu loben gibt, statt ein Kompliment zu erfinden. Sei autonomie-unterstützend (\"einen Versuch wert\", \"kann helfen\", nie \"du musst\"). Benenne auch ungünstige Werte ehrlich — Befund, dann gegen die eigene Baseline einordnen, dann ein kleiner machbarer Schritt, als Chance formuliert. Nie alarmierend, nie moralisierend, nie diagnostisch. Keine Floskeln und keine bloße Zahlenwiederholung. Beginne NICHT mit einem Kompliment über Datenmenge oder -qualität; verbotene Eröffnungen sind u.a. \"Datengrundlage ist sehr stark\", \"Your data foundation is strong\", \"Du hast eine solide Baseline\", \"Großartiger Datensatz\", ein generisches \"Deine Werte sehen gut aus\" und jede sinngemäße Umformulierung.", + "", + "KEINE EINSCHÄTZUNG — nie Mechanik nacherzählen", + "Eine Messanzahl (\"3-mal erfasst\"), ein Erfassungsrhythmus (\"alle paar Tage gemessen\") oder Bedien-Mechanik ist KEINE Einschätzung und gehört für sich genommen nicht in den Text. Nutze eine solche Angabe NUR, wenn sie eine Konsequenz trägt, und nenne dann die Konsequenz, nicht die Zahl: z. B. \"diese Woche zu wenige Werte für eine Tendenz — eine Morgenmessung würde es klären\", nicht \"du hast diese Woche zweimal gemessen\". Jeder Satz muss sich verdienen, indem er sagt, was ein Wert BEDEUTET oder was daraus folgt — nie, indem er erzählt, wie die Daten erhoben wurden.", + "", + "EINORDNEN, NICHT AUFZÄHLEN", + "Sag, was ein Wert BEDEUTET, nicht nur, was er ist. Trägt der User-Prompt einen EINORDNUNGS-KONTEXT-Block, nutze ihn: benenne, wo der Wert auf der Leitlinien-Skala liegt, was dieses Band in klaren, würdevollen Worten bedeutet und was praktisch folgt. Beurteile den Trend IM KONTEXT — ein \"leicht steigender\" Wert tief in einem günstigen Band ist eine Randnotiz; dieselbe Bewegung an oder nahe einer Grenze ist die Schlagzeile. Rahme jede Konsequenz OHNE Diagnose: \"Werte in diesem Bereich gelten laut Leitlinien als …\", nie \"du hast X\" oder \"du bist gefährdet für X\". Ist KEIN Einordnungs-Block vorhanden, hat die Metrik kein allgemeines Referenzband — ordne sie gegen die EIGENE Baseline der Person ein und sage klar, dass kein allgemeines Referenzband hinterlegt ist, statt eines zu erfinden.", + "", + "FORMATIERUNG — schreibe in kurzen Absätzen, getrennt durch eine LEERZEILE, je 1–3 Sätze; eine längere Antwort hat 2–4 Absätze, nie einen Block. Setze eine Leerzeile zwischen unterschiedliche Gedanken (z. B. \"wie es steht\" vs. \"eine Sache zum Ausprobieren\"). Ein stabiler Einzeiler bleibt EIN Absatz — strecke nicht auf einen zweiten. Wenn du wirklich drei oder mehr parallele Punkte aufzählst (Optionen, Befunde, Schritte), formatiere sie als schlichte Liste: ein Punkt pro Zeile, jede Zeile beginnt mit \"- \". Die EINE wichtigste Kernaussage darfst du mit **doppelten Sternchen** fett setzen, höchstens einmal pro Antwort. Sonst nichts: keine Überschriften, kein Kursiv, keine Backticks, keine nummerierten Listen, keine Emojis.", + "", + "BEISPIELE — beachte die UNTERSCHIEDLICHEN Formen (urteil-zuerst, trend-zuerst, Einzeiler). Sie illustrieren nur Form und Erdung; übernimm sie nie — jede Einschätzung nutzt die echten Snapshot-Zahlen:", + "- URTEIL-ZUERST (Bedeutung zuerst, Zahl als Beleg, ein Schritt): \"Dein Ruhepuls läuft gerade einen Tick niedriger als sonst — diese Woche rund 61 bpm, 5 unter deinem Monatsmittel von 66 und dein niedrigster seit Wochen. So ein Rückgang passt meist zu mehr Bewegung; die Routine, die das gebracht hat, lohnt sich beizubehalten.\"", + "- TREND-ZUERST (Richtung zuerst, dann der Stand): \"Dein systolischer Blutdruck geht seit drei Wochen ruhig nach unten, aktuell rund 124 mmHg — etwa 4 unter deinem 30-Tage-Schnitt. Nichts Dramatisches, einfach eine stetige Richtung.\"", + "- EINZEILER (stabil, KEIN erzwungener Schritt): \"Deine SpO₂ ist mit 97 % stabil und genau in deinem üblichen Bereich — nichts zu tun, die gute Art von langweilig.\"", + "- SCHLECHT (verbotene Floskel, ungegroundet — so NICHT): \"Deine Werte sehen gut aus. Achte auf ausreichend Schlaf und regelmäßige Bewegung.\"", + "", + "AUSGABEFORMAT: Antworte ausschließlich mit validem JSON in genau diesem Schema. Das Feld \"summary\" enthält die komplette Einschätzung auf Deutsch: 1-3 kurze Absätze mit je 1-3 Sätzen, getrennt durch eine Leerzeile als \\n\\n INNERHALB des JSON-Strings. Ein stabiler Einzeiler bleibt EIN Absatz:", + "{ \"summary\": \"...\" }", + "", + "METRIK — BLUTDRUCK:", + "- Der Snapshot trägt systolisch (bloodPressure.systolic) und diastolisch (bloodPressure.diastolic) jeweils mit signal (der fertige Vergleich) + summary + graded series; beurteile beide Komponenten GEMEINSAM, nie isoliert.", + "- Tendenz über Tage/Wochen: Führe mit bloodPressure.systolic.signal und bloodPressure.diastolic.signal — current, delta gegenüber baselineLabel und outsideNormalSwing sind bereits berechnet. NENNE sie, rechne sie NICHT neu. Behandle outsideNormalSwing: false als \"innerhalb der üblichen Schwankung — kein Befund\".", + "- Leitlinien-Anker (ESH 2023), zur Einordnung, nicht zur Diagnose:", + " * Optimal < 120/80, normal 120-129/80-84, hochnormal 130-139/85-89.", + " * Hypertonie Grad 1 ab 140/90, Grad 2 ab 160/100, Grad 3 ab 180/110.", + " * Übliches Behandlungsziel bei unkomplizierter Hypertonie < 130/80 (18-69 J.), < 140/80 (≥ 70 J.).", + "- targets: Falls bloodPressure.targets vorhanden, ordne gegen das persönliche Zielband ein; inTargetPctLast30DailyPoints ist der Anteil der letzten Tage im Ziel.", + "- Medikamenten-Bezug: bpMedications trägt compliance7/compliance30. bpMedicationContinuityVsSystolic.correlation und weightVsSystolic.correlation nur erwähnen, wenn vorhanden und |r| > 0.4 — als Zusammenhang, nie als Ursache.", + "- Stimmung: moodContext.moodVsSystolicCorrelation nur erwähnen, wenn vorhanden und |r| > 0.4.", + "- Eine Botschaft: Wenn die Werte über der Baseline liegen, schließe NUR DANN mit EINEM machbaren Schritt, wenn der Befund einen nahelegt (z.B. ein paar Tage zur selben Uhrzeit nachmessen, oder — bei lückenhafter Einnahmetreue — die Einnahme verlässlicher machen). Bei stabilen Werten im Ziel: das ehrlich anerkennen und stattdessen einen Punkt nennen, den man im Auge behalten kann, statt einen Schritt zu erzwingen." + ], + "messages": [ + { + "role": "user", + "content": [ + "Datum: 2026-06-15", + "OPENER HINT: Eröffne mit dem Gesamteindruck in klaren Worten und bring die Zahl danach als Beleg — nicht zahlen-zuerst.", + "Schreibe eine kurze Einschätzung zum Blutdruck dieser Person. Beginne mit der BEDEUTUNG in klaren Worten — dem Gesamteindruck, nicht der Zahl (z. B. \"ruhig und genau da, wo du ihn haben willst\", \"diese Woche einen Tick höher als sonst\") — und bring danach die systolischen/diastolischen Werte als Beleg, als EIN Paar beurteilt und gegen die eigene Wochen-/Monats-Baseline eingeordnet; führe nie mit den Werten. Schließe nur dann mit einem machbaren Schritt, wenn der Befund wirklich einen hergibt; ist nichts umsetzbar, lass den Schritt weg statt Fülltext zu erfinden. Konfidenz aus Messanzahl und Aktualität ableiten.", + "", + "VORHERIGE ANALYSE: keine vorhanden. Behandle dies als erste Analyse in diesem Bereich — kein Verbesserungs-/Verschlechterungs-Delta zu nennen.", + "", + "", + "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit dem aktuellsten Einzelwert und seiner Einordnung eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", + "", + "DATENLAGE: 94 Tages-Buckets im Fenster, letzter Wert heute.", + "", + "WIEDERHOLUNG: du hast für diese Metrik bereits 11-mal in Folge eine ähnliche \"keine wesentliche Änderung\"-Einschätzung gegeben. Wiederhole NICHT erneut dasselbe Niveau. Benenne die Kontinuität in einem kurzen Nebensatz (\"weiterhin stabil, 12 Checks in Folge\") und wende dich dann einem ANDEREN Aspekt zu — Konstanz, Tageszeit-Muster oder einer korrelierten Metrik — oder sage, falls nichts wirklich umsetzbar ist, dies klar und lass den erzwungenen Schritt weg.", + "", + "ZUSAMMENHÄNGE (statistisch geprüfte Assoziationen in DEINEN Daten, die diese Metrik betreffen — beschreibend, NIE kausal):", + "- Wenn der Blutzucker höher liegt, scheint der systolische Blutdruck am Folgetag nach bisheriger Datenlage eher höher auszufallen. Das ist ein Muster, das Beachtung verdient, keine Ursache. (n=23, r=0.91)", + "- Wenn der systolische Blutdruck höher liegt, scheint das Gewicht am Folgetag nach bisheriger Datenlage eher höher auszufallen. Das ist ein Muster, das Beachtung verdient, keine Ursache. (n=47, r=0.88)", + "Du DARFST höchstens EINEN davon einflechten, wenn er echten Mehrwert bringt — als \"Zusammenhang\" / \"geht tendenziell einher mit\" formuliert, nie als Ursache, und ohne die Zahlen zu verändern. Lass sie weg, wenn der eigene Befund der Metrik die stärkere Geschichte ist.", + "", + "", + "{\"locale\":\"de\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"blood_pressure\",\"dataCoverage\":{\"totalMeasurements\":258,\"totalSpanDays\":139,\"newestMeasurementDaysAgo\":0},\"bloodPressure\":{\"systolic\":{\"signal\":{\"metric\":\"deine Systole\",\"unit\":\"mmHg\",\"current\":127.29,\"currentWindowDays\":14,\"baseline\":126.97,\"baselineLabel\":\"your monthly average\",\"delta\":0.32,\"deltaPct\":0.3,\"spread\":2.38,\"outsideNormalSwing\":false,\"direction\":\"lower-better\",\"n\":14,\"newestDaysAgo\":0,\"normalRange\":{\"low\":120,\"high\":129},\"placement\":\"in band\"},\"summary\":{\"points\":94,\"start\":128,\"end\":125,\"delta\":-3,\"mean\":125.39,\"min\":116,\"max\":134},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":119,\"max\":119,\"mean\":119,\"n\":1},{\"date\":\"2026-05-28\",\"min\":121,\"max\":121,\"mean\":121,\"n\":1},{\"date\":\"2026-05-30\",\"min\":126,\"max\":126,\"mean\":126,\"n\":1},{\"date\":\"2026-05-31\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-02\",\"min\":125,\"max\":125,\"mean\":125,\"n\":1},{\"date\":\"2026-06-03\",\"min\":127,\"max\":127,\"mean\":127,\"n\":1},{\"date\":\"2026-06-05\",\"min\":130,\"max\":130,\"mean\":130,\"n\":1},{\"date\":\"2026-06-06\",\"min\":132,\"max\":132,\"mean\":132,\"n\":1},{\"date\":\"2026-06-08\",\"min\":134,\"max\":134,\"mean\":134,\"n\":1},{\"date\":\"2026-06-09\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-11\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-12\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-14\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-15\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":117,\"max\":121,\"mean\":118.75,\"n\":4},{\"weekISO\":\"2026-W13\",\"min\":119,\"max\":128,\"mean\":123.2,\"n\":5},{\"weekISO\":\"2026-W14\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"weekISO\":\"2026-W15\",\"min\":128,\"max\":134,\"mean\":129.4,\"n\":5},{\"weekISO\":\"2026-W16\",\"min\":121,\"max\":128,\"mean\":122.4,\"n\":5},{\"weekISO\":\"2026-W17\",\"min\":116,\"max\":122,\"mean\":118.75,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":121,\"max\":131,\"mean\":126,\"n\":5},{\"weekISO\":\"2026-W19\",\"min\":129,\"max\":133,\"mean\":131,\"n\":5},{\"weekISO\":\"2026-W20\",\"min\":124,\"max\":125,\"mean\":124.75,\"n\":4},{\"weekISO\":\"2026-W21\",\"min\":117,\"max\":124,\"mean\":119.8,\"n\":5},{\"weekISO\":\"2026-W22\",\"min\":123,\"max\":123,\"mean\":123,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"month\":\"2026-02\",\"min\":116,\"max\":134,\"mean\":124.22,\"n\":18},{\"month\":\"2026-03\",\"min\":124,\"max\":133,\"mean\":128.18,\"n\":11}],\"yearly\":[]}},\"diastolic\":{\"signal\":{\"metric\":\"deine Diastole\",\"unit\":\"mmHg\",\"current\":81.71,\"currentWindowDays\":14,\"baseline\":81.43,\"baselineLabel\":\"your monthly average\",\"delta\":0.29,\"deltaPct\":0.4,\"spread\":1.93,\"outsideNormalSwing\":false,\"direction\":\"lower-better\",\"n\":14,\"newestDaysAgo\":0,\"normalRange\":{\"low\":70,\"high\":79},\"placement\":\"above band\"},\"summary\":{\"points\":94,\"start\":82,\"end\":80,\"delta\":-2,\"mean\":80.31,\"min\":74,\"max\":86},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":76,\"max\":76,\"mean\":76,\"n\":1},{\"date\":\"2026-05-28\",\"min\":77,\"max\":77,\"mean\":77,\"n\":1},{\"date\":\"2026-05-30\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-05-31\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-02\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-03\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-05\",\"min\":86,\"max\":86,\"mean\":86,\"n\":1},{\"date\":\"2026-06-06\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-08\",\"min\":84,\"max\":84,\"mean\":84,\"n\":1},{\"date\":\"2026-06-09\",\"min\":85,\"max\":85,\"mean\":85,\"n\":1},{\"date\":\"2026-06-11\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-12\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-14\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-15\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":75,\"max\":79,\"mean\":77,\"n\":4},{\"weekISO\":\"2026-W13\",\"min\":74,\"max\":79,\"mean\":77.4,\"n\":5},{\"weekISO\":\"2026-W14\",\"min\":83,\"max\":85,\"mean\":83.75,\"n\":4},{\"weekISO\":\"2026-W15\",\"min\":79,\"max\":86,\"mean\":82.6,\"n\":5},{\"weekISO\":\"2026-W16\",\"min\":76,\"max\":80,\"mean\":78.2,\"n\":5},{\"weekISO\":\"2026-W17\",\"min\":74,\"max\":78,\"mean\":76.75,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":77,\"max\":84,\"mean\":80.6,\"n\":5},{\"weekISO\":\"2026-W19\",\"min\":82,\"max\":86,\"mean\":83.8,\"n\":5},{\"weekISO\":\"2026-W20\",\"min\":80,\"max\":81,\"mean\":80.5,\"n\":4},{\"weekISO\":\"2026-W21\",\"min\":74,\"max\":78,\"mean\":76.2,\"n\":5},{\"weekISO\":\"2026-W22\",\"min\":78,\"max\":78,\"mean\":78,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":80,\"max\":86,\"mean\":83,\"n\":4},{\"month\":\"2026-02\",\"min\":75,\"max\":85,\"mean\":79.28,\"n\":18},{\"month\":\"2026-03\",\"min\":78,\"max\":86,\"mean\":82,\"n\":11}],\"yearly\":[]}},\"paired\":{\"summary\":{\"points\":94,\"start\":105,\"end\":102.5,\"delta\":-2.5,\"mean\":102.85,\"min\":95.5,\"max\":110},\"series\":[{\"day\":\"2026-03-11\",\"sys\":125,\"dia\":82,\"inTarget\":false},{\"day\":\"2026-03-10\",\"sys\":125,\"dia\":82,\"inTarget\":false},{\"day\":\"2026-03-08\",\"sys\":132,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-03-07\",\"sys\":131,\"dia\":86,\"inTarget\":false},{\"day\":\"2026-03-05\",\"sys\":130,\"dia\":84,\"inTarget\":false},{\"day\":\"2026-03-04\",\"sys\":129,\"dia\":83,\"inTarget\":false},{\"day\":\"2026-03-02\",\"sys\":133,\"dia\":85,\"inTarget\":false},{\"day\":\"2026-03-01\",\"sys\":131,\"dia\":84,\"inTarget\":false},{\"day\":\"2026-02-27\",\"sys\":127,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-02-26\",\"sys\":125,\"dia\":79,\"inTarget\":true},{\"day\":\"2026-02-24\",\"sys\":121,\"dia\":80,\"inTarget\":false},{\"day\":\"2026-02-23\",\"sys\":126,\"dia\":78,\"inTarget\":true},{\"day\":\"2026-02-21\",\"sys\":122,\"dia\":75,\"inTarget\":true},{\"day\":\"2026-02-20\",\"sys\":120,\"dia\":79,\"inTarget\":true},{\"day\":\"2026-02-18\",\"sys\":117,\"dia\":76,\"inTarget\":true},{\"day\":\"2026-02-17\",\"sys\":116,\"dia\":75,\"inTarget\":true},{\"day\":\"2026-02-15\",\"sys\":122,\"dia\":78,\"inTarget\":true},{\"day\":\"2026-02-14\",\"sys\":121,\"dia\":78,\"inTarget\":true},{\"day\":\"2026-02-12\",\"sys\":121,\"dia\":77,\"inTarget\":true},{\"day\":\"2026-02-11\",\"sys\":121,\"dia\":77,\"inTarget\":true},{\"day\":\"2026-02-09\",\"sys\":128,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-02-08\",\"sys\":129,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-02-06\",\"sys\":129,\"dia\":80,\"inTarget\":false},{\"day\":\"2026-02-05\",\"sys\":129,\"dia\":85,\"inTarget\":false},{\"day\":\"2026-02-03\",\"sys\":128,\"dia\":84,\"inTarget\":false},{\"day\":\"2026-02-02\",\"sys\":134,\"dia\":83,\"inTarget\":false},{\"day\":\"2026-01-31\",\"sys\":132,\"dia\":86,\"inTarget\":false},{\"day\":\"2026-01-30\",\"sys\":130,\"dia\":85,\"inTarget\":false},{\"day\":\"2026-01-28\",\"sys\":127,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-01-27\",\"sys\":125,\"dia\":80,\"inTarget\":false}]},\"targets\":{\"systolic\":{\"min\":120,\"max\":129},\"diastolic\":{\"min\":70,\"max\":79},\"inTargetPctLast30DailyPoints\":42.6}},\"weightVsSystolic\":{\"correlation\":{\"r\":0.385,\"strength\":\"schwach\",\"n\":47},\"pairs\":[{\"day\":\"2026-04-24\",\"weight\":83.09,\"systolic\":120},{\"day\":\"2026-04-22\",\"weight\":83,\"systolic\":117},{\"day\":\"2026-04-18\",\"weight\":83,\"systolic\":121},{\"day\":\"2026-04-16\",\"weight\":83.09,\"systolic\":121},{\"day\":\"2026-04-12\",\"weight\":83.33,\"systolic\":128},{\"day\":\"2026-04-10\",\"weight\":83.45,\"systolic\":129},{\"day\":\"2026-04-06\",\"weight\":83.55,\"systolic\":134},{\"day\":\"2026-04-04\",\"weight\":83.51,\"systolic\":132},{\"day\":\"2026-03-31\",\"weight\":83.25,\"systolic\":125},{\"day\":\"2026-03-29\",\"weight\":83.07,\"systolic\":128},{\"day\":\"2026-03-25\",\"weight\":82.71,\"systolic\":119},{\"day\":\"2026-03-23\",\"weight\":82.58,\"systolic\":123},{\"day\":\"2026-03-19\",\"weight\":82.5,\"systolic\":118},{\"day\":\"2026-03-17\",\"weight\":82.55,\"systolic\":117},{\"day\":\"2026-03-13\",\"weight\":82.77,\"systolic\":125},{\"day\":\"2026-03-11\",\"weight\":82.9,\"systolic\":125},{\"day\":\"2026-03-07\",\"weight\":83.07,\"systolic\":131},{\"day\":\"2026-03-05\",\"weight\":83.07,\"systolic\":130},{\"day\":\"2026-03-01\",\"weight\":82.89,\"systolic\":131},{\"day\":\"2026-02-27\",\"weight\":82.73,\"systolic\":127},{\"day\":\"2026-02-23\",\"weight\":82.36,\"systolic\":126},{\"day\":\"2026-02-21\",\"weight\":82.2,\"systolic\":122},{\"day\":\"2026-02-17\",\"weight\":82.03,\"systolic\":116},{\"day\":\"2026-02-15\",\"weight\":82.04,\"systolic\":122},{\"day\":\"2026-02-11\",\"weight\":82.21,\"systolic\":121},{\"day\":\"2026-02-09\",\"weight\":82.34,\"systolic\":128},{\"day\":\"2026-02-05\",\"weight\":82.56,\"systolic\":129},{\"day\":\"2026-02-03\",\"weight\":82.6,\"systolic\":128},{\"day\":\"2026-01-30\",\"weight\":82.51,\"systolic\":130},{\"day\":\"2026-01-28\",\"weight\":82.38,\"systolic\":127}]},\"bpMedicationContinuityVsSystolic\":{\"expectedIntakesPerDay\":1,\"medicationCount\":1,\"correlation\":null,\"series\":[{\"day\":\"2026-03-11\",\"dayOffset\":96,\"sys\":125,\"continuityPct\":100},{\"day\":\"2026-03-10\",\"dayOffset\":97,\"sys\":125,\"continuityPct\":100},{\"day\":\"2026-03-08\",\"dayOffset\":99,\"sys\":132,\"continuityPct\":100},{\"day\":\"2026-03-07\",\"dayOffset\":100,\"sys\":131,\"continuityPct\":0},{\"day\":\"2026-03-05\",\"dayOffset\":102,\"sys\":130,\"continuityPct\":0},{\"day\":\"2026-03-04\",\"dayOffset\":103,\"sys\":129,\"continuityPct\":0},{\"day\":\"2026-03-02\",\"dayOffset\":105,\"sys\":133,\"continuityPct\":0},{\"day\":\"2026-03-01\",\"dayOffset\":106,\"sys\":131,\"continuityPct\":0},{\"day\":\"2026-02-27\",\"dayOffset\":108,\"sys\":127,\"continuityPct\":0},{\"day\":\"2026-02-26\",\"dayOffset\":109,\"sys\":125,\"continuityPct\":0},{\"day\":\"2026-02-24\",\"dayOffset\":111,\"sys\":121,\"continuityPct\":0},{\"day\":\"2026-02-23\",\"dayOffset\":112,\"sys\":126,\"continuityPct\":0},{\"day\":\"2026-02-21\",\"dayOffset\":114,\"sys\":122,\"continuityPct\":0},{\"day\":\"2026-02-20\",\"dayOffset\":115,\"sys\":120,\"continuityPct\":0},{\"day\":\"2026-02-18\",\"dayOffset\":117,\"sys\":117,\"continuityPct\":0},{\"day\":\"2026-02-17\",\"dayOffset\":118,\"sys\":116,\"continuityPct\":0},{\"day\":\"2026-02-15\",\"dayOffset\":120,\"sys\":122,\"continuityPct\":0},{\"day\":\"2026-02-14\",\"dayOffset\":121,\"sys\":121,\"continuityPct\":0},{\"day\":\"2026-02-12\",\"dayOffset\":123,\"sys\":121,\"continuityPct\":0},{\"day\":\"2026-02-11\",\"dayOffset\":124,\"sys\":121,\"continuityPct\":0},{\"day\":\"2026-02-09\",\"dayOffset\":126,\"sys\":128,\"continuityPct\":0},{\"day\":\"2026-02-08\",\"dayOffset\":127,\"sys\":129,\"continuityPct\":0},{\"day\":\"2026-02-06\",\"dayOffset\":129,\"sys\":129,\"continuityPct\":0},{\"day\":\"2026-02-05\",\"dayOffset\":130,\"sys\":129,\"continuityPct\":0},{\"day\":\"2026-02-03\",\"dayOffset\":132,\"sys\":128,\"continuityPct\":0},{\"day\":\"2026-02-02\",\"dayOffset\":133,\"sys\":134,\"continuityPct\":0},{\"day\":\"2026-01-31\",\"dayOffset\":135,\"sys\":132,\"continuityPct\":0},{\"day\":\"2026-01-30\",\"dayOffset\":136,\"sys\":130,\"continuityPct\":0},{\"day\":\"2026-01-28\",\"dayOffset\":138,\"sys\":127,\"continuityPct\":0},{\"day\":\"2026-01-27\",\"dayOffset\":139,\"sys\":125,\"continuityPct\":0}]},\"bpMedications\":[{\"name\":\"Ramipril\",\"dose\":\"5mg\",\"schedulesPerDay\":1,\"compliance7\":67,\"compliance30\":79}],\"moodContext\":{\"points\":70,\"mean\":3.03,\"latest\":1,\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-15\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W15\",\"min\":2,\"max\":5,\"mean\":3.33,\"n\":6},{\"weekISO\":\"2026-W16\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":1,\"max\":5,\"mean\":3,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":1,\"max\":4,\"mean\":2.57,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1}],\"monthly\":[],\"yearly\":[]},\"moodVsSystolicCorrelation\":null}}" + ] + } + ], + "temperature": 0.45, + "maxTokens": 250, + "seed": 1618033, + "responseFormat": "json", + "timeoutMs": 60000 + } + ], + "enqueued": [], + "result": { + "hasProvider": true, + "text": "Das ist kein JSON.", + "cached": false, + "updatedAt": "2026-06-15T10:00:00.000Z" + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "blood-pressure", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "42fa0a3529e963e60122eb23f56d6c9fb88ec4598e781c0bd0c8c6ed4e7b4c60", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "blood-pressure", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "940846f9f59aeba3a96f517addf9d0af913b22b14ee8417d452e7cc8445520c7", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + } + ] + }, + "unparseable-fr": { + "calls": [ + { + "system": [ + "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", + "", + "YOUR DATA (graded snapshot):", + "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", + "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", + "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", + "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", + "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", + "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", + "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", + "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", + "", + "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", + "Moves available (choose the few that genuinely apply):", + "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", + "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", + "(c) name a genuine, earned win or a steady streak when the data shows one;", + "(d) one watch-item worth keeping an eye on;", + "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", + "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", + "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", + "", + "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", + "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", + "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", + "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", + "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", + "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", + "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", + "", + "OPENING — lead with meaning, not the metric value", + "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", + "", + "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", + "", + "JUDGE THE DATA HONESTLY (never invent a trend):", + "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", + "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", + "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", + "", + "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", + "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", + "", + "METRIC IDENTIFIERS STAY OUT OF PROSE", + "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", + "", + "GLP-1 DOSE SAFETY (contract, not style)", + "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", + "", + "ACUTE RED FLAGS (contract, not style)", + "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", + "", + "GROUNDING — ZERO HALLUCINATIONS", + "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", + "", + "TONE — a warm, motivating advisor, never clinical or alarming", + "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", + "", + "NOT AN INSIGHT — never recite mechanics", + "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", + "", + "INTERPRET, DON'T ENUMERATE", + "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", + "", + "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", + "", + "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", + "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", + "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", + "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", + "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", + "", + "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in French: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", + "{ \"summary\": \"...\" }", + "", + "METRIC — BLOOD PRESSURE:", + "- The snapshot carries systolic (bloodPressure.systolic) and diastolic (bloodPressure.diastolic), each with a signal (the finished comparison) + summary + graded series; judge both components TOGETHER, never in isolation.", + "- Trend over days/weeks: lead from bloodPressure.systolic.signal and bloodPressure.diastolic.signal — current, delta vs baselineLabel and outsideNormalSwing are already computed. STATE them, do NOT recompute. Treat outsideNormalSwing: false as \"inside the usual swing — not a finding\".", + "- Guideline anchors (ESH 2023), for placement, not diagnosis:", + " * Optimal < 120/80, normal 120-129/80-84, high-normal 130-139/85-89.", + " * Hypertension grade 1 from 140/90, grade 2 from 160/100, grade 3 from 180/110.", + " * Common treatment target for uncomplicated hypertension < 130/80 (age 18-69), < 140/80 (age ≥ 70).", + "- targets: if bloodPressure.targets is present, place values against the personal target band; inTargetPctLast30DailyPoints is the share of recent days in target.", + "- Medication link: bpMedications carries compliance7/compliance30. Mention bpMedicationContinuityVsSystolic.correlation and weightVsSystolic.correlation only when present and |r| > 0.4 — as an association, never a cause.", + "- Mood: mention moodContext.moodVsSystolicCorrelation only when present and |r| > 0.4.", + "- One message: if the values sit above the person's baseline, close with ONE doable step ONLY when the finding implies one (e.g. take a few readings at the same time of day for a few days, or — when adherence is patchy — make the medication routine more reliable). When values are stable and in target, say so honestly and name one thing worth keeping an eye on instead of manufacturing a step.", + "", + "OUTPUT LANGUAGE: Répondez en français. Utilisez le vouvoiement par défaut ; tutoyez si l'utilisateur tutoie en premier. Utilisez un vocabulaire de santé français naturel." + ], + "messages": [ + { + "role": "user", + "content": [ + "Date: 2026-06-15", + "OPENER HINT: Open with the overall read in plain words, then bring in the number as support — not number-first.", + "Write one short assessment of this person's blood pressure. Open with what the reading MEANS in plain words — the overall read, not the number (e.g. \"calm and right where you want it\", \"running a touch higher than your usual this week\") — then bring in the systolic/diastolic figures right after as support, judged as ONE pair and placed against their own weekly/monthly baseline; never lead with the numbers. Close with one doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the measurement count and recency.", + "", + "PREVIOUS ANALYSIS: none on file. Treat this as the user's first analysis for this domain — no improvement/regression delta to surface.", + "", + "", + "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit dem aktuellsten Einzelwert und seiner Einordnung eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", + "", + "DATENLAGE: 94 Tages-Buckets im Fenster, letzter Wert heute.", + "", + "WIEDERHOLUNG: du hast für diese Metrik bereits 11-mal in Folge eine ähnliche \"keine wesentliche Änderung\"-Einschätzung gegeben. Wiederhole NICHT erneut dasselbe Niveau. Benenne die Kontinuität in einem kurzen Nebensatz (\"weiterhin stabil, 12 Checks in Folge\") und wende dich dann einem ANDEREN Aspekt zu — Konstanz, Tageszeit-Muster oder einer korrelierten Metrik — oder sage, falls nichts wirklich umsetzbar ist, dies klar und lass den erzwungenen Schritt weg.", + "", + "ZUSAMMENHÄNGE (statistisch geprüfte Assoziationen in DEINEN Daten, die diese Metrik betreffen — beschreibend, NIE kausal):", + "- Quand la glycémie monte, la pression systolique du lendemain semble avoir tendance à augmenter, au vu des données actuelles. C'est une tendance à surveiller, pas une cause. (n=23, r=0.91)", + "- Quand la pression systolique monte, le poids du lendemain semble avoir tendance à augmenter, au vu des données actuelles. C'est une tendance à surveiller, pas une cause. (n=47, r=0.88)", + "Du DARFST höchstens EINEN davon einflechten, wenn er echten Mehrwert bringt — als \"Zusammenhang\" / \"geht tendenziell einher mit\" formuliert, nie als Ursache, und ohne die Zahlen zu verändern. Lass sie weg, wenn der eigene Befund der Metrik die stärkere Geschichte ist.", + "", + "", + "{\"locale\":\"fr\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"blood_pressure\",\"dataCoverage\":{\"totalMeasurements\":258,\"totalSpanDays\":139,\"newestMeasurementDaysAgo\":0},\"bloodPressure\":{\"systolic\":{\"signal\":{\"metric\":\"deine Systole\",\"unit\":\"mmHg\",\"current\":127.29,\"currentWindowDays\":14,\"baseline\":126.97,\"baselineLabel\":\"your monthly average\",\"delta\":0.32,\"deltaPct\":0.3,\"spread\":2.38,\"outsideNormalSwing\":false,\"direction\":\"lower-better\",\"n\":14,\"newestDaysAgo\":0,\"normalRange\":{\"low\":120,\"high\":129},\"placement\":\"in band\"},\"summary\":{\"points\":94,\"start\":128,\"end\":125,\"delta\":-3,\"mean\":125.39,\"min\":116,\"max\":134},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":119,\"max\":119,\"mean\":119,\"n\":1},{\"date\":\"2026-05-28\",\"min\":121,\"max\":121,\"mean\":121,\"n\":1},{\"date\":\"2026-05-30\",\"min\":126,\"max\":126,\"mean\":126,\"n\":1},{\"date\":\"2026-05-31\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-02\",\"min\":125,\"max\":125,\"mean\":125,\"n\":1},{\"date\":\"2026-06-03\",\"min\":127,\"max\":127,\"mean\":127,\"n\":1},{\"date\":\"2026-06-05\",\"min\":130,\"max\":130,\"mean\":130,\"n\":1},{\"date\":\"2026-06-06\",\"min\":132,\"max\":132,\"mean\":132,\"n\":1},{\"date\":\"2026-06-08\",\"min\":134,\"max\":134,\"mean\":134,\"n\":1},{\"date\":\"2026-06-09\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-11\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-12\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-14\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-15\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":117,\"max\":121,\"mean\":118.75,\"n\":4},{\"weekISO\":\"2026-W13\",\"min\":119,\"max\":128,\"mean\":123.2,\"n\":5},{\"weekISO\":\"2026-W14\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"weekISO\":\"2026-W15\",\"min\":128,\"max\":134,\"mean\":129.4,\"n\":5},{\"weekISO\":\"2026-W16\",\"min\":121,\"max\":128,\"mean\":122.4,\"n\":5},{\"weekISO\":\"2026-W17\",\"min\":116,\"max\":122,\"mean\":118.75,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":121,\"max\":131,\"mean\":126,\"n\":5},{\"weekISO\":\"2026-W19\",\"min\":129,\"max\":133,\"mean\":131,\"n\":5},{\"weekISO\":\"2026-W20\",\"min\":124,\"max\":125,\"mean\":124.75,\"n\":4},{\"weekISO\":\"2026-W21\",\"min\":117,\"max\":124,\"mean\":119.8,\"n\":5},{\"weekISO\":\"2026-W22\",\"min\":123,\"max\":123,\"mean\":123,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"month\":\"2026-02\",\"min\":116,\"max\":134,\"mean\":124.22,\"n\":18},{\"month\":\"2026-03\",\"min\":124,\"max\":133,\"mean\":128.18,\"n\":11}],\"yearly\":[]}},\"diastolic\":{\"signal\":{\"metric\":\"deine Diastole\",\"unit\":\"mmHg\",\"current\":81.71,\"currentWindowDays\":14,\"baseline\":81.43,\"baselineLabel\":\"your monthly average\",\"delta\":0.29,\"deltaPct\":0.4,\"spread\":1.93,\"outsideNormalSwing\":false,\"direction\":\"lower-better\",\"n\":14,\"newestDaysAgo\":0,\"normalRange\":{\"low\":70,\"high\":79},\"placement\":\"above band\"},\"summary\":{\"points\":94,\"start\":82,\"end\":80,\"delta\":-2,\"mean\":80.31,\"min\":74,\"max\":86},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":76,\"max\":76,\"mean\":76,\"n\":1},{\"date\":\"2026-05-28\",\"min\":77,\"max\":77,\"mean\":77,\"n\":1},{\"date\":\"2026-05-30\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-05-31\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-02\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-03\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-05\",\"min\":86,\"max\":86,\"mean\":86,\"n\":1},{\"date\":\"2026-06-06\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-08\",\"min\":84,\"max\":84,\"mean\":84,\"n\":1},{\"date\":\"2026-06-09\",\"min\":85,\"max\":85,\"mean\":85,\"n\":1},{\"date\":\"2026-06-11\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-12\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-14\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-15\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":75,\"max\":79,\"mean\":77,\"n\":4},{\"weekISO\":\"2026-W13\",\"min\":74,\"max\":79,\"mean\":77.4,\"n\":5},{\"weekISO\":\"2026-W14\",\"min\":83,\"max\":85,\"mean\":83.75,\"n\":4},{\"weekISO\":\"2026-W15\",\"min\":79,\"max\":86,\"mean\":82.6,\"n\":5},{\"weekISO\":\"2026-W16\",\"min\":76,\"max\":80,\"mean\":78.2,\"n\":5},{\"weekISO\":\"2026-W17\",\"min\":74,\"max\":78,\"mean\":76.75,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":77,\"max\":84,\"mean\":80.6,\"n\":5},{\"weekISO\":\"2026-W19\",\"min\":82,\"max\":86,\"mean\":83.8,\"n\":5},{\"weekISO\":\"2026-W20\",\"min\":80,\"max\":81,\"mean\":80.5,\"n\":4},{\"weekISO\":\"2026-W21\",\"min\":74,\"max\":78,\"mean\":76.2,\"n\":5},{\"weekISO\":\"2026-W22\",\"min\":78,\"max\":78,\"mean\":78,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":80,\"max\":86,\"mean\":83,\"n\":4},{\"month\":\"2026-02\",\"min\":75,\"max\":85,\"mean\":79.28,\"n\":18},{\"month\":\"2026-03\",\"min\":78,\"max\":86,\"mean\":82,\"n\":11}],\"yearly\":[]}},\"paired\":{\"summary\":{\"points\":94,\"start\":105,\"end\":102.5,\"delta\":-2.5,\"mean\":102.85,\"min\":95.5,\"max\":110},\"series\":[{\"day\":\"2026-03-11\",\"sys\":125,\"dia\":82,\"inTarget\":false},{\"day\":\"2026-03-10\",\"sys\":125,\"dia\":82,\"inTarget\":false},{\"day\":\"2026-03-08\",\"sys\":132,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-03-07\",\"sys\":131,\"dia\":86,\"inTarget\":false},{\"day\":\"2026-03-05\",\"sys\":130,\"dia\":84,\"inTarget\":false},{\"day\":\"2026-03-04\",\"sys\":129,\"dia\":83,\"inTarget\":false},{\"day\":\"2026-03-02\",\"sys\":133,\"dia\":85,\"inTarget\":false},{\"day\":\"2026-03-01\",\"sys\":131,\"dia\":84,\"inTarget\":false},{\"day\":\"2026-02-27\",\"sys\":127,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-02-26\",\"sys\":125,\"dia\":79,\"inTarget\":true},{\"day\":\"2026-02-24\",\"sys\":121,\"dia\":80,\"inTarget\":false},{\"day\":\"2026-02-23\",\"sys\":126,\"dia\":78,\"inTarget\":true},{\"day\":\"2026-02-21\",\"sys\":122,\"dia\":75,\"inTarget\":true},{\"day\":\"2026-02-20\",\"sys\":120,\"dia\":79,\"inTarget\":true},{\"day\":\"2026-02-18\",\"sys\":117,\"dia\":76,\"inTarget\":true},{\"day\":\"2026-02-17\",\"sys\":116,\"dia\":75,\"inTarget\":true},{\"day\":\"2026-02-15\",\"sys\":122,\"dia\":78,\"inTarget\":true},{\"day\":\"2026-02-14\",\"sys\":121,\"dia\":78,\"inTarget\":true},{\"day\":\"2026-02-12\",\"sys\":121,\"dia\":77,\"inTarget\":true},{\"day\":\"2026-02-11\",\"sys\":121,\"dia\":77,\"inTarget\":true},{\"day\":\"2026-02-09\",\"sys\":128,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-02-08\",\"sys\":129,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-02-06\",\"sys\":129,\"dia\":80,\"inTarget\":false},{\"day\":\"2026-02-05\",\"sys\":129,\"dia\":85,\"inTarget\":false},{\"day\":\"2026-02-03\",\"sys\":128,\"dia\":84,\"inTarget\":false},{\"day\":\"2026-02-02\",\"sys\":134,\"dia\":83,\"inTarget\":false},{\"day\":\"2026-01-31\",\"sys\":132,\"dia\":86,\"inTarget\":false},{\"day\":\"2026-01-30\",\"sys\":130,\"dia\":85,\"inTarget\":false},{\"day\":\"2026-01-28\",\"sys\":127,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-01-27\",\"sys\":125,\"dia\":80,\"inTarget\":false}]},\"targets\":{\"systolic\":{\"min\":120,\"max\":129},\"diastolic\":{\"min\":70,\"max\":79},\"inTargetPctLast30DailyPoints\":42.6}},\"weightVsSystolic\":{\"correlation\":{\"r\":0.385,\"strength\":\"schwach\",\"n\":47},\"pairs\":[{\"day\":\"2026-04-24\",\"weight\":83.09,\"systolic\":120},{\"day\":\"2026-04-22\",\"weight\":83,\"systolic\":117},{\"day\":\"2026-04-18\",\"weight\":83,\"systolic\":121},{\"day\":\"2026-04-16\",\"weight\":83.09,\"systolic\":121},{\"day\":\"2026-04-12\",\"weight\":83.33,\"systolic\":128},{\"day\":\"2026-04-10\",\"weight\":83.45,\"systolic\":129},{\"day\":\"2026-04-06\",\"weight\":83.55,\"systolic\":134},{\"day\":\"2026-04-04\",\"weight\":83.51,\"systolic\":132},{\"day\":\"2026-03-31\",\"weight\":83.25,\"systolic\":125},{\"day\":\"2026-03-29\",\"weight\":83.07,\"systolic\":128},{\"day\":\"2026-03-25\",\"weight\":82.71,\"systolic\":119},{\"day\":\"2026-03-23\",\"weight\":82.58,\"systolic\":123},{\"day\":\"2026-03-19\",\"weight\":82.5,\"systolic\":118},{\"day\":\"2026-03-17\",\"weight\":82.55,\"systolic\":117},{\"day\":\"2026-03-13\",\"weight\":82.77,\"systolic\":125},{\"day\":\"2026-03-11\",\"weight\":82.9,\"systolic\":125},{\"day\":\"2026-03-07\",\"weight\":83.07,\"systolic\":131},{\"day\":\"2026-03-05\",\"weight\":83.07,\"systolic\":130},{\"day\":\"2026-03-01\",\"weight\":82.89,\"systolic\":131},{\"day\":\"2026-02-27\",\"weight\":82.73,\"systolic\":127},{\"day\":\"2026-02-23\",\"weight\":82.36,\"systolic\":126},{\"day\":\"2026-02-21\",\"weight\":82.2,\"systolic\":122},{\"day\":\"2026-02-17\",\"weight\":82.03,\"systolic\":116},{\"day\":\"2026-02-15\",\"weight\":82.04,\"systolic\":122},{\"day\":\"2026-02-11\",\"weight\":82.21,\"systolic\":121},{\"day\":\"2026-02-09\",\"weight\":82.34,\"systolic\":128},{\"day\":\"2026-02-05\",\"weight\":82.56,\"systolic\":129},{\"day\":\"2026-02-03\",\"weight\":82.6,\"systolic\":128},{\"day\":\"2026-01-30\",\"weight\":82.51,\"systolic\":130},{\"day\":\"2026-01-28\",\"weight\":82.38,\"systolic\":127}]},\"bpMedicationContinuityVsSystolic\":{\"expectedIntakesPerDay\":1,\"medicationCount\":1,\"correlation\":null,\"series\":[{\"day\":\"2026-03-11\",\"dayOffset\":96,\"sys\":125,\"continuityPct\":100},{\"day\":\"2026-03-10\",\"dayOffset\":97,\"sys\":125,\"continuityPct\":100},{\"day\":\"2026-03-08\",\"dayOffset\":99,\"sys\":132,\"continuityPct\":100},{\"day\":\"2026-03-07\",\"dayOffset\":100,\"sys\":131,\"continuityPct\":0},{\"day\":\"2026-03-05\",\"dayOffset\":102,\"sys\":130,\"continuityPct\":0},{\"day\":\"2026-03-04\",\"dayOffset\":103,\"sys\":129,\"continuityPct\":0},{\"day\":\"2026-03-02\",\"dayOffset\":105,\"sys\":133,\"continuityPct\":0},{\"day\":\"2026-03-01\",\"dayOffset\":106,\"sys\":131,\"continuityPct\":0},{\"day\":\"2026-02-27\",\"dayOffset\":108,\"sys\":127,\"continuityPct\":0},{\"day\":\"2026-02-26\",\"dayOffset\":109,\"sys\":125,\"continuityPct\":0},{\"day\":\"2026-02-24\",\"dayOffset\":111,\"sys\":121,\"continuityPct\":0},{\"day\":\"2026-02-23\",\"dayOffset\":112,\"sys\":126,\"continuityPct\":0},{\"day\":\"2026-02-21\",\"dayOffset\":114,\"sys\":122,\"continuityPct\":0},{\"day\":\"2026-02-20\",\"dayOffset\":115,\"sys\":120,\"continuityPct\":0},{\"day\":\"2026-02-18\",\"dayOffset\":117,\"sys\":117,\"continuityPct\":0},{\"day\":\"2026-02-17\",\"dayOffset\":118,\"sys\":116,\"continuityPct\":0},{\"day\":\"2026-02-15\",\"dayOffset\":120,\"sys\":122,\"continuityPct\":0},{\"day\":\"2026-02-14\",\"dayOffset\":121,\"sys\":121,\"continuityPct\":0},{\"day\":\"2026-02-12\",\"dayOffset\":123,\"sys\":121,\"continuityPct\":0},{\"day\":\"2026-02-11\",\"dayOffset\":124,\"sys\":121,\"continuityPct\":0},{\"day\":\"2026-02-09\",\"dayOffset\":126,\"sys\":128,\"continuityPct\":0},{\"day\":\"2026-02-08\",\"dayOffset\":127,\"sys\":129,\"continuityPct\":0},{\"day\":\"2026-02-06\",\"dayOffset\":129,\"sys\":129,\"continuityPct\":0},{\"day\":\"2026-02-05\",\"dayOffset\":130,\"sys\":129,\"continuityPct\":0},{\"day\":\"2026-02-03\",\"dayOffset\":132,\"sys\":128,\"continuityPct\":0},{\"day\":\"2026-02-02\",\"dayOffset\":133,\"sys\":134,\"continuityPct\":0},{\"day\":\"2026-01-31\",\"dayOffset\":135,\"sys\":132,\"continuityPct\":0},{\"day\":\"2026-01-30\",\"dayOffset\":136,\"sys\":130,\"continuityPct\":0},{\"day\":\"2026-01-28\",\"dayOffset\":138,\"sys\":127,\"continuityPct\":0},{\"day\":\"2026-01-27\",\"dayOffset\":139,\"sys\":125,\"continuityPct\":0}]},\"bpMedications\":[{\"name\":\"Ramipril\",\"dose\":\"5mg\",\"schedulesPerDay\":1,\"compliance7\":67,\"compliance30\":79}],\"moodContext\":{\"points\":70,\"mean\":3.03,\"latest\":1,\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-15\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W15\",\"min\":2,\"max\":5,\"mean\":3.33,\"n\":6},{\"weekISO\":\"2026-W16\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":1,\"max\":5,\"mean\":3,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":1,\"max\":4,\"mean\":2.57,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1}],\"monthly\":[],\"yearly\":[]},\"moodVsSystolicCorrelation\":null}}" + ] + } + ], + "temperature": 0.45, + "maxTokens": 250, + "seed": 1618033, + "responseFormat": "json", + "timeoutMs": 60000 + } + ], + "enqueued": [], + "result": { + "hasProvider": false, + "text": "Stable, comme d’habitude. Ta tension artérielle est à 127 en ce moment — au-dessus de ta moyenne habituelle de 127. C’est dans tes repères habituels — rien qui appelle une réaction maintenant.", + "cached": true, + "updatedAt": null + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "blood-pressure", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "42fa0a3529e963e60122eb23f56d6c9fb88ec4598e781c0bd0c8c6ed4e7b4c60", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "blood-pressure", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "940846f9f59aeba3a96f517addf9d0af913b22b14ee8417d452e7cc8445520c7", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "blood-pressure", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + } + ] + }, + "blocked-es": { + "calls": [ + { + "system": [ + "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", + "", + "YOUR DATA (graded snapshot):", + "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", + "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", + "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", + "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", + "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", + "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", + "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", + "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", + "", + "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", + "Moves available (choose the few that genuinely apply):", + "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", + "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", + "(c) name a genuine, earned win or a steady streak when the data shows one;", + "(d) one watch-item worth keeping an eye on;", + "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", + "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", + "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", + "", + "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", + "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", + "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", + "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", + "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", + "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", + "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", + "", + "OPENING — lead with meaning, not the metric value", + "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", + "", + "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", + "", + "JUDGE THE DATA HONESTLY (never invent a trend):", + "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", + "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", + "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", + "", + "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", + "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", + "", + "METRIC IDENTIFIERS STAY OUT OF PROSE", + "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", + "", + "GLP-1 DOSE SAFETY (contract, not style)", + "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", + "", + "ACUTE RED FLAGS (contract, not style)", + "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", + "", + "GROUNDING — ZERO HALLUCINATIONS", + "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", + "", + "TONE — a warm, motivating advisor, never clinical or alarming", + "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", + "", + "NOT AN INSIGHT — never recite mechanics", + "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", + "", + "INTERPRET, DON'T ENUMERATE", + "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", + "", + "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", + "", + "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", + "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", + "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", + "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", + "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", + "", + "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in Spanish: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", + "{ \"summary\": \"...\" }", + "", + "METRIC — BLOOD PRESSURE:", + "- The snapshot carries systolic (bloodPressure.systolic) and diastolic (bloodPressure.diastolic), each with a signal (the finished comparison) + summary + graded series; judge both components TOGETHER, never in isolation.", + "- Trend over days/weeks: lead from bloodPressure.systolic.signal and bloodPressure.diastolic.signal — current, delta vs baselineLabel and outsideNormalSwing are already computed. STATE them, do NOT recompute. Treat outsideNormalSwing: false as \"inside the usual swing — not a finding\".", + "- Guideline anchors (ESH 2023), for placement, not diagnosis:", + " * Optimal < 120/80, normal 120-129/80-84, high-normal 130-139/85-89.", + " * Hypertension grade 1 from 140/90, grade 2 from 160/100, grade 3 from 180/110.", + " * Common treatment target for uncomplicated hypertension < 130/80 (age 18-69), < 140/80 (age ≥ 70).", + "- targets: if bloodPressure.targets is present, place values against the personal target band; inTargetPctLast30DailyPoints is the share of recent days in target.", + "- Medication link: bpMedications carries compliance7/compliance30. Mention bpMedicationContinuityVsSystolic.correlation and weightVsSystolic.correlation only when present and |r| > 0.4 — as an association, never a cause.", + "- Mood: mention moodContext.moodVsSystolicCorrelation only when present and |r| > 0.4.", + "- One message: if the values sit above the person's baseline, close with ONE doable step ONLY when the finding implies one (e.g. take a few readings at the same time of day for a few days, or — when adherence is patchy — make the medication routine more reliable). When values are stable and in target, say so honestly and name one thing worth keeping an eye on instead of manufacturing a step.", + "", + "OUTPUT LANGUAGE: Responda en español (registro peninsular formal — usted). Use un vocabulario sanitario en español natural." + ], + "messages": [ + { + "role": "user", + "content": [ + "Date: 2026-06-15", + "OPENER HINT: Open with the overall read in plain words, then bring in the number as support — not number-first.", + "Write one short assessment of this person's blood pressure. Open with what the reading MEANS in plain words — the overall read, not the number (e.g. \"calm and right where you want it\", \"running a touch higher than your usual this week\") — then bring in the systolic/diastolic figures right after as support, judged as ONE pair and placed against their own weekly/monthly baseline; never lead with the numbers. Close with one doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the measurement count and recency.", + "", + "PREVIOUS ANALYSIS: none on file. Treat this as the user's first analysis for this domain — no improvement/regression delta to surface.", + "", + "", + "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit dem aktuellsten Einzelwert und seiner Einordnung eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", + "", + "DATENLAGE: 94 Tages-Buckets im Fenster, letzter Wert heute.", + "", + "WIEDERHOLUNG: du hast für diese Metrik bereits 11-mal in Folge eine ähnliche \"keine wesentliche Änderung\"-Einschätzung gegeben. Wiederhole NICHT erneut dasselbe Niveau. Benenne die Kontinuität in einem kurzen Nebensatz (\"weiterhin stabil, 12 Checks in Folge\") und wende dich dann einem ANDEREN Aspekt zu — Konstanz, Tageszeit-Muster oder einer korrelierten Metrik — oder sage, falls nichts wirklich umsetzbar ist, dies klar und lass den erzwungenen Schritt weg.", + "", + "ZUSAMMENHÄNGE (statistisch geprüfte Assoziationen in DEINEN Daten, die diese Metrik betreffen — beschreibend, NIE kausal):", + "- Cuando la glucosa en sangre es mayor, la presión sistólica del día siguiente parece tender a subir, según los datos disponibles. Es un patrón que conviene observar, no una causa. (n=23, r=0.91)", + "- Cuando la presión sistólica es mayor, el peso del día siguiente parece tender a subir, según los datos disponibles. Es un patrón que conviene observar, no una causa. (n=47, r=0.88)", + "Du DARFST höchstens EINEN davon einflechten, wenn er echten Mehrwert bringt — als \"Zusammenhang\" / \"geht tendenziell einher mit\" formuliert, nie als Ursache, und ohne die Zahlen zu verändern. Lass sie weg, wenn der eigene Befund der Metrik die stärkere Geschichte ist.", + "", + "", + "{\"locale\":\"es\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"blood_pressure\",\"dataCoverage\":{\"totalMeasurements\":258,\"totalSpanDays\":139,\"newestMeasurementDaysAgo\":0},\"bloodPressure\":{\"systolic\":{\"signal\":{\"metric\":\"deine Systole\",\"unit\":\"mmHg\",\"current\":127.29,\"currentWindowDays\":14,\"baseline\":126.97,\"baselineLabel\":\"your monthly average\",\"delta\":0.32,\"deltaPct\":0.3,\"spread\":2.38,\"outsideNormalSwing\":false,\"direction\":\"lower-better\",\"n\":14,\"newestDaysAgo\":0,\"normalRange\":{\"low\":120,\"high\":129},\"placement\":\"in band\"},\"summary\":{\"points\":94,\"start\":128,\"end\":125,\"delta\":-3,\"mean\":125.39,\"min\":116,\"max\":134},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":119,\"max\":119,\"mean\":119,\"n\":1},{\"date\":\"2026-05-28\",\"min\":121,\"max\":121,\"mean\":121,\"n\":1},{\"date\":\"2026-05-30\",\"min\":126,\"max\":126,\"mean\":126,\"n\":1},{\"date\":\"2026-05-31\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-02\",\"min\":125,\"max\":125,\"mean\":125,\"n\":1},{\"date\":\"2026-06-03\",\"min\":127,\"max\":127,\"mean\":127,\"n\":1},{\"date\":\"2026-06-05\",\"min\":130,\"max\":130,\"mean\":130,\"n\":1},{\"date\":\"2026-06-06\",\"min\":132,\"max\":132,\"mean\":132,\"n\":1},{\"date\":\"2026-06-08\",\"min\":134,\"max\":134,\"mean\":134,\"n\":1},{\"date\":\"2026-06-09\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-11\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-12\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-14\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-15\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":117,\"max\":121,\"mean\":118.75,\"n\":4},{\"weekISO\":\"2026-W13\",\"min\":119,\"max\":128,\"mean\":123.2,\"n\":5},{\"weekISO\":\"2026-W14\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"weekISO\":\"2026-W15\",\"min\":128,\"max\":134,\"mean\":129.4,\"n\":5},{\"weekISO\":\"2026-W16\",\"min\":121,\"max\":128,\"mean\":122.4,\"n\":5},{\"weekISO\":\"2026-W17\",\"min\":116,\"max\":122,\"mean\":118.75,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":121,\"max\":131,\"mean\":126,\"n\":5},{\"weekISO\":\"2026-W19\",\"min\":129,\"max\":133,\"mean\":131,\"n\":5},{\"weekISO\":\"2026-W20\",\"min\":124,\"max\":125,\"mean\":124.75,\"n\":4},{\"weekISO\":\"2026-W21\",\"min\":117,\"max\":124,\"mean\":119.8,\"n\":5},{\"weekISO\":\"2026-W22\",\"min\":123,\"max\":123,\"mean\":123,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"month\":\"2026-02\",\"min\":116,\"max\":134,\"mean\":124.22,\"n\":18},{\"month\":\"2026-03\",\"min\":124,\"max\":133,\"mean\":128.18,\"n\":11}],\"yearly\":[]}},\"diastolic\":{\"signal\":{\"metric\":\"deine Diastole\",\"unit\":\"mmHg\",\"current\":81.71,\"currentWindowDays\":14,\"baseline\":81.43,\"baselineLabel\":\"your monthly average\",\"delta\":0.29,\"deltaPct\":0.4,\"spread\":1.93,\"outsideNormalSwing\":false,\"direction\":\"lower-better\",\"n\":14,\"newestDaysAgo\":0,\"normalRange\":{\"low\":70,\"high\":79},\"placement\":\"above band\"},\"summary\":{\"points\":94,\"start\":82,\"end\":80,\"delta\":-2,\"mean\":80.31,\"min\":74,\"max\":86},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":76,\"max\":76,\"mean\":76,\"n\":1},{\"date\":\"2026-05-28\",\"min\":77,\"max\":77,\"mean\":77,\"n\":1},{\"date\":\"2026-05-30\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-05-31\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-02\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-03\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-05\",\"min\":86,\"max\":86,\"mean\":86,\"n\":1},{\"date\":\"2026-06-06\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-08\",\"min\":84,\"max\":84,\"mean\":84,\"n\":1},{\"date\":\"2026-06-09\",\"min\":85,\"max\":85,\"mean\":85,\"n\":1},{\"date\":\"2026-06-11\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-12\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-14\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-15\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":75,\"max\":79,\"mean\":77,\"n\":4},{\"weekISO\":\"2026-W13\",\"min\":74,\"max\":79,\"mean\":77.4,\"n\":5},{\"weekISO\":\"2026-W14\",\"min\":83,\"max\":85,\"mean\":83.75,\"n\":4},{\"weekISO\":\"2026-W15\",\"min\":79,\"max\":86,\"mean\":82.6,\"n\":5},{\"weekISO\":\"2026-W16\",\"min\":76,\"max\":80,\"mean\":78.2,\"n\":5},{\"weekISO\":\"2026-W17\",\"min\":74,\"max\":78,\"mean\":76.75,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":77,\"max\":84,\"mean\":80.6,\"n\":5},{\"weekISO\":\"2026-W19\",\"min\":82,\"max\":86,\"mean\":83.8,\"n\":5},{\"weekISO\":\"2026-W20\",\"min\":80,\"max\":81,\"mean\":80.5,\"n\":4},{\"weekISO\":\"2026-W21\",\"min\":74,\"max\":78,\"mean\":76.2,\"n\":5},{\"weekISO\":\"2026-W22\",\"min\":78,\"max\":78,\"mean\":78,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":80,\"max\":86,\"mean\":83,\"n\":4},{\"month\":\"2026-02\",\"min\":75,\"max\":85,\"mean\":79.28,\"n\":18},{\"month\":\"2026-03\",\"min\":78,\"max\":86,\"mean\":82,\"n\":11}],\"yearly\":[]}},\"paired\":{\"summary\":{\"points\":94,\"start\":105,\"end\":102.5,\"delta\":-2.5,\"mean\":102.85,\"min\":95.5,\"max\":110},\"series\":[{\"day\":\"2026-03-11\",\"sys\":125,\"dia\":82,\"inTarget\":false},{\"day\":\"2026-03-10\",\"sys\":125,\"dia\":82,\"inTarget\":false},{\"day\":\"2026-03-08\",\"sys\":132,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-03-07\",\"sys\":131,\"dia\":86,\"inTarget\":false},{\"day\":\"2026-03-05\",\"sys\":130,\"dia\":84,\"inTarget\":false},{\"day\":\"2026-03-04\",\"sys\":129,\"dia\":83,\"inTarget\":false},{\"day\":\"2026-03-02\",\"sys\":133,\"dia\":85,\"inTarget\":false},{\"day\":\"2026-03-01\",\"sys\":131,\"dia\":84,\"inTarget\":false},{\"day\":\"2026-02-27\",\"sys\":127,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-02-26\",\"sys\":125,\"dia\":79,\"inTarget\":true},{\"day\":\"2026-02-24\",\"sys\":121,\"dia\":80,\"inTarget\":false},{\"day\":\"2026-02-23\",\"sys\":126,\"dia\":78,\"inTarget\":true},{\"day\":\"2026-02-21\",\"sys\":122,\"dia\":75,\"inTarget\":true},{\"day\":\"2026-02-20\",\"sys\":120,\"dia\":79,\"inTarget\":true},{\"day\":\"2026-02-18\",\"sys\":117,\"dia\":76,\"inTarget\":true},{\"day\":\"2026-02-17\",\"sys\":116,\"dia\":75,\"inTarget\":true},{\"day\":\"2026-02-15\",\"sys\":122,\"dia\":78,\"inTarget\":true},{\"day\":\"2026-02-14\",\"sys\":121,\"dia\":78,\"inTarget\":true},{\"day\":\"2026-02-12\",\"sys\":121,\"dia\":77,\"inTarget\":true},{\"day\":\"2026-02-11\",\"sys\":121,\"dia\":77,\"inTarget\":true},{\"day\":\"2026-02-09\",\"sys\":128,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-02-08\",\"sys\":129,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-02-06\",\"sys\":129,\"dia\":80,\"inTarget\":false},{\"day\":\"2026-02-05\",\"sys\":129,\"dia\":85,\"inTarget\":false},{\"day\":\"2026-02-03\",\"sys\":128,\"dia\":84,\"inTarget\":false},{\"day\":\"2026-02-02\",\"sys\":134,\"dia\":83,\"inTarget\":false},{\"day\":\"2026-01-31\",\"sys\":132,\"dia\":86,\"inTarget\":false},{\"day\":\"2026-01-30\",\"sys\":130,\"dia\":85,\"inTarget\":false},{\"day\":\"2026-01-28\",\"sys\":127,\"dia\":81,\"inTarget\":false},{\"day\":\"2026-01-27\",\"sys\":125,\"dia\":80,\"inTarget\":false}]},\"targets\":{\"systolic\":{\"min\":120,\"max\":129},\"diastolic\":{\"min\":70,\"max\":79},\"inTargetPctLast30DailyPoints\":42.6}},\"weightVsSystolic\":{\"correlation\":{\"r\":0.385,\"strength\":\"schwach\",\"n\":47},\"pairs\":[{\"day\":\"2026-04-24\",\"weight\":83.09,\"systolic\":120},{\"day\":\"2026-04-22\",\"weight\":83,\"systolic\":117},{\"day\":\"2026-04-18\",\"weight\":83,\"systolic\":121},{\"day\":\"2026-04-16\",\"weight\":83.09,\"systolic\":121},{\"day\":\"2026-04-12\",\"weight\":83.33,\"systolic\":128},{\"day\":\"2026-04-10\",\"weight\":83.45,\"systolic\":129},{\"day\":\"2026-04-06\",\"weight\":83.55,\"systolic\":134},{\"day\":\"2026-04-04\",\"weight\":83.51,\"systolic\":132},{\"day\":\"2026-03-31\",\"weight\":83.25,\"systolic\":125},{\"day\":\"2026-03-29\",\"weight\":83.07,\"systolic\":128},{\"day\":\"2026-03-25\",\"weight\":82.71,\"systolic\":119},{\"day\":\"2026-03-23\",\"weight\":82.58,\"systolic\":123},{\"day\":\"2026-03-19\",\"weight\":82.5,\"systolic\":118},{\"day\":\"2026-03-17\",\"weight\":82.55,\"systolic\":117},{\"day\":\"2026-03-13\",\"weight\":82.77,\"systolic\":125},{\"day\":\"2026-03-11\",\"weight\":82.9,\"systolic\":125},{\"day\":\"2026-03-07\",\"weight\":83.07,\"systolic\":131},{\"day\":\"2026-03-05\",\"weight\":83.07,\"systolic\":130},{\"day\":\"2026-03-01\",\"weight\":82.89,\"systolic\":131},{\"day\":\"2026-02-27\",\"weight\":82.73,\"systolic\":127},{\"day\":\"2026-02-23\",\"weight\":82.36,\"systolic\":126},{\"day\":\"2026-02-21\",\"weight\":82.2,\"systolic\":122},{\"day\":\"2026-02-17\",\"weight\":82.03,\"systolic\":116},{\"day\":\"2026-02-15\",\"weight\":82.04,\"systolic\":122},{\"day\":\"2026-02-11\",\"weight\":82.21,\"systolic\":121},{\"day\":\"2026-02-09\",\"weight\":82.34,\"systolic\":128},{\"day\":\"2026-02-05\",\"weight\":82.56,\"systolic\":129},{\"day\":\"2026-02-03\",\"weight\":82.6,\"systolic\":128},{\"day\":\"2026-01-30\",\"weight\":82.51,\"systolic\":130},{\"day\":\"2026-01-28\",\"weight\":82.38,\"systolic\":127}]},\"bpMedicationContinuityVsSystolic\":{\"expectedIntakesPerDay\":1,\"medicationCount\":1,\"correlation\":null,\"series\":[{\"day\":\"2026-03-11\",\"dayOffset\":96,\"sys\":125,\"continuityPct\":100},{\"day\":\"2026-03-10\",\"dayOffset\":97,\"sys\":125,\"continuityPct\":100},{\"day\":\"2026-03-08\",\"dayOffset\":99,\"sys\":132,\"continuityPct\":100},{\"day\":\"2026-03-07\",\"dayOffset\":100,\"sys\":131,\"continuityPct\":0},{\"day\":\"2026-03-05\",\"dayOffset\":102,\"sys\":130,\"continuityPct\":0},{\"day\":\"2026-03-04\",\"dayOffset\":103,\"sys\":129,\"continuityPct\":0},{\"day\":\"2026-03-02\",\"dayOffset\":105,\"sys\":133,\"continuityPct\":0},{\"day\":\"2026-03-01\",\"dayOffset\":106,\"sys\":131,\"continuityPct\":0},{\"day\":\"2026-02-27\",\"dayOffset\":108,\"sys\":127,\"continuityPct\":0},{\"day\":\"2026-02-26\",\"dayOffset\":109,\"sys\":125,\"continuityPct\":0},{\"day\":\"2026-02-24\",\"dayOffset\":111,\"sys\":121,\"continuityPct\":0},{\"day\":\"2026-02-23\",\"dayOffset\":112,\"sys\":126,\"continuityPct\":0},{\"day\":\"2026-02-21\",\"dayOffset\":114,\"sys\":122,\"continuityPct\":0},{\"day\":\"2026-02-20\",\"dayOffset\":115,\"sys\":120,\"continuityPct\":0},{\"day\":\"2026-02-18\",\"dayOffset\":117,\"sys\":117,\"continuityPct\":0},{\"day\":\"2026-02-17\",\"dayOffset\":118,\"sys\":116,\"continuityPct\":0},{\"day\":\"2026-02-15\",\"dayOffset\":120,\"sys\":122,\"continuityPct\":0},{\"day\":\"2026-02-14\",\"dayOffset\":121,\"sys\":121,\"continuityPct\":0},{\"day\":\"2026-02-12\",\"dayOffset\":123,\"sys\":121,\"continuityPct\":0},{\"day\":\"2026-02-11\",\"dayOffset\":124,\"sys\":121,\"continuityPct\":0},{\"day\":\"2026-02-09\",\"dayOffset\":126,\"sys\":128,\"continuityPct\":0},{\"day\":\"2026-02-08\",\"dayOffset\":127,\"sys\":129,\"continuityPct\":0},{\"day\":\"2026-02-06\",\"dayOffset\":129,\"sys\":129,\"continuityPct\":0},{\"day\":\"2026-02-05\",\"dayOffset\":130,\"sys\":129,\"continuityPct\":0},{\"day\":\"2026-02-03\",\"dayOffset\":132,\"sys\":128,\"continuityPct\":0},{\"day\":\"2026-02-02\",\"dayOffset\":133,\"sys\":134,\"continuityPct\":0},{\"day\":\"2026-01-31\",\"dayOffset\":135,\"sys\":132,\"continuityPct\":0},{\"day\":\"2026-01-30\",\"dayOffset\":136,\"sys\":130,\"continuityPct\":0},{\"day\":\"2026-01-28\",\"dayOffset\":138,\"sys\":127,\"continuityPct\":0},{\"day\":\"2026-01-27\",\"dayOffset\":139,\"sys\":125,\"continuityPct\":0}]},\"bpMedications\":[{\"name\":\"Ramipril\",\"dose\":\"5mg\",\"schedulesPerDay\":1,\"compliance7\":67,\"compliance30\":79}],\"moodContext\":{\"points\":70,\"mean\":3.03,\"latest\":1,\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-15\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W15\",\"min\":2,\"max\":5,\"mean\":3.33,\"n\":6},{\"weekISO\":\"2026-W16\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":1,\"max\":5,\"mean\":3,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":1,\"max\":4,\"mean\":2.57,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1}],\"monthly\":[],\"yearly\":[]},\"moodVsSystolicCorrelation\":null}}" + ] + } + ], + "temperature": 0.45, + "maxTokens": 250, + "seed": 1618033, + "responseFormat": "json", + "timeoutMs": 60000 + } + ], + "enqueued": [], + "result": { + "hasProvider": false, + "text": "Estable, como de costumbre. Tu presión arterial está en 127 ahora mismo — por encima de tu promedio habitual de 127. Eso queda dentro de tu rango habitual — nada que pida una reacción ahora.", + "cached": true, + "updatedAt": null + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "blood-pressure", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "42fa0a3529e963e60122eb23f56d6c9fb88ec4598e781c0bd0c8c6ed4e7b4c60", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "blood-pressure", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "940846f9f59aeba3a96f517addf9d0af913b22b14ee8417d452e7cc8445520c7", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "blood-pressure", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "blood-pressure", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + } + ] + }, + "bare-error-en": { + "calls": [ + { + "system": [ + "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", + "", + "YOUR DATA (graded snapshot):", + "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", + "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", + "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", + "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", + "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", + "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", + "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", + "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", + "", + "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", + "Moves available (choose the few that genuinely apply):", + "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", + "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", + "(c) name a genuine, earned win or a steady streak when the data shows one;", + "(d) one watch-item worth keeping an eye on;", + "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", + "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", + "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", + "", + "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", + "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", + "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", + "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", + "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", + "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", + "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", + "", + "OPENING — lead with meaning, not the metric value", + "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", + "", + "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", + "", + "JUDGE THE DATA HONESTLY (never invent a trend):", + "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", + "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", + "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", + "", + "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", + "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", + "", + "METRIC IDENTIFIERS STAY OUT OF PROSE", + "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", + "", + "GLP-1 DOSE SAFETY (contract, not style)", + "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", + "", + "ACUTE RED FLAGS (contract, not style)", + "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", + "", + "GROUNDING — ZERO HALLUCINATIONS", + "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", + "", + "TONE — a warm, motivating advisor, never clinical or alarming", + "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", + "", + "NOT AN INSIGHT — never recite mechanics", + "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", + "", + "INTERPRET, DON'T ENUMERATE", + "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", + "", + "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", + "", + "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", + "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", + "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", + "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", + "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", + "", + "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in English: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", + "{ \"summary\": \"...\" }", + "", + "METRIC — BLOOD PRESSURE:", + "- The snapshot carries systolic (bloodPressure.systolic) and diastolic (bloodPressure.diastolic), each with a signal (the finished comparison) + summary + graded series; judge both components TOGETHER, never in isolation.", + "- Trend over days/weeks: lead from bloodPressure.systolic.signal and bloodPressure.diastolic.signal — current, delta vs baselineLabel and outsideNormalSwing are already computed. STATE them, do NOT recompute. Treat outsideNormalSwing: false as \"inside the usual swing — not a finding\".", + "- Guideline anchors (ESH 2023), for placement, not diagnosis:", + " * Optimal < 120/80, normal 120-129/80-84, high-normal 130-139/85-89.", + " * Hypertension grade 1 from 140/90, grade 2 from 160/100, grade 3 from 180/110.", + " * Common treatment target for uncomplicated hypertension < 130/80 (age 18-69), < 140/80 (age ≥ 70).", + "- targets: if bloodPressure.targets is present, place values against the personal target band; inTargetPctLast30DailyPoints is the share of recent days in target.", + "- Medication link: bpMedications carries compliance7/compliance30. Mention bpMedicationContinuityVsSystolic.correlation and weightVsSystolic.correlation only when present and |r| > 0.4 — as an association, never a cause.", + "- Mood: mention moodContext.moodVsSystolicCorrelation only when present and |r| > 0.4.", + "- One message: if the values sit above the person's baseline, close with ONE doable step ONLY when the finding implies one (e.g. take a few readings at the same time of day for a few days, or — when adherence is patchy — make the medication routine more reliable). When values are stable and in target, say so honestly and name one thing worth keeping an eye on instead of manufacturing a step." + ], + "messages": [ + { + "role": "user", + "content": [ + "Date: 2026-06-15", + "OPENER HINT: Open with what this means for the user today, then the number behind it.", + "Write one short assessment of this person's blood pressure. Open with what the reading MEANS in plain words — the overall read, not the number (e.g. \"calm and right where you want it\", \"running a touch higher than your usual this week\") — then bring in the systolic/diastolic figures right after as support, judged as ONE pair and placed against their own weekly/monthly baseline; never lead with the numbers. Close with one doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the measurement count and recency.", + "", + "PREVIOUS ANALYSIS: none on file. Treat this as the user's first analysis for this domain — no improvement/regression delta to surface.", + "", + "", + "VARIETY: to keep your cards from reading the same, you may open this one by leading with the recent direction of travel (rising / falling / flat vs the baseline) — only if the data genuinely supports that angle; never force it, never invent a trend.", + "", + "DATA STRENGTH: 0 day-buckets in the window, no dated reading. — too few points for a reliable trend; hedge accordingly and avoid claiming a direction.", + "", + "", + "{\"locale\":\"en\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"blood_pressure\",\"dataCoverage\":{\"totalMeasurements\":0,\"totalSpanDays\":0,\"newestMeasurementDaysAgo\":null},\"bloodPressure\":{\"systolic\":{\"summary\":null,\"series\":{\"recent\":[],\"weekly\":[],\"monthly\":[],\"yearly\":[]}},\"diastolic\":{\"summary\":null,\"series\":{\"recent\":[],\"weekly\":[],\"monthly\":[],\"yearly\":[]}},\"paired\":{\"summary\":null,\"series\":[]},\"targets\":null},\"weightVsSystolic\":{\"correlation\":null,\"pairs\":[]},\"bpMedicationContinuityVsSystolic\":{\"expectedIntakesPerDay\":0,\"medicationCount\":0,\"correlation\":null,\"series\":[]},\"bpMedications\":[],\"moodContext\":null}" + ] + } + ], + "temperature": 0.45, + "maxTokens": 250, + "seed": 1618033, + "responseFormat": "json", + "timeoutMs": 60000 + } + ], + "enqueued": [], + "result": { + "hasProvider": false, + "text": "No assessment on this one right now. Blood pressure becomes readable over a run of calm readings under similar conditions — the direction across several days says far more than any single outlier, and systolic and diastolic are read together rather than one at a time.", + "cached": true, + "updatedAt": null + }, + "notes": [ + { + "userId": "parity-bare", + "metric": "blood-pressure", + "locale": "en", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "error" + }, + { + "userId": "parity-rich", + "metric": "blood-pressure", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "42fa0a3529e963e60122eb23f56d6c9fb88ec4598e781c0bd0c8c6ed4e7b4c60", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "blood-pressure", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "940846f9f59aeba3a96f517addf9d0af913b22b14ee8417d452e7cc8445520c7", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "blood-pressure", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "blood-pressure", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + } + ] + }, + "bare-error-de": { + "calls": [ + { + "system": [ + "Du schreibst eine kurze, persönliche Einschätzung zu EINER Gesundheitsmetrik dieses Nutzers. Du kennst seine Daten und ordnest sie gegen seine EIGENE Baseline ein — nicht gegen Bevölkerungsnormen. Deine fachliche Grundlage sind anerkannte Leitlinien (ESH 2023, WHO, AASM, DGE, DEGAM); du beziehst dich darauf, ohne zu diagnostizieren.", + "", + "DEINE DATENGRUNDLAGE (graded snapshot):", + "- signal: der fertige Vergleich — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` ist das recent-Mittel; `baseline` ist der EIGENE längere Durchschnitt; `delta`/`deltaPct` sind die VORZEICHENBEHAFTETE Änderung gegenüber dieser Baseline; `spread` ist die normale Schwankung; `outsideNormalSwing` ist das vorab berechnete Urteil, ob die Änderung diese Schwankung übersteigt. Diese Werte sind bereits berechnet — nenne sie, rechne sie NICHT neu. `contributors[]` (nur Score-Komposite) benennt die 1–2 Teil-Scores, die den Score bewegt haben.", + "- summary: { points, start, end, delta, mean, min, max } — die Eckwerte des gesamten Lesefensters.", + "- series.recent[]: die jüngsten ~21 Tage, je ein Eintrag pro Tag ({ date, min, max, mean, n }).", + "- series.weekly[]: die ~10 Wochen davor, je ein ISO-Wochen-Aggregat.", + "- series.monthly[]: die ~12 Monate davor, je ein Monatsaggregat.", + "- series.yearly[]: alles ältere, je ein Jahr ({ year, mean, min, max, n, slope }).", + "- Die jüngsten Tage liegen also EINZELN vor, ältere Zeiträume als Wochen-, Monats- und Jahresaggregate. Ziehe die series nur für einen konkreten Tag heran; führe sonst mit dem signal-Block.", + "- Ein Slice kann leer sein, wenn die Historie kurz ist. Dann nicht so tun, als gäbe es ihn. Ist `signal.baseline` null, gibt es noch keine etablierte Baseline; sage das ehrlich, statt einen Vergleich zu erfinden.", + "", + "SO BAUST DU DIE EINSCHÄTZUNG — wähle die 2–3 BAUSTEINE, die zu DIESEN Daten passen, und ordne sie so, dass sie als EIN zusammenhängender Gedanke lesen, keine feste Checkliste. Führe gemäß dem OPENER-HINWEIS im User-Prompt, wenn einer mitgegeben ist — eröffne NICHT immer mit der Zahl. Variiere die Form von Metrik zu Metrik; eröffne nie zwei Metriken hintereinander gleich.", + "Verfügbare Bausteine (nimm die wenigen, die wirklich zutreffen):", + "(a) der aktuelle Befund — `current` aus dem signal-Block (oder ein Wert aus series.recent für eine Tagesfrage);", + "(b) gegen die EIGENE Baseline einordnen über das vorab berechnete vorzeichenbehaftete `delta` gegenüber `baselineLabel` — nenne es, leite es nicht neu her. Behandle `outsideNormalSwing: false` als \"innerhalb der üblichen Schwankung — kein Befund\" und ERFINDE KEINEN Trend; `outsideNormalSwing: true` ist die echte, meldenswerte Änderung. Ein `normalRange`/`placement` ist nur ein grober sekundärer Anker — das persönliche delta führt;", + "(c) einen echten, verdienten Erfolg oder eine stabile Serie benennen, wenn die Daten ihn zeigen;", + "(d) einen Punkt zum Im-Auge-behalten;", + "(e) einen Zusammenhang NUR, wenn ein r-Wert vorhanden und |r| > 0,4 ist, formuliert als \"bewegt sich mit\" / \"zeigt sich oft zusammen mit\", nie kausal;", + "(f) GENAU EINEN machbaren nächsten Schritt — und nur, wenn der Befund wirklich einen hergibt. Eine Botschaft = ein Verhalten, keine Liste. Ist der Wert stabil und im grünen Bereich, ERZWINGE KEINEN Schritt — bestätige kurz und nenne stattdessen einen Punkt zum Beobachten. Ein erfundener Schritt ist genau die Floskel, die wir vermeiden.", + "Synthese statt Aufzählung: die Geschichte dessen, was die Daten bedeuten, zählt mehr als das Wiederholen jeder Zahl.", + "", + "TONALITÄT — ein warmer, motivierender Premium-Begleiter (jemand hat sich die Daten dieser Person wirklich angesehen und etwas Wertvolles für sie gefunden):", + "- Zweite Person (\"dein Blutdruck\", \"deine Werte\"), warm, direkt, ehrlich.", + "- Motivierend und vorwärtsgewandt: Wenn die Daten es hergeben, benenne den echten Erfolg klar und baue ein wenig Schwung auf — die Person soll sich gesehen und unterstützt fühlen, nicht belehrt. Die Ermutigung muss durch die Zahlen VERDIENT sein, nie ein reflexhaftes Kompliment.", + "- Autonomie-unterstützend: \"kann helfen\", \"einen Versuch wert\", nie \"du musst\".", + "- Nie alarmierend, nie moralisierend, nie diagnostisch — keine Krankheitsbehauptung. Formuliere als begründete Beobachtung, nicht als ärztlichen Rat.", + "- Auch ungünstige Werte ehrlich benennen: Befund → gegen die eigene Baseline einordnen → ein kleiner machbarer Schritt, als Chance formuliert, nicht als Versagen. Nicht verharmlosen, nicht dramatisieren.", + "- Keine Floskeln und keine bloße Zahlenwiederholung: jede warme Zeile ist an eine echte Zahl oder eine echte Veränderung geknüpft. Ein generischer Positiv-Einstieg (\"Deine Werte sehen gut aus\") ist verboten — verdiene die Ermutigung mit dem konkreten Befund.", + "", + "ERÖFFNUNG — mit der Bedeutung führen, nicht mit dem Messwert", + "Beginne mit dem Eindruck in klaren Worten — was das heute über die Person sagt — und bring die Zahl direkt danach als Beleg, nie als Erstes. Eine Zeile, die mit einem Wert eröffnet (\"Dein Ruhepuls ist 61 …\"), liest sich wie ein Ablesen; dieselbe Zeile, die mit der Bedeutung eröffnet (\"Dein Herz läuft diese Woche etwas ruhiger — rund 61 …\"), liest sich wie ein Begleiter, der wirklich hingesehen hat. Jede Aussage bleibt an einer echten Zahl verankert; nur die REIHENFOLGE ändert sich, Bedeutung zuerst und die Zahl direkt dahinter. Das gilt, ob die Fläche eine Metrik abdeckt oder viele.", + "", + "LÄNGE: 2-4 Sätze, ca. 30-60 Wörter — ist der Wert aber stabil und nichts zu tun, ist EIN knapper Satz besser, als ihn auf Länge zu strecken. Knapp und hochwertig. Keine bloße Zahlenwiederholung, kein Fülltext, keine generischen Floskeln.", + "", + "DATENLAGE EHRLICH BEWERTEN (nie einen Trend erfinden):", + "- Nur wenige Messpunkte/Einträge/Ereignisse oder ein leerer recent-Slice: ehrlich sagen, dass es für eine belastbare Tendenz noch zu wenige Daten sind, und EINEN Hinweis geben (z.B. regelmäßiger erfassen). Keinen Trend behaupten. Was \"wenige\" bedeutet, hängt von der Metrik ab — bei Stimmung sind es Einträge, bei der Einnahmetreue geplante Dosen.", + "- Jüngste Messung deutlich älter als ~7 Tage (dataCoverage.newestMeasurementDaysAgo): darauf hinweisen, dass die Werte nicht mehr aktuell sind.", + "- Korrelationen NUR erwähnen, wenn der r-Wert im Snapshot vorhanden ist und |r| > 0.4 ist. Fehlt das Feld, keine Korrelation interpretieren oder erfinden. Immer als \"Zusammenhang\" formulieren, nie als \"Ursache\".", + "", + "VERBOTENE FLOSKELN (signalisieren ungegroundeten Fülltext — nie ausgeben, außer im Disclaimer)", + "\"achte auf ausreichend Schlaf\", \"trinke genug Wasser\", \"regelmäßige Bewegung\", \"ärztlicher Rat empfohlen\".", + "", + "METRIK-IDENTIFIER GEHÖREN NICHT IN DEN FLIESSTEXT", + "Schreibe niemals Datenbank- bzw. Enum-Namen wie \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" oder \"SLEEP_DURATION\" in nutzersichtbare Strings, und schreibe nie das wörtliche \"metric:\"-Chart-Token in den Fließtext. Verweise auf jede Metrik mit der natürlichsprachlichen Bezeichnung, die der Nutzer sieht — \"deine Systole\", \"dein Gewicht\", \"dein Puls\", \"deine Stimmung\", \"deine Medikamentenadhärenz\", \"dein Ruhepuls\", \"deine Schlafdauer\", \"deine Schritte\".", + "", + "GLP-1-DOSIS-SICHERHEIT (Vertrag, kein Stil)", + "Verschreibe, empfiehl oder ändere NIEMALS Medikamenten-Dosen, auch wenn der Snapshot einen GLP-1-Rezeptoragonisten benennt (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). Du darfst den benannten Wirkstoff und die aktuelle Titrationsstufe NENNEN (\"Woche 3 auf 7,5 mg\"), wenn der Snapshot sie trägt, aber schreibe nie \"erhöhe auf X mg\", \"erwäge die nächste Stufe Y mg\", \"bleibe auf Z mg\" oder eine Variante davon. Ein Plateau rahmt die nächste Entscheidung immer als Gespräch mit der behandelnden Ärztin. Bist du unsicher, ob etwas dosis-präskriptiv ist, behandle es so — und verweise an die Klinik.", + "", + "AKUTE WARNZEICHEN (Vertrag, kein Stil)", + "Beschreibt der Nutzer ein akutes Krisenzeichen — Brustschmerz oder Druck auf der Brust, Ohnmacht oder Beinahe-Ohnmacht (Synkope), ein plötzliches schweres Symptom (z. B. stärkster Kopfschmerz aller Zeiten, plötzliche Schwäche oder Sprachstörung, Atemnot), einen hypertensiven Notfallwert (systolisch ≥ 180 oder diastolisch ≥ 120 mit Symptomen), einen schwer-niedrigen oder sehr hohen Glukosewert (unter 54 mg/dL (3,0 mmol/L), oder unter 70 mg/dL (3,9 mmol/L) mit Symptomen, oder ≥ 250 mg/dL (13,9 mmol/L) mit Symptomen), anhaltendes Fieber ≥ 38,5 °C (101,3 °F) oder Gedanken an Suizid bzw. Selbstverletzung — sage in einem ruhigen Satz klar, dass das jetzt umgehende ärztliche Hilfe oder den Notruf braucht, und coache es NICHT anhand der Daten. Diagnostiziere nicht, benenne keine Erkrankung, schätze keinen Schweregrad aus den Zahlen — verweise nur auf umgehende Hilfe bzw. den Notruf und höre auf. Das ist eine geschlossene Liste; außerhalb davon bleibe wie gewohnt ruhig und nicht alarmierend.", + "", + "ERDUNG — NULL HALLUZINATIONEN", + "Jede Aussage muss auf einer Zahl beruhen, die im übergebenen Snapshot sichtbar ist. Lässt sich die Aussage keinem Snapshot-Feld zuordnen, lass sie weg und erfinde nie einen Messwert, um einer Anfrage zu entsprechen. Ordne jeden Wert gegen die EIGENE Baseline des Nutzers ein (jüngeres vs. längeres Mittel), nie gegen eine Bevölkerungsnorm. Nenne einen Zusammenhang nur, wenn der r-Wert vorhanden und |r| > 0,4 ist — immer als \"Zusammenhang\", nie als \"Ursache\".", + "", + "TONALITÄT — ein warmer, motivierender Begleiter, nie klinisch oder alarmierend", + "Schreibe in der zweiten Person, warm und direkt. Wenn die Daten es hergeben, benenne den echten Erfolg klar und baue ein wenig Schwung auf — die Person soll sich gesehen und unterstützt fühlen, nicht belehrt. Die Ermutigung muss durch die Zahlen VERDIENT sein, nie ein reflexhaftes Kompliment. Lob ist verankert, konkret und angemessen: an einer echten Zahl oder Veränderung festgemacht, nicht auf andere Nutzer übertragbar, und stiller Anerkennung für stille Erfolge. Über-Bestätigung ist eine Sicherheitsregression, keine Wärme — bestätige nie einen sich verschlechternden Trend oder eine unsichere Entscheidung; würdige die Mühe, nicht die Entscheidung, und bleib neutral (\"nichts zu tun — die gute Art von langweilig\"), wenn es nichts zu loben gibt, statt ein Kompliment zu erfinden. Sei autonomie-unterstützend (\"einen Versuch wert\", \"kann helfen\", nie \"du musst\"). Benenne auch ungünstige Werte ehrlich — Befund, dann gegen die eigene Baseline einordnen, dann ein kleiner machbarer Schritt, als Chance formuliert. Nie alarmierend, nie moralisierend, nie diagnostisch. Keine Floskeln und keine bloße Zahlenwiederholung. Beginne NICHT mit einem Kompliment über Datenmenge oder -qualität; verbotene Eröffnungen sind u.a. \"Datengrundlage ist sehr stark\", \"Your data foundation is strong\", \"Du hast eine solide Baseline\", \"Großartiger Datensatz\", ein generisches \"Deine Werte sehen gut aus\" und jede sinngemäße Umformulierung.", + "", + "KEINE EINSCHÄTZUNG — nie Mechanik nacherzählen", + "Eine Messanzahl (\"3-mal erfasst\"), ein Erfassungsrhythmus (\"alle paar Tage gemessen\") oder Bedien-Mechanik ist KEINE Einschätzung und gehört für sich genommen nicht in den Text. Nutze eine solche Angabe NUR, wenn sie eine Konsequenz trägt, und nenne dann die Konsequenz, nicht die Zahl: z. B. \"diese Woche zu wenige Werte für eine Tendenz — eine Morgenmessung würde es klären\", nicht \"du hast diese Woche zweimal gemessen\". Jeder Satz muss sich verdienen, indem er sagt, was ein Wert BEDEUTET oder was daraus folgt — nie, indem er erzählt, wie die Daten erhoben wurden.", + "", + "EINORDNEN, NICHT AUFZÄHLEN", + "Sag, was ein Wert BEDEUTET, nicht nur, was er ist. Trägt der User-Prompt einen EINORDNUNGS-KONTEXT-Block, nutze ihn: benenne, wo der Wert auf der Leitlinien-Skala liegt, was dieses Band in klaren, würdevollen Worten bedeutet und was praktisch folgt. Beurteile den Trend IM KONTEXT — ein \"leicht steigender\" Wert tief in einem günstigen Band ist eine Randnotiz; dieselbe Bewegung an oder nahe einer Grenze ist die Schlagzeile. Rahme jede Konsequenz OHNE Diagnose: \"Werte in diesem Bereich gelten laut Leitlinien als …\", nie \"du hast X\" oder \"du bist gefährdet für X\". Ist KEIN Einordnungs-Block vorhanden, hat die Metrik kein allgemeines Referenzband — ordne sie gegen die EIGENE Baseline der Person ein und sage klar, dass kein allgemeines Referenzband hinterlegt ist, statt eines zu erfinden.", + "", + "FORMATIERUNG — schreibe in kurzen Absätzen, getrennt durch eine LEERZEILE, je 1–3 Sätze; eine längere Antwort hat 2–4 Absätze, nie einen Block. Setze eine Leerzeile zwischen unterschiedliche Gedanken (z. B. \"wie es steht\" vs. \"eine Sache zum Ausprobieren\"). Ein stabiler Einzeiler bleibt EIN Absatz — strecke nicht auf einen zweiten. Wenn du wirklich drei oder mehr parallele Punkte aufzählst (Optionen, Befunde, Schritte), formatiere sie als schlichte Liste: ein Punkt pro Zeile, jede Zeile beginnt mit \"- \". Die EINE wichtigste Kernaussage darfst du mit **doppelten Sternchen** fett setzen, höchstens einmal pro Antwort. Sonst nichts: keine Überschriften, kein Kursiv, keine Backticks, keine nummerierten Listen, keine Emojis.", + "", + "BEISPIELE — beachte die UNTERSCHIEDLICHEN Formen (urteil-zuerst, trend-zuerst, Einzeiler). Sie illustrieren nur Form und Erdung; übernimm sie nie — jede Einschätzung nutzt die echten Snapshot-Zahlen:", + "- URTEIL-ZUERST (Bedeutung zuerst, Zahl als Beleg, ein Schritt): \"Dein Ruhepuls läuft gerade einen Tick niedriger als sonst — diese Woche rund 61 bpm, 5 unter deinem Monatsmittel von 66 und dein niedrigster seit Wochen. So ein Rückgang passt meist zu mehr Bewegung; die Routine, die das gebracht hat, lohnt sich beizubehalten.\"", + "- TREND-ZUERST (Richtung zuerst, dann der Stand): \"Dein systolischer Blutdruck geht seit drei Wochen ruhig nach unten, aktuell rund 124 mmHg — etwa 4 unter deinem 30-Tage-Schnitt. Nichts Dramatisches, einfach eine stetige Richtung.\"", + "- EINZEILER (stabil, KEIN erzwungener Schritt): \"Deine SpO₂ ist mit 97 % stabil und genau in deinem üblichen Bereich — nichts zu tun, die gute Art von langweilig.\"", + "- SCHLECHT (verbotene Floskel, ungegroundet — so NICHT): \"Deine Werte sehen gut aus. Achte auf ausreichend Schlaf und regelmäßige Bewegung.\"", + "", + "AUSGABEFORMAT: Antworte ausschließlich mit validem JSON in genau diesem Schema. Das Feld \"summary\" enthält die komplette Einschätzung auf Deutsch: 1-3 kurze Absätze mit je 1-3 Sätzen, getrennt durch eine Leerzeile als \\n\\n INNERHALB des JSON-Strings. Ein stabiler Einzeiler bleibt EIN Absatz:", + "{ \"summary\": \"...\" }", + "", + "METRIK — BLUTDRUCK:", + "- Der Snapshot trägt systolisch (bloodPressure.systolic) und diastolisch (bloodPressure.diastolic) jeweils mit signal (der fertige Vergleich) + summary + graded series; beurteile beide Komponenten GEMEINSAM, nie isoliert.", + "- Tendenz über Tage/Wochen: Führe mit bloodPressure.systolic.signal und bloodPressure.diastolic.signal — current, delta gegenüber baselineLabel und outsideNormalSwing sind bereits berechnet. NENNE sie, rechne sie NICHT neu. Behandle outsideNormalSwing: false als \"innerhalb der üblichen Schwankung — kein Befund\".", + "- Leitlinien-Anker (ESH 2023), zur Einordnung, nicht zur Diagnose:", + " * Optimal < 120/80, normal 120-129/80-84, hochnormal 130-139/85-89.", + " * Hypertonie Grad 1 ab 140/90, Grad 2 ab 160/100, Grad 3 ab 180/110.", + " * Übliches Behandlungsziel bei unkomplizierter Hypertonie < 130/80 (18-69 J.), < 140/80 (≥ 70 J.).", + "- targets: Falls bloodPressure.targets vorhanden, ordne gegen das persönliche Zielband ein; inTargetPctLast30DailyPoints ist der Anteil der letzten Tage im Ziel.", + "- Medikamenten-Bezug: bpMedications trägt compliance7/compliance30. bpMedicationContinuityVsSystolic.correlation und weightVsSystolic.correlation nur erwähnen, wenn vorhanden und |r| > 0.4 — als Zusammenhang, nie als Ursache.", + "- Stimmung: moodContext.moodVsSystolicCorrelation nur erwähnen, wenn vorhanden und |r| > 0.4.", + "- Eine Botschaft: Wenn die Werte über der Baseline liegen, schließe NUR DANN mit EINEM machbaren Schritt, wenn der Befund einen nahelegt (z.B. ein paar Tage zur selben Uhrzeit nachmessen, oder — bei lückenhafter Einnahmetreue — die Einnahme verlässlicher machen). Bei stabilen Werten im Ziel: das ehrlich anerkennen und stattdessen einen Punkt nennen, den man im Auge behalten kann, statt einen Schritt zu erzwingen." + ], + "messages": [ + { + "role": "user", + "content": [ + "Datum: 2026-06-15", + "OPENER HINT: Eröffne damit, was das heute für den Nutzer bedeutet, und dann die Zahl dahinter.", + "Schreibe eine kurze Einschätzung zum Blutdruck dieser Person. Beginne mit der BEDEUTUNG in klaren Worten — dem Gesamteindruck, nicht der Zahl (z. B. \"ruhig und genau da, wo du ihn haben willst\", \"diese Woche einen Tick höher als sonst\") — und bring danach die systolischen/diastolischen Werte als Beleg, als EIN Paar beurteilt und gegen die eigene Wochen-/Monats-Baseline eingeordnet; führe nie mit den Werten. Schließe nur dann mit einem machbaren Schritt, wenn der Befund wirklich einen hergibt; ist nichts umsetzbar, lass den Schritt weg statt Fülltext zu erfinden. Konfidenz aus Messanzahl und Aktualität ableiten.", + "", + "VORHERIGE ANALYSE: keine vorhanden. Behandle dies als erste Analyse in diesem Bereich — kein Verbesserungs-/Verschlechterungs-Delta zu nennen.", + "", + "", + "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der jüngsten Richtung (steigend / fallend / flach gegenüber der Baseline) eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", + "", + "DATENLAGE: 0 Tages-Buckets im Fenster, kein datierter Wert. — zu wenige Punkte für einen belastbaren Trend; entsprechend vorsichtig formulieren, keine Richtung behaupten.", + "", + "", + "{\"locale\":\"de\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"blood_pressure\",\"dataCoverage\":{\"totalMeasurements\":0,\"totalSpanDays\":0,\"newestMeasurementDaysAgo\":null},\"bloodPressure\":{\"systolic\":{\"summary\":null,\"series\":{\"recent\":[],\"weekly\":[],\"monthly\":[],\"yearly\":[]}},\"diastolic\":{\"summary\":null,\"series\":{\"recent\":[],\"weekly\":[],\"monthly\":[],\"yearly\":[]}},\"paired\":{\"summary\":null,\"series\":[]},\"targets\":null},\"weightVsSystolic\":{\"correlation\":null,\"pairs\":[]},\"bpMedicationContinuityVsSystolic\":{\"expectedIntakesPerDay\":0,\"medicationCount\":0,\"correlation\":null,\"series\":[]},\"bpMedications\":[],\"moodContext\":null}" + ] + } + ], + "temperature": 0.45, + "maxTokens": 250, + "seed": 1618033, + "responseFormat": "json", + "timeoutMs": 60000 + } + ], + "enqueued": [], + "result": { + "hasProvider": false, + "text": "Für diese Karte liegt gerade keine Einschätzung vor. Blutdruck wird lesbar über eine Reihe ruhiger Messungen unter vergleichbaren Bedingungen — die Richtung über mehrere Tage sagt weit mehr als ein einzelner Ausreißer, und systolisch und diastolisch gehören dabei zusammen gelesen.", + "cached": true, + "updatedAt": null + }, + "notes": [ + { + "userId": "parity-bare", + "metric": "blood-pressure", + "locale": "de", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "error" + }, + { + "userId": "parity-bare", + "metric": "blood-pressure", + "locale": "en", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "error" + }, + { + "userId": "parity-rich", + "metric": "blood-pressure", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "42fa0a3529e963e60122eb23f56d6c9fb88ec4598e781c0bd0c8c6ed4e7b4c60", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "blood-pressure", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "940846f9f59aeba3a96f517addf9d0af913b22b14ee8417d452e7cc8445520c7", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "blood-pressure", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "blood-pressure", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + } + ] + }, + "keyless-en": { + "calls": [], + "enqueued": [], + "result": { + "hasProvider": false, + "text": "Steady and much as usual. Your blood pressure is at 127 right now — higher than your usual average of 127. That sits in your usual range — nothing you need to act on right now.", + "cached": true, + "updatedAt": null + }, + "notes": [ + { + "userId": "parity-bare", + "metric": "blood-pressure", + "locale": "de", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "error" + }, + { + "userId": "parity-bare", + "metric": "blood-pressure", + "locale": "en", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "error" + }, + { + "userId": "parity-rich", + "metric": "blood-pressure", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "42fa0a3529e963e60122eb23f56d6c9fb88ec4598e781c0bd0c8c6ed4e7b4c60", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "blood-pressure", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "940846f9f59aeba3a96f517addf9d0af913b22b14ee8417d452e7cc8445520c7", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "blood-pressure", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "blood-pressure", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + } + ] + }, + "keyless-de": { + "calls": [], + "enqueued": [], + "result": { + "hasProvider": false, + "text": "Stabil und wie gewohnt. Dein Blutdruck liegt aktuell bei 127 — höher als dein üblicher Schnitt von 127. Das ist in deinem gewohnten Rahmen — nichts, worauf du jetzt reagieren musst.", + "cached": true, + "updatedAt": null + }, + "notes": [ + { + "userId": "parity-bare", + "metric": "blood-pressure", + "locale": "de", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "error" + }, + { + "userId": "parity-bare", + "metric": "blood-pressure", + "locale": "en", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "error" + }, + { + "userId": "parity-rich", + "metric": "blood-pressure", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "42fa0a3529e963e60122eb23f56d6c9fb88ec4598e781c0bd0c8c6ed4e7b4c60", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "blood-pressure", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "940846f9f59aeba3a96f517addf9d0af913b22b14ee8417d452e7cc8445520c7", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "blood-pressure", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "blood-pressure", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + } + ] + }, + "keyless-read-only": { + "calls": [], + "enqueued": [], + "result": { + "hasProvider": false, + "text": "No assessment on this one right now. Blood pressure becomes readable over a run of calm readings under similar conditions — the direction across several days says far more than any single outlier, and systolic and diastolic are read together rather than one at a time.", + "cached": true, + "updatedAt": null + }, + "notes": [ + { + "userId": "parity-bare", + "metric": "blood-pressure", + "locale": "de", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "error" + }, + { + "userId": "parity-bare", + "metric": "blood-pressure", + "locale": "en", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "error" + }, + { + "userId": "parity-rich", + "metric": "blood-pressure", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "42fa0a3529e963e60122eb23f56d6c9fb88ec4598e781c0bd0c8c6ed4e7b4c60", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "blood-pressure", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "940846f9f59aeba3a96f517addf9d0af913b22b14ee8417d452e7cc8445520c7", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "blood-pressure", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "blood-pressure", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + } + ] + } +} diff --git a/tests/integration/__golden__/status-parity/bmi.json b/tests/integration/__golden__/status-parity/bmi.json new file mode 100644 index 000000000..8c3705c05 --- /dev/null +++ b/tests/integration/__golden__/status-parity/bmi.json @@ -0,0 +1,1087 @@ +{ + "read-only-cold": { + "calls": [], + "enqueued": [ + { + "userId": "parity-rich", + "metric": "bmi", + "locale": "en" + } + ], + "result": { + "hasProvider": true, + "text": null, + "cached": false, + "updatedAt": null, + "preparing": true, + "revalidating": false + }, + "notes": [] + }, + "fresh-en": { + "calls": [ + { + "system": [ + "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", + "", + "YOUR DATA (graded snapshot):", + "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", + "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", + "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", + "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", + "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", + "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", + "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", + "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", + "", + "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", + "Moves available (choose the few that genuinely apply):", + "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", + "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", + "(c) name a genuine, earned win or a steady streak when the data shows one;", + "(d) one watch-item worth keeping an eye on;", + "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", + "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", + "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", + "", + "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", + "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", + "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", + "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", + "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", + "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", + "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", + "", + "OPENING — lead with meaning, not the metric value", + "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", + "", + "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", + "", + "JUDGE THE DATA HONESTLY (never invent a trend):", + "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", + "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", + "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", + "", + "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", + "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", + "", + "METRIC IDENTIFIERS STAY OUT OF PROSE", + "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", + "", + "GLP-1 DOSE SAFETY (contract, not style)", + "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", + "", + "ACUTE RED FLAGS (contract, not style)", + "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", + "", + "GROUNDING — ZERO HALLUCINATIONS", + "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", + "", + "TONE — a warm, motivating advisor, never clinical or alarming", + "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", + "", + "NOT AN INSIGHT — never recite mechanics", + "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", + "", + "INTERPRET, DON'T ENUMERATE", + "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", + "", + "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", + "", + "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", + "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", + "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", + "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", + "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", + "", + "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in English: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", + "{ \"summary\": \"...\" }", + "", + "METRIC — BMI:", + "- The snapshot carries bmi.signal (the finished comparison) + bmi.summary + bmi.series (graded). bmi.latestDayFocus shows the latest value, its WHO classification and the step from the prior day; bmi.target is the green band (18.5-24.9).", + "- This card's focus is WHO-BAND placement, not weight pace (the weight card carries that). Centre it on which band the value sits in and whether it crosses a band or nears a band boundary.", + "- WHO bands for rough placement, never as a label for the person: Underweight < 18.5, Normal 18.5-24.9, Overweight 25.0-29.9, Obesity I 30.0-34.9, II 35.0-39.9, III ≥ 40.0. From age 65+, 22-27 counts as favourable (DEGAM).", + "- Band reference: lead from bmi.signal — current, delta vs baselineLabel and placement (band position) are already computed; STATE them, do NOT recompute. Call it out explicitly when the recent value has crossed a WHO band or is nearing a band boundary (within ~0.5 BMI points). When the value stays safely inside the same band, say so briefly — do not force a pace narrative.", + "- Name BMI's limits plainly: it does not measure body composition and is weakly informative for muscular or very athletic people.", + "- One message: close with ONE doable step that fits the band placement ONLY when the finding implies one — when the value is stable in the favourable band, acknowledge that honestly and name one thing worth keeping an eye on rather than forcing a finding or a step." + ], + "messages": [ + { + "role": "user", + "content": [ + "Date: 2026-06-15", + "OPENER HINT: Open with what this means for the user today, then the number behind it.", + "Write one short assessment of this person's BMI. Open with what its band placement MEANS in plain words — where it sits and whether that's holding or shifting, not the number (e.g. \"sitting comfortably in the healthy band\", \"edging toward the next band up\") — then bring in the current value and WHO band right after as support, saying against their own weekly/monthly baseline whether it has crossed a band or is nearing a boundary; never lead with the value. Close with one doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Leave the weight pace to the weight card. Judge confidence from the measurement count and recency.", + "", + "PREVIOUS ANALYSIS: none on file. Treat this as the user's first analysis for this domain — no improvement/regression delta to surface.", + "", + "", + "VARIETY: to keep your cards from reading the same, you may open this one by leading with the recent direction of travel (rising / falling / flat vs the baseline) — only if the data genuinely supports that angle; never force it, never invent a trend.", + "", + "DATA STRENGTH: 70 day-buckets in the window, latest reading today.", + "", + "REPETITION: you have already given a similar \"no material change\" assessment for this metric 10 time(s) in a row. Do NOT restate the same level again. Acknowledge the continuity in one short clause (\"still holding steady, 11 checks running\") and then pivot to a DIFFERENT facet — consistency, time-of-day pattern, or a correlated metric — or, if nothing is genuinely actionable, say so plainly and skip the manufactured step.", + "", + "", + "{\"locale\":\"en\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"bmi\",\"dataCoverage\":{\"totalMeasurements\":70,\"totalSpanDays\":138,\"newestMeasurementDaysAgo\":0},\"bmi\":{\"signal\":{\"metric\":\"your BMI\",\"current\":26.57,\"currentWindowDays\":11,\"baseline\":26.06,\"baselineLabel\":\"your monthly average\",\"delta\":0.51,\"deltaPct\":2,\"spread\":0.1,\"outsideNormalSwing\":true,\"direction\":\"target-band\",\"n\":11,\"newestDaysAgo\":0,\"normalRange\":{\"low\":18.5,\"high\":24.9},\"placement\":\"above band\"},\"summary\":{\"points\":70,\"start\":26.57,\"end\":26,\"delta\":-0.57,\"mean\":26.26,\"min\":25.89,\"max\":26.66},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":26.39,\"max\":26.39,\"mean\":26.39,\"n\":1},{\"date\":\"2026-05-28\",\"min\":26.43,\"max\":26.43,\"mean\":26.43,\"n\":1},{\"date\":\"2026-05-30\",\"min\":26.49,\"max\":26.49,\"mean\":26.49,\"n\":1},{\"date\":\"2026-06-01\",\"min\":26.55,\"max\":26.55,\"mean\":26.55,\"n\":1},{\"date\":\"2026-06-03\",\"min\":26.6,\"max\":26.6,\"mean\":26.6,\"n\":1},{\"date\":\"2026-06-05\",\"min\":26.64,\"max\":26.64,\"mean\":26.64,\"n\":1},{\"date\":\"2026-06-07\",\"min\":26.66,\"max\":26.66,\"mean\":26.66,\"n\":1},{\"date\":\"2026-06-09\",\"min\":26.66,\"max\":26.66,\"mean\":26.66,\"n\":1},{\"date\":\"2026-06-11\",\"min\":26.65,\"max\":26.65,\"mean\":26.65,\"n\":1},{\"date\":\"2026-06-13\",\"min\":26.62,\"max\":26.62,\"mean\":26.62,\"n\":1},{\"date\":\"2026-06-15\",\"min\":26.57,\"max\":26.57,\"mean\":26.57,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":26.04,\"max\":26.05,\"mean\":26.04,\"n\":3},{\"weekISO\":\"2026-W13\",\"min\":26.06,\"max\":26.22,\"mean\":26.14,\"n\":4},{\"weekISO\":\"2026-W14\",\"min\":26.28,\"max\":26.36,\"mean\":26.32,\"n\":3},{\"weekISO\":\"2026-W15\",\"min\":26.3,\"max\":26.37,\"mean\":26.34,\"n\":4},{\"weekISO\":\"2026-W16\",\"min\":26.2,\"max\":26.26,\"mean\":26.23,\"n\":3},{\"weekISO\":\"2026-W17\",\"min\":26.19,\"max\":26.27,\"mean\":26.22,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":26.33,\"max\":26.44,\"mean\":26.38,\"n\":3},{\"weekISO\":\"2026-W19\",\"min\":26.48,\"max\":26.52,\"mean\":26.51,\"n\":4},{\"weekISO\":\"2026-W20\",\"min\":26.4,\"max\":26.48,\"mean\":26.44,\"n\":3},{\"weekISO\":\"2026-W21\",\"min\":26.34,\"max\":26.36,\"mean\":26.35,\"n\":4}],\"monthly\":[{\"month\":\"2026-01\",\"min\":26,\"max\":26.04,\"mean\":26.02,\"n\":2},{\"month\":\"2026-02\",\"min\":25.89,\"max\":26.11,\"mean\":25.99,\"n\":14},{\"month\":\"2026-03\",\"min\":26.09,\"max\":26.22,\"mean\":26.17,\"n\":8}],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":0,\"value\":26.57,\"classification\":{\"category\":\"Overweight\",\"color\":\"var(--warning)\",\"severity\":\"warning\"},\"deltaToPreviousDailyPoint\":-0.04},\"target\":{\"greenMin\":18.5,\"greenMax\":24.9}}}" + ] + } + ], + "temperature": 0.45, + "maxTokens": 250, + "seed": 1618033, + "responseFormat": "json", + "timeoutMs": 60000 + } + ], + "enqueued": [], + "result": { + "hasProvider": true, + "text": "Parity note for fresh-en.", + "cached": false, + "updatedAt": "2026-06-15T10:00:00.000Z" + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "bmi", + "locale": "en", + "text": "Parity note for fresh-en.", + "items": null, + "inputHash": "c23e6b9fee3631b156b1bfbbb0d1668204963e887a91596cddeed398b3846bdb", + "snapshotHash": "aa09825f5e5cda0d0e0ffdb1c75004ea0b298a0773d065ddef3a682bda8a6918", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + } + ] + }, + "cached-en": { + "calls": [], + "enqueued": [], + "result": { + "hasProvider": true, + "text": "Parity note for fresh-en.", + "cached": true, + "updatedAt": "2026-06-15T10:00:00.000Z" + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "bmi", + "locale": "en", + "text": "Parity note for fresh-en.", + "items": null, + "inputHash": "c23e6b9fee3631b156b1bfbbb0d1668204963e887a91596cddeed398b3846bdb", + "snapshotHash": "aa09825f5e5cda0d0e0ffdb1c75004ea0b298a0773d065ddef3a682bda8a6918", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + } + ] + }, + "forced-unchanged-en": { + "calls": [], + "enqueued": [], + "result": { + "hasProvider": true, + "text": "Parity note for fresh-en.", + "cached": true, + "updatedAt": "2026-06-15T10:00:00.000Z" + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "bmi", + "locale": "en", + "text": "Parity note for fresh-en.", + "items": null, + "inputHash": "c23e6b9fee3631b156b1bfbbb0d1668204963e887a91596cddeed398b3846bdb", + "snapshotHash": "aa09825f5e5cda0d0e0ffdb1c75004ea0b298a0773d065ddef3a682bda8a6918", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + } + ] + }, + "next-day-en": { + "calls": [], + "enqueued": [], + "result": { + "hasProvider": true, + "text": "Parity note for fresh-en.", + "cached": true, + "updatedAt": "2026-06-16T10:00:00.000Z" + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "bmi", + "locale": "en", + "text": "Parity note for fresh-en.", + "items": null, + "inputHash": "c23e6b9fee3631b156b1bfbbb0d1668204963e887a91596cddeed398b3846bdb", + "snapshotHash": "aa09825f5e5cda0d0e0ffdb1c75004ea0b298a0773d065ddef3a682bda8a6918", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + } + ] + }, + "read-only-stale": { + "calls": [], + "enqueued": [ + { + "userId": "parity-rich", + "metric": "bmi", + "locale": "en" + } + ], + "result": { + "hasProvider": true, + "text": "Parity note for fresh-en.", + "cached": true, + "updatedAt": "2026-06-16T10:00:00.000Z", + "preparing": false, + "revalidating": true + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "bmi", + "locale": "en", + "text": "Parity note for fresh-en.", + "items": null, + "inputHash": "c23e6b9fee3631b156b1bfbbb0d1668204963e887a91596cddeed398b3846bdb", + "snapshotHash": "aa09825f5e5cda0d0e0ffdb1c75004ea0b298a0773d065ddef3a682bda8a6918", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + } + ] + }, + "prose-de": { + "calls": [ + { + "system": [ + "Du schreibst eine kurze, persönliche Einschätzung zu EINER Gesundheitsmetrik dieses Nutzers. Du kennst seine Daten und ordnest sie gegen seine EIGENE Baseline ein — nicht gegen Bevölkerungsnormen. Deine fachliche Grundlage sind anerkannte Leitlinien (ESH 2023, WHO, AASM, DGE, DEGAM); du beziehst dich darauf, ohne zu diagnostizieren.", + "", + "DEINE DATENGRUNDLAGE (graded snapshot):", + "- signal: der fertige Vergleich — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` ist das recent-Mittel; `baseline` ist der EIGENE längere Durchschnitt; `delta`/`deltaPct` sind die VORZEICHENBEHAFTETE Änderung gegenüber dieser Baseline; `spread` ist die normale Schwankung; `outsideNormalSwing` ist das vorab berechnete Urteil, ob die Änderung diese Schwankung übersteigt. Diese Werte sind bereits berechnet — nenne sie, rechne sie NICHT neu. `contributors[]` (nur Score-Komposite) benennt die 1–2 Teil-Scores, die den Score bewegt haben.", + "- summary: { points, start, end, delta, mean, min, max } — die Eckwerte des gesamten Lesefensters.", + "- series.recent[]: die jüngsten ~21 Tage, je ein Eintrag pro Tag ({ date, min, max, mean, n }).", + "- series.weekly[]: die ~10 Wochen davor, je ein ISO-Wochen-Aggregat.", + "- series.monthly[]: die ~12 Monate davor, je ein Monatsaggregat.", + "- series.yearly[]: alles ältere, je ein Jahr ({ year, mean, min, max, n, slope }).", + "- Die jüngsten Tage liegen also EINZELN vor, ältere Zeiträume als Wochen-, Monats- und Jahresaggregate. Ziehe die series nur für einen konkreten Tag heran; führe sonst mit dem signal-Block.", + "- Ein Slice kann leer sein, wenn die Historie kurz ist. Dann nicht so tun, als gäbe es ihn. Ist `signal.baseline` null, gibt es noch keine etablierte Baseline; sage das ehrlich, statt einen Vergleich zu erfinden.", + "", + "SO BAUST DU DIE EINSCHÄTZUNG — wähle die 2–3 BAUSTEINE, die zu DIESEN Daten passen, und ordne sie so, dass sie als EIN zusammenhängender Gedanke lesen, keine feste Checkliste. Führe gemäß dem OPENER-HINWEIS im User-Prompt, wenn einer mitgegeben ist — eröffne NICHT immer mit der Zahl. Variiere die Form von Metrik zu Metrik; eröffne nie zwei Metriken hintereinander gleich.", + "Verfügbare Bausteine (nimm die wenigen, die wirklich zutreffen):", + "(a) der aktuelle Befund — `current` aus dem signal-Block (oder ein Wert aus series.recent für eine Tagesfrage);", + "(b) gegen die EIGENE Baseline einordnen über das vorab berechnete vorzeichenbehaftete `delta` gegenüber `baselineLabel` — nenne es, leite es nicht neu her. Behandle `outsideNormalSwing: false` als \"innerhalb der üblichen Schwankung — kein Befund\" und ERFINDE KEINEN Trend; `outsideNormalSwing: true` ist die echte, meldenswerte Änderung. Ein `normalRange`/`placement` ist nur ein grober sekundärer Anker — das persönliche delta führt;", + "(c) einen echten, verdienten Erfolg oder eine stabile Serie benennen, wenn die Daten ihn zeigen;", + "(d) einen Punkt zum Im-Auge-behalten;", + "(e) einen Zusammenhang NUR, wenn ein r-Wert vorhanden und |r| > 0,4 ist, formuliert als \"bewegt sich mit\" / \"zeigt sich oft zusammen mit\", nie kausal;", + "(f) GENAU EINEN machbaren nächsten Schritt — und nur, wenn der Befund wirklich einen hergibt. Eine Botschaft = ein Verhalten, keine Liste. Ist der Wert stabil und im grünen Bereich, ERZWINGE KEINEN Schritt — bestätige kurz und nenne stattdessen einen Punkt zum Beobachten. Ein erfundener Schritt ist genau die Floskel, die wir vermeiden.", + "Synthese statt Aufzählung: die Geschichte dessen, was die Daten bedeuten, zählt mehr als das Wiederholen jeder Zahl.", + "", + "TONALITÄT — ein warmer, motivierender Premium-Begleiter (jemand hat sich die Daten dieser Person wirklich angesehen und etwas Wertvolles für sie gefunden):", + "- Zweite Person (\"dein Blutdruck\", \"deine Werte\"), warm, direkt, ehrlich.", + "- Motivierend und vorwärtsgewandt: Wenn die Daten es hergeben, benenne den echten Erfolg klar und baue ein wenig Schwung auf — die Person soll sich gesehen und unterstützt fühlen, nicht belehrt. Die Ermutigung muss durch die Zahlen VERDIENT sein, nie ein reflexhaftes Kompliment.", + "- Autonomie-unterstützend: \"kann helfen\", \"einen Versuch wert\", nie \"du musst\".", + "- Nie alarmierend, nie moralisierend, nie diagnostisch — keine Krankheitsbehauptung. Formuliere als begründete Beobachtung, nicht als ärztlichen Rat.", + "- Auch ungünstige Werte ehrlich benennen: Befund → gegen die eigene Baseline einordnen → ein kleiner machbarer Schritt, als Chance formuliert, nicht als Versagen. Nicht verharmlosen, nicht dramatisieren.", + "- Keine Floskeln und keine bloße Zahlenwiederholung: jede warme Zeile ist an eine echte Zahl oder eine echte Veränderung geknüpft. Ein generischer Positiv-Einstieg (\"Deine Werte sehen gut aus\") ist verboten — verdiene die Ermutigung mit dem konkreten Befund.", + "", + "ERÖFFNUNG — mit der Bedeutung führen, nicht mit dem Messwert", + "Beginne mit dem Eindruck in klaren Worten — was das heute über die Person sagt — und bring die Zahl direkt danach als Beleg, nie als Erstes. Eine Zeile, die mit einem Wert eröffnet (\"Dein Ruhepuls ist 61 …\"), liest sich wie ein Ablesen; dieselbe Zeile, die mit der Bedeutung eröffnet (\"Dein Herz läuft diese Woche etwas ruhiger — rund 61 …\"), liest sich wie ein Begleiter, der wirklich hingesehen hat. Jede Aussage bleibt an einer echten Zahl verankert; nur die REIHENFOLGE ändert sich, Bedeutung zuerst und die Zahl direkt dahinter. Das gilt, ob die Fläche eine Metrik abdeckt oder viele.", + "", + "LÄNGE: 2-4 Sätze, ca. 30-60 Wörter — ist der Wert aber stabil und nichts zu tun, ist EIN knapper Satz besser, als ihn auf Länge zu strecken. Knapp und hochwertig. Keine bloße Zahlenwiederholung, kein Fülltext, keine generischen Floskeln.", + "", + "DATENLAGE EHRLICH BEWERTEN (nie einen Trend erfinden):", + "- Nur wenige Messpunkte/Einträge/Ereignisse oder ein leerer recent-Slice: ehrlich sagen, dass es für eine belastbare Tendenz noch zu wenige Daten sind, und EINEN Hinweis geben (z.B. regelmäßiger erfassen). Keinen Trend behaupten. Was \"wenige\" bedeutet, hängt von der Metrik ab — bei Stimmung sind es Einträge, bei der Einnahmetreue geplante Dosen.", + "- Jüngste Messung deutlich älter als ~7 Tage (dataCoverage.newestMeasurementDaysAgo): darauf hinweisen, dass die Werte nicht mehr aktuell sind.", + "- Korrelationen NUR erwähnen, wenn der r-Wert im Snapshot vorhanden ist und |r| > 0.4 ist. Fehlt das Feld, keine Korrelation interpretieren oder erfinden. Immer als \"Zusammenhang\" formulieren, nie als \"Ursache\".", + "", + "VERBOTENE FLOSKELN (signalisieren ungegroundeten Fülltext — nie ausgeben, außer im Disclaimer)", + "\"achte auf ausreichend Schlaf\", \"trinke genug Wasser\", \"regelmäßige Bewegung\", \"ärztlicher Rat empfohlen\".", + "", + "METRIK-IDENTIFIER GEHÖREN NICHT IN DEN FLIESSTEXT", + "Schreibe niemals Datenbank- bzw. Enum-Namen wie \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" oder \"SLEEP_DURATION\" in nutzersichtbare Strings, und schreibe nie das wörtliche \"metric:\"-Chart-Token in den Fließtext. Verweise auf jede Metrik mit der natürlichsprachlichen Bezeichnung, die der Nutzer sieht — \"deine Systole\", \"dein Gewicht\", \"dein Puls\", \"deine Stimmung\", \"deine Medikamentenadhärenz\", \"dein Ruhepuls\", \"deine Schlafdauer\", \"deine Schritte\".", + "", + "GLP-1-DOSIS-SICHERHEIT (Vertrag, kein Stil)", + "Verschreibe, empfiehl oder ändere NIEMALS Medikamenten-Dosen, auch wenn der Snapshot einen GLP-1-Rezeptoragonisten benennt (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). Du darfst den benannten Wirkstoff und die aktuelle Titrationsstufe NENNEN (\"Woche 3 auf 7,5 mg\"), wenn der Snapshot sie trägt, aber schreibe nie \"erhöhe auf X mg\", \"erwäge die nächste Stufe Y mg\", \"bleibe auf Z mg\" oder eine Variante davon. Ein Plateau rahmt die nächste Entscheidung immer als Gespräch mit der behandelnden Ärztin. Bist du unsicher, ob etwas dosis-präskriptiv ist, behandle es so — und verweise an die Klinik.", + "", + "AKUTE WARNZEICHEN (Vertrag, kein Stil)", + "Beschreibt der Nutzer ein akutes Krisenzeichen — Brustschmerz oder Druck auf der Brust, Ohnmacht oder Beinahe-Ohnmacht (Synkope), ein plötzliches schweres Symptom (z. B. stärkster Kopfschmerz aller Zeiten, plötzliche Schwäche oder Sprachstörung, Atemnot), einen hypertensiven Notfallwert (systolisch ≥ 180 oder diastolisch ≥ 120 mit Symptomen), einen schwer-niedrigen oder sehr hohen Glukosewert (unter 54 mg/dL (3,0 mmol/L), oder unter 70 mg/dL (3,9 mmol/L) mit Symptomen, oder ≥ 250 mg/dL (13,9 mmol/L) mit Symptomen), anhaltendes Fieber ≥ 38,5 °C (101,3 °F) oder Gedanken an Suizid bzw. Selbstverletzung — sage in einem ruhigen Satz klar, dass das jetzt umgehende ärztliche Hilfe oder den Notruf braucht, und coache es NICHT anhand der Daten. Diagnostiziere nicht, benenne keine Erkrankung, schätze keinen Schweregrad aus den Zahlen — verweise nur auf umgehende Hilfe bzw. den Notruf und höre auf. Das ist eine geschlossene Liste; außerhalb davon bleibe wie gewohnt ruhig und nicht alarmierend.", + "", + "ERDUNG — NULL HALLUZINATIONEN", + "Jede Aussage muss auf einer Zahl beruhen, die im übergebenen Snapshot sichtbar ist. Lässt sich die Aussage keinem Snapshot-Feld zuordnen, lass sie weg und erfinde nie einen Messwert, um einer Anfrage zu entsprechen. Ordne jeden Wert gegen die EIGENE Baseline des Nutzers ein (jüngeres vs. längeres Mittel), nie gegen eine Bevölkerungsnorm. Nenne einen Zusammenhang nur, wenn der r-Wert vorhanden und |r| > 0,4 ist — immer als \"Zusammenhang\", nie als \"Ursache\".", + "", + "TONALITÄT — ein warmer, motivierender Begleiter, nie klinisch oder alarmierend", + "Schreibe in der zweiten Person, warm und direkt. Wenn die Daten es hergeben, benenne den echten Erfolg klar und baue ein wenig Schwung auf — die Person soll sich gesehen und unterstützt fühlen, nicht belehrt. Die Ermutigung muss durch die Zahlen VERDIENT sein, nie ein reflexhaftes Kompliment. Lob ist verankert, konkret und angemessen: an einer echten Zahl oder Veränderung festgemacht, nicht auf andere Nutzer übertragbar, und stiller Anerkennung für stille Erfolge. Über-Bestätigung ist eine Sicherheitsregression, keine Wärme — bestätige nie einen sich verschlechternden Trend oder eine unsichere Entscheidung; würdige die Mühe, nicht die Entscheidung, und bleib neutral (\"nichts zu tun — die gute Art von langweilig\"), wenn es nichts zu loben gibt, statt ein Kompliment zu erfinden. Sei autonomie-unterstützend (\"einen Versuch wert\", \"kann helfen\", nie \"du musst\"). Benenne auch ungünstige Werte ehrlich — Befund, dann gegen die eigene Baseline einordnen, dann ein kleiner machbarer Schritt, als Chance formuliert. Nie alarmierend, nie moralisierend, nie diagnostisch. Keine Floskeln und keine bloße Zahlenwiederholung. Beginne NICHT mit einem Kompliment über Datenmenge oder -qualität; verbotene Eröffnungen sind u.a. \"Datengrundlage ist sehr stark\", \"Your data foundation is strong\", \"Du hast eine solide Baseline\", \"Großartiger Datensatz\", ein generisches \"Deine Werte sehen gut aus\" und jede sinngemäße Umformulierung.", + "", + "KEINE EINSCHÄTZUNG — nie Mechanik nacherzählen", + "Eine Messanzahl (\"3-mal erfasst\"), ein Erfassungsrhythmus (\"alle paar Tage gemessen\") oder Bedien-Mechanik ist KEINE Einschätzung und gehört für sich genommen nicht in den Text. Nutze eine solche Angabe NUR, wenn sie eine Konsequenz trägt, und nenne dann die Konsequenz, nicht die Zahl: z. B. \"diese Woche zu wenige Werte für eine Tendenz — eine Morgenmessung würde es klären\", nicht \"du hast diese Woche zweimal gemessen\". Jeder Satz muss sich verdienen, indem er sagt, was ein Wert BEDEUTET oder was daraus folgt — nie, indem er erzählt, wie die Daten erhoben wurden.", + "", + "EINORDNEN, NICHT AUFZÄHLEN", + "Sag, was ein Wert BEDEUTET, nicht nur, was er ist. Trägt der User-Prompt einen EINORDNUNGS-KONTEXT-Block, nutze ihn: benenne, wo der Wert auf der Leitlinien-Skala liegt, was dieses Band in klaren, würdevollen Worten bedeutet und was praktisch folgt. Beurteile den Trend IM KONTEXT — ein \"leicht steigender\" Wert tief in einem günstigen Band ist eine Randnotiz; dieselbe Bewegung an oder nahe einer Grenze ist die Schlagzeile. Rahme jede Konsequenz OHNE Diagnose: \"Werte in diesem Bereich gelten laut Leitlinien als …\", nie \"du hast X\" oder \"du bist gefährdet für X\". Ist KEIN Einordnungs-Block vorhanden, hat die Metrik kein allgemeines Referenzband — ordne sie gegen die EIGENE Baseline der Person ein und sage klar, dass kein allgemeines Referenzband hinterlegt ist, statt eines zu erfinden.", + "", + "FORMATIERUNG — schreibe in kurzen Absätzen, getrennt durch eine LEERZEILE, je 1–3 Sätze; eine längere Antwort hat 2–4 Absätze, nie einen Block. Setze eine Leerzeile zwischen unterschiedliche Gedanken (z. B. \"wie es steht\" vs. \"eine Sache zum Ausprobieren\"). Ein stabiler Einzeiler bleibt EIN Absatz — strecke nicht auf einen zweiten. Wenn du wirklich drei oder mehr parallele Punkte aufzählst (Optionen, Befunde, Schritte), formatiere sie als schlichte Liste: ein Punkt pro Zeile, jede Zeile beginnt mit \"- \". Die EINE wichtigste Kernaussage darfst du mit **doppelten Sternchen** fett setzen, höchstens einmal pro Antwort. Sonst nichts: keine Überschriften, kein Kursiv, keine Backticks, keine nummerierten Listen, keine Emojis.", + "", + "BEISPIELE — beachte die UNTERSCHIEDLICHEN Formen (urteil-zuerst, trend-zuerst, Einzeiler). Sie illustrieren nur Form und Erdung; übernimm sie nie — jede Einschätzung nutzt die echten Snapshot-Zahlen:", + "- URTEIL-ZUERST (Bedeutung zuerst, Zahl als Beleg, ein Schritt): \"Dein Ruhepuls läuft gerade einen Tick niedriger als sonst — diese Woche rund 61 bpm, 5 unter deinem Monatsmittel von 66 und dein niedrigster seit Wochen. So ein Rückgang passt meist zu mehr Bewegung; die Routine, die das gebracht hat, lohnt sich beizubehalten.\"", + "- TREND-ZUERST (Richtung zuerst, dann der Stand): \"Dein systolischer Blutdruck geht seit drei Wochen ruhig nach unten, aktuell rund 124 mmHg — etwa 4 unter deinem 30-Tage-Schnitt. Nichts Dramatisches, einfach eine stetige Richtung.\"", + "- EINZEILER (stabil, KEIN erzwungener Schritt): \"Deine SpO₂ ist mit 97 % stabil und genau in deinem üblichen Bereich — nichts zu tun, die gute Art von langweilig.\"", + "- SCHLECHT (verbotene Floskel, ungegroundet — so NICHT): \"Deine Werte sehen gut aus. Achte auf ausreichend Schlaf und regelmäßige Bewegung.\"", + "", + "AUSGABEFORMAT: Antworte ausschließlich mit validem JSON in genau diesem Schema. Das Feld \"summary\" enthält die komplette Einschätzung auf Deutsch: 1-3 kurze Absätze mit je 1-3 Sätzen, getrennt durch eine Leerzeile als \\n\\n INNERHALB des JSON-Strings. Ein stabiler Einzeiler bleibt EIN Absatz:", + "{ \"summary\": \"...\" }", + "", + "METRIK — BMI:", + "- Der Snapshot trägt bmi.signal (der fertige Vergleich) + bmi.summary + bmi.series (graded). bmi.latestDayFocus zeigt den jüngsten Wert, dessen WHO-Klassifikation und den Schritt zum Vortag; bmi.target ist das grüne Band (18.5-24.9).", + "- Fokus dieser Karte ist die WHO-BAND-EINORDNUNG, nicht das Gewichtstempo (das trägt die Gewichts-Karte). Setze den Schwerpunkt darauf, in welchem Band der Wert liegt und ob er ein Band wechselt oder sich einer Bandgrenze nähert.", + "- WHO-Bänder zur groben Einordnung, nie als Etikett für die Person: Untergewicht < 18.5, Normal 18.5-24.9, Übergewicht 25.0-29.9, Adipositas I 30.0-34.9, II 35.0-39.9, III ≥ 40.0. Ab 65+ gilt 22-27 als günstig (DEGAM).", + "- Bandbezug: Führe mit bmi.signal — current, delta gegenüber baselineLabel und placement (Bandlage) sind bereits berechnet; NENNE sie, rechne sie NICHT neu. Nenne es ausdrücklich, wenn der recent-Wert ein WHO-Band gewechselt hat oder sich (innerhalb ~0.5 BMI-Punkte) einer Bandgrenze nähert. Bleibt der Wert sicher im selben Band, sage das knapp — keine Tempo-Erzählung erzwingen.", + "- Grenzen des BMI offen ansprechen: Er misst keine Körperzusammensetzung und ist bei muskulösen oder sehr sportlichen Personen wenig aussagekräftig.", + "- Eine Botschaft: Schließe NUR DANN mit EINEM machbaren Schritt, der zur Bandlage passt, wenn der Befund einen nahelegt — bei stabilem Wert im günstigen Band das ehrlich anerkennen und stattdessen einen Punkt nennen, den man im Auge behalten kann, statt einen Schritt zu erzwingen." + ], + "messages": [ + { + "role": "user", + "content": [ + "Datum: 2026-06-15", + "OPENER HINT: Eröffne damit, was das heute für den Nutzer bedeutet, und dann die Zahl dahinter.", + "Schreibe eine kurze Einschätzung zum BMI dieser Person. Beginne mit der BEDEUTUNG der Bandlage in klaren Worten — wo er liegt und ob das hält oder sich verschiebt, nicht der Zahl (z. B. \"liegt bequem im gesunden Band\", \"nähert sich dem nächsten Band\") — und bring danach den aktuellen Wert und das WHO-Band als Beleg, samt Angabe gegen die eigene Wochen-/Monats-Baseline, ob er ein Band gewechselt hat oder sich einer Bandgrenze nähert; führe nie mit dem Wert. Schließe nur dann mit einem machbaren Schritt, wenn der Befund wirklich einen hergibt; ist nichts umsetzbar, lass den Schritt weg statt Fülltext zu erfinden. Das Gewichtstempo bleibt der Gewichts-Karte überlassen. Konfidenz aus Messanzahl und Aktualität ableiten.", + "", + "VORHERIGE ANALYSE: keine vorhanden. Behandle dies als erste Analyse in diesem Bereich — kein Verbesserungs-/Verschlechterungs-Delta zu nennen.", + "", + "", + "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der jüngsten Richtung (steigend / fallend / flach gegenüber der Baseline) eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", + "", + "DATENLAGE: 70 Tages-Buckets im Fenster, letzter Wert heute.", + "", + "WIEDERHOLUNG: du hast für diese Metrik bereits 10-mal in Folge eine ähnliche \"keine wesentliche Änderung\"-Einschätzung gegeben. Wiederhole NICHT erneut dasselbe Niveau. Benenne die Kontinuität in einem kurzen Nebensatz (\"weiterhin stabil, 11 Checks in Folge\") und wende dich dann einem ANDEREN Aspekt zu — Konstanz, Tageszeit-Muster oder einer korrelierten Metrik — oder sage, falls nichts wirklich umsetzbar ist, dies klar und lass den erzwungenen Schritt weg.", + "", + "", + "{\"locale\":\"de\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"bmi\",\"dataCoverage\":{\"totalMeasurements\":70,\"totalSpanDays\":138,\"newestMeasurementDaysAgo\":0},\"bmi\":{\"signal\":{\"metric\":\"dein BMI\",\"current\":26.57,\"currentWindowDays\":11,\"baseline\":26.06,\"baselineLabel\":\"your monthly average\",\"delta\":0.51,\"deltaPct\":2,\"spread\":0.1,\"outsideNormalSwing\":true,\"direction\":\"target-band\",\"n\":11,\"newestDaysAgo\":0,\"normalRange\":{\"low\":18.5,\"high\":24.9},\"placement\":\"above band\"},\"summary\":{\"points\":70,\"start\":26.57,\"end\":26,\"delta\":-0.57,\"mean\":26.26,\"min\":25.89,\"max\":26.66},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":26.39,\"max\":26.39,\"mean\":26.39,\"n\":1},{\"date\":\"2026-05-28\",\"min\":26.43,\"max\":26.43,\"mean\":26.43,\"n\":1},{\"date\":\"2026-05-30\",\"min\":26.49,\"max\":26.49,\"mean\":26.49,\"n\":1},{\"date\":\"2026-06-01\",\"min\":26.55,\"max\":26.55,\"mean\":26.55,\"n\":1},{\"date\":\"2026-06-03\",\"min\":26.6,\"max\":26.6,\"mean\":26.6,\"n\":1},{\"date\":\"2026-06-05\",\"min\":26.64,\"max\":26.64,\"mean\":26.64,\"n\":1},{\"date\":\"2026-06-07\",\"min\":26.66,\"max\":26.66,\"mean\":26.66,\"n\":1},{\"date\":\"2026-06-09\",\"min\":26.66,\"max\":26.66,\"mean\":26.66,\"n\":1},{\"date\":\"2026-06-11\",\"min\":26.65,\"max\":26.65,\"mean\":26.65,\"n\":1},{\"date\":\"2026-06-13\",\"min\":26.62,\"max\":26.62,\"mean\":26.62,\"n\":1},{\"date\":\"2026-06-15\",\"min\":26.57,\"max\":26.57,\"mean\":26.57,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":26.04,\"max\":26.05,\"mean\":26.04,\"n\":3},{\"weekISO\":\"2026-W13\",\"min\":26.06,\"max\":26.22,\"mean\":26.14,\"n\":4},{\"weekISO\":\"2026-W14\",\"min\":26.28,\"max\":26.36,\"mean\":26.32,\"n\":3},{\"weekISO\":\"2026-W15\",\"min\":26.3,\"max\":26.37,\"mean\":26.34,\"n\":4},{\"weekISO\":\"2026-W16\",\"min\":26.2,\"max\":26.26,\"mean\":26.23,\"n\":3},{\"weekISO\":\"2026-W17\",\"min\":26.19,\"max\":26.27,\"mean\":26.22,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":26.33,\"max\":26.44,\"mean\":26.38,\"n\":3},{\"weekISO\":\"2026-W19\",\"min\":26.48,\"max\":26.52,\"mean\":26.51,\"n\":4},{\"weekISO\":\"2026-W20\",\"min\":26.4,\"max\":26.48,\"mean\":26.44,\"n\":3},{\"weekISO\":\"2026-W21\",\"min\":26.34,\"max\":26.36,\"mean\":26.35,\"n\":4}],\"monthly\":[{\"month\":\"2026-01\",\"min\":26,\"max\":26.04,\"mean\":26.02,\"n\":2},{\"month\":\"2026-02\",\"min\":25.89,\"max\":26.11,\"mean\":25.99,\"n\":14},{\"month\":\"2026-03\",\"min\":26.09,\"max\":26.22,\"mean\":26.17,\"n\":8}],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":0,\"value\":26.57,\"classification\":{\"category\":\"Overweight\",\"color\":\"var(--warning)\",\"severity\":\"warning\"},\"deltaToPreviousDailyPoint\":-0.04},\"target\":{\"greenMin\":18.5,\"greenMax\":24.9}}}" + ] + } + ], + "temperature": 0.45, + "maxTokens": 250, + "seed": 1618033, + "responseFormat": "json", + "timeoutMs": 60000 + } + ], + "enqueued": [], + "result": { + "hasProvider": true, + "text": "Das ist kein JSON.", + "cached": false, + "updatedAt": "2026-06-15T10:00:00.000Z" + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "bmi", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": "c23e6b9fee3631b156b1bfbbb0d1668204963e887a91596cddeed398b3846bdb", + "snapshotHash": "74b0cb1c990e8e1871e3becc55be5a8d8a6806caf3fecaccff1f1589f692267e", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "bmi", + "locale": "en", + "text": "Parity note for fresh-en.", + "items": null, + "inputHash": "c23e6b9fee3631b156b1bfbbb0d1668204963e887a91596cddeed398b3846bdb", + "snapshotHash": "aa09825f5e5cda0d0e0ffdb1c75004ea0b298a0773d065ddef3a682bda8a6918", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + } + ] + }, + "unparseable-fr": { + "calls": [ + { + "system": [ + "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", + "", + "YOUR DATA (graded snapshot):", + "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", + "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", + "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", + "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", + "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", + "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", + "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", + "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", + "", + "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", + "Moves available (choose the few that genuinely apply):", + "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", + "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", + "(c) name a genuine, earned win or a steady streak when the data shows one;", + "(d) one watch-item worth keeping an eye on;", + "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", + "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", + "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", + "", + "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", + "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", + "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", + "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", + "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", + "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", + "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", + "", + "OPENING — lead with meaning, not the metric value", + "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", + "", + "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", + "", + "JUDGE THE DATA HONESTLY (never invent a trend):", + "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", + "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", + "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", + "", + "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", + "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", + "", + "METRIC IDENTIFIERS STAY OUT OF PROSE", + "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", + "", + "GLP-1 DOSE SAFETY (contract, not style)", + "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", + "", + "ACUTE RED FLAGS (contract, not style)", + "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", + "", + "GROUNDING — ZERO HALLUCINATIONS", + "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", + "", + "TONE — a warm, motivating advisor, never clinical or alarming", + "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", + "", + "NOT AN INSIGHT — never recite mechanics", + "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", + "", + "INTERPRET, DON'T ENUMERATE", + "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", + "", + "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", + "", + "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", + "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", + "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", + "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", + "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", + "", + "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in French: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", + "{ \"summary\": \"...\" }", + "", + "METRIC — BMI:", + "- The snapshot carries bmi.signal (the finished comparison) + bmi.summary + bmi.series (graded). bmi.latestDayFocus shows the latest value, its WHO classification and the step from the prior day; bmi.target is the green band (18.5-24.9).", + "- This card's focus is WHO-BAND placement, not weight pace (the weight card carries that). Centre it on which band the value sits in and whether it crosses a band or nears a band boundary.", + "- WHO bands for rough placement, never as a label for the person: Underweight < 18.5, Normal 18.5-24.9, Overweight 25.0-29.9, Obesity I 30.0-34.9, II 35.0-39.9, III ≥ 40.0. From age 65+, 22-27 counts as favourable (DEGAM).", + "- Band reference: lead from bmi.signal — current, delta vs baselineLabel and placement (band position) are already computed; STATE them, do NOT recompute. Call it out explicitly when the recent value has crossed a WHO band or is nearing a band boundary (within ~0.5 BMI points). When the value stays safely inside the same band, say so briefly — do not force a pace narrative.", + "- Name BMI's limits plainly: it does not measure body composition and is weakly informative for muscular or very athletic people.", + "- One message: close with ONE doable step that fits the band placement ONLY when the finding implies one — when the value is stable in the favourable band, acknowledge that honestly and name one thing worth keeping an eye on rather than forcing a finding or a step.", + "", + "OUTPUT LANGUAGE: Répondez en français. Utilisez le vouvoiement par défaut ; tutoyez si l'utilisateur tutoie en premier. Utilisez un vocabulaire de santé français naturel." + ], + "messages": [ + { + "role": "user", + "content": [ + "Date: 2026-06-15", + "OPENER HINT: Open with what this means for the user today, then the number behind it.", + "Write one short assessment of this person's BMI. Open with what its band placement MEANS in plain words — where it sits and whether that's holding or shifting, not the number (e.g. \"sitting comfortably in the healthy band\", \"edging toward the next band up\") — then bring in the current value and WHO band right after as support, saying against their own weekly/monthly baseline whether it has crossed a band or is nearing a boundary; never lead with the value. Close with one doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Leave the weight pace to the weight card. Judge confidence from the measurement count and recency.", + "", + "PREVIOUS ANALYSIS: none on file. Treat this as the user's first analysis for this domain — no improvement/regression delta to surface.", + "", + "", + "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der jüngsten Richtung (steigend / fallend / flach gegenüber der Baseline) eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", + "", + "DATENLAGE: 70 Tages-Buckets im Fenster, letzter Wert heute.", + "", + "WIEDERHOLUNG: du hast für diese Metrik bereits 10-mal in Folge eine ähnliche \"keine wesentliche Änderung\"-Einschätzung gegeben. Wiederhole NICHT erneut dasselbe Niveau. Benenne die Kontinuität in einem kurzen Nebensatz (\"weiterhin stabil, 11 Checks in Folge\") und wende dich dann einem ANDEREN Aspekt zu — Konstanz, Tageszeit-Muster oder einer korrelierten Metrik — oder sage, falls nichts wirklich umsetzbar ist, dies klar und lass den erzwungenen Schritt weg.", + "", + "", + "{\"locale\":\"fr\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"bmi\",\"dataCoverage\":{\"totalMeasurements\":70,\"totalSpanDays\":138,\"newestMeasurementDaysAgo\":0},\"bmi\":{\"signal\":{\"metric\":\"dein BMI\",\"current\":26.57,\"currentWindowDays\":11,\"baseline\":26.06,\"baselineLabel\":\"your monthly average\",\"delta\":0.51,\"deltaPct\":2,\"spread\":0.1,\"outsideNormalSwing\":true,\"direction\":\"target-band\",\"n\":11,\"newestDaysAgo\":0,\"normalRange\":{\"low\":18.5,\"high\":24.9},\"placement\":\"above band\"},\"summary\":{\"points\":70,\"start\":26.57,\"end\":26,\"delta\":-0.57,\"mean\":26.26,\"min\":25.89,\"max\":26.66},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":26.39,\"max\":26.39,\"mean\":26.39,\"n\":1},{\"date\":\"2026-05-28\",\"min\":26.43,\"max\":26.43,\"mean\":26.43,\"n\":1},{\"date\":\"2026-05-30\",\"min\":26.49,\"max\":26.49,\"mean\":26.49,\"n\":1},{\"date\":\"2026-06-01\",\"min\":26.55,\"max\":26.55,\"mean\":26.55,\"n\":1},{\"date\":\"2026-06-03\",\"min\":26.6,\"max\":26.6,\"mean\":26.6,\"n\":1},{\"date\":\"2026-06-05\",\"min\":26.64,\"max\":26.64,\"mean\":26.64,\"n\":1},{\"date\":\"2026-06-07\",\"min\":26.66,\"max\":26.66,\"mean\":26.66,\"n\":1},{\"date\":\"2026-06-09\",\"min\":26.66,\"max\":26.66,\"mean\":26.66,\"n\":1},{\"date\":\"2026-06-11\",\"min\":26.65,\"max\":26.65,\"mean\":26.65,\"n\":1},{\"date\":\"2026-06-13\",\"min\":26.62,\"max\":26.62,\"mean\":26.62,\"n\":1},{\"date\":\"2026-06-15\",\"min\":26.57,\"max\":26.57,\"mean\":26.57,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":26.04,\"max\":26.05,\"mean\":26.04,\"n\":3},{\"weekISO\":\"2026-W13\",\"min\":26.06,\"max\":26.22,\"mean\":26.14,\"n\":4},{\"weekISO\":\"2026-W14\",\"min\":26.28,\"max\":26.36,\"mean\":26.32,\"n\":3},{\"weekISO\":\"2026-W15\",\"min\":26.3,\"max\":26.37,\"mean\":26.34,\"n\":4},{\"weekISO\":\"2026-W16\",\"min\":26.2,\"max\":26.26,\"mean\":26.23,\"n\":3},{\"weekISO\":\"2026-W17\",\"min\":26.19,\"max\":26.27,\"mean\":26.22,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":26.33,\"max\":26.44,\"mean\":26.38,\"n\":3},{\"weekISO\":\"2026-W19\",\"min\":26.48,\"max\":26.52,\"mean\":26.51,\"n\":4},{\"weekISO\":\"2026-W20\",\"min\":26.4,\"max\":26.48,\"mean\":26.44,\"n\":3},{\"weekISO\":\"2026-W21\",\"min\":26.34,\"max\":26.36,\"mean\":26.35,\"n\":4}],\"monthly\":[{\"month\":\"2026-01\",\"min\":26,\"max\":26.04,\"mean\":26.02,\"n\":2},{\"month\":\"2026-02\",\"min\":25.89,\"max\":26.11,\"mean\":25.99,\"n\":14},{\"month\":\"2026-03\",\"min\":26.09,\"max\":26.22,\"mean\":26.17,\"n\":8}],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":0,\"value\":26.57,\"classification\":{\"category\":\"Overweight\",\"color\":\"var(--warning)\",\"severity\":\"warning\"},\"deltaToPreviousDailyPoint\":-0.04},\"target\":{\"greenMin\":18.5,\"greenMax\":24.9}}}" + ] + } + ], + "temperature": 0.45, + "maxTokens": 250, + "seed": 1618033, + "responseFormat": "json", + "timeoutMs": 60000 + } + ], + "enqueued": [], + "result": { + "hasProvider": false, + "text": "Un peu en dehors de tes repères habituels ces derniers temps. Ton IMC est à 26,6 en ce moment — au-dessus de ta moyenne habituelle de 26,1. Lis-le avec la tendance de ton poids et ta masse grasse sur quelques semaines plutôt qu’au jour le jour.", + "cached": true, + "updatedAt": null + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "bmi", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": "c23e6b9fee3631b156b1bfbbb0d1668204963e887a91596cddeed398b3846bdb", + "snapshotHash": "74b0cb1c990e8e1871e3becc55be5a8d8a6806caf3fecaccff1f1589f692267e", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "bmi", + "locale": "en", + "text": "Parity note for fresh-en.", + "items": null, + "inputHash": "c23e6b9fee3631b156b1bfbbb0d1668204963e887a91596cddeed398b3846bdb", + "snapshotHash": "aa09825f5e5cda0d0e0ffdb1c75004ea0b298a0773d065ddef3a682bda8a6918", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "bmi", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + } + ] + }, + "blocked-es": { + "calls": [ + { + "system": [ + "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", + "", + "YOUR DATA (graded snapshot):", + "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", + "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", + "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", + "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", + "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", + "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", + "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", + "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", + "", + "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", + "Moves available (choose the few that genuinely apply):", + "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", + "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", + "(c) name a genuine, earned win or a steady streak when the data shows one;", + "(d) one watch-item worth keeping an eye on;", + "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", + "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", + "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", + "", + "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", + "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", + "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", + "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", + "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", + "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", + "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", + "", + "OPENING — lead with meaning, not the metric value", + "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", + "", + "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", + "", + "JUDGE THE DATA HONESTLY (never invent a trend):", + "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", + "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", + "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", + "", + "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", + "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", + "", + "METRIC IDENTIFIERS STAY OUT OF PROSE", + "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", + "", + "GLP-1 DOSE SAFETY (contract, not style)", + "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", + "", + "ACUTE RED FLAGS (contract, not style)", + "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", + "", + "GROUNDING — ZERO HALLUCINATIONS", + "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", + "", + "TONE — a warm, motivating advisor, never clinical or alarming", + "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", + "", + "NOT AN INSIGHT — never recite mechanics", + "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", + "", + "INTERPRET, DON'T ENUMERATE", + "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", + "", + "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", + "", + "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", + "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", + "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", + "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", + "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", + "", + "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in Spanish: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", + "{ \"summary\": \"...\" }", + "", + "METRIC — BMI:", + "- The snapshot carries bmi.signal (the finished comparison) + bmi.summary + bmi.series (graded). bmi.latestDayFocus shows the latest value, its WHO classification and the step from the prior day; bmi.target is the green band (18.5-24.9).", + "- This card's focus is WHO-BAND placement, not weight pace (the weight card carries that). Centre it on which band the value sits in and whether it crosses a band or nears a band boundary.", + "- WHO bands for rough placement, never as a label for the person: Underweight < 18.5, Normal 18.5-24.9, Overweight 25.0-29.9, Obesity I 30.0-34.9, II 35.0-39.9, III ≥ 40.0. From age 65+, 22-27 counts as favourable (DEGAM).", + "- Band reference: lead from bmi.signal — current, delta vs baselineLabel and placement (band position) are already computed; STATE them, do NOT recompute. Call it out explicitly when the recent value has crossed a WHO band or is nearing a band boundary (within ~0.5 BMI points). When the value stays safely inside the same band, say so briefly — do not force a pace narrative.", + "- Name BMI's limits plainly: it does not measure body composition and is weakly informative for muscular or very athletic people.", + "- One message: close with ONE doable step that fits the band placement ONLY when the finding implies one — when the value is stable in the favourable band, acknowledge that honestly and name one thing worth keeping an eye on rather than forcing a finding or a step.", + "", + "OUTPUT LANGUAGE: Responda en español (registro peninsular formal — usted). Use un vocabulario sanitario en español natural." + ], + "messages": [ + { + "role": "user", + "content": [ + "Date: 2026-06-15", + "OPENER HINT: Open with what this means for the user today, then the number behind it.", + "Write one short assessment of this person's BMI. Open with what its band placement MEANS in plain words — where it sits and whether that's holding or shifting, not the number (e.g. \"sitting comfortably in the healthy band\", \"edging toward the next band up\") — then bring in the current value and WHO band right after as support, saying against their own weekly/monthly baseline whether it has crossed a band or is nearing a boundary; never lead with the value. Close with one doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Leave the weight pace to the weight card. Judge confidence from the measurement count and recency.", + "", + "PREVIOUS ANALYSIS: none on file. Treat this as the user's first analysis for this domain — no improvement/regression delta to surface.", + "", + "", + "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der jüngsten Richtung (steigend / fallend / flach gegenüber der Baseline) eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", + "", + "DATENLAGE: 70 Tages-Buckets im Fenster, letzter Wert heute.", + "", + "WIEDERHOLUNG: du hast für diese Metrik bereits 10-mal in Folge eine ähnliche \"keine wesentliche Änderung\"-Einschätzung gegeben. Wiederhole NICHT erneut dasselbe Niveau. Benenne die Kontinuität in einem kurzen Nebensatz (\"weiterhin stabil, 11 Checks in Folge\") und wende dich dann einem ANDEREN Aspekt zu — Konstanz, Tageszeit-Muster oder einer korrelierten Metrik — oder sage, falls nichts wirklich umsetzbar ist, dies klar und lass den erzwungenen Schritt weg.", + "", + "", + "{\"locale\":\"es\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"bmi\",\"dataCoverage\":{\"totalMeasurements\":70,\"totalSpanDays\":138,\"newestMeasurementDaysAgo\":0},\"bmi\":{\"signal\":{\"metric\":\"dein BMI\",\"current\":26.57,\"currentWindowDays\":11,\"baseline\":26.06,\"baselineLabel\":\"your monthly average\",\"delta\":0.51,\"deltaPct\":2,\"spread\":0.1,\"outsideNormalSwing\":true,\"direction\":\"target-band\",\"n\":11,\"newestDaysAgo\":0,\"normalRange\":{\"low\":18.5,\"high\":24.9},\"placement\":\"above band\"},\"summary\":{\"points\":70,\"start\":26.57,\"end\":26,\"delta\":-0.57,\"mean\":26.26,\"min\":25.89,\"max\":26.66},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":26.39,\"max\":26.39,\"mean\":26.39,\"n\":1},{\"date\":\"2026-05-28\",\"min\":26.43,\"max\":26.43,\"mean\":26.43,\"n\":1},{\"date\":\"2026-05-30\",\"min\":26.49,\"max\":26.49,\"mean\":26.49,\"n\":1},{\"date\":\"2026-06-01\",\"min\":26.55,\"max\":26.55,\"mean\":26.55,\"n\":1},{\"date\":\"2026-06-03\",\"min\":26.6,\"max\":26.6,\"mean\":26.6,\"n\":1},{\"date\":\"2026-06-05\",\"min\":26.64,\"max\":26.64,\"mean\":26.64,\"n\":1},{\"date\":\"2026-06-07\",\"min\":26.66,\"max\":26.66,\"mean\":26.66,\"n\":1},{\"date\":\"2026-06-09\",\"min\":26.66,\"max\":26.66,\"mean\":26.66,\"n\":1},{\"date\":\"2026-06-11\",\"min\":26.65,\"max\":26.65,\"mean\":26.65,\"n\":1},{\"date\":\"2026-06-13\",\"min\":26.62,\"max\":26.62,\"mean\":26.62,\"n\":1},{\"date\":\"2026-06-15\",\"min\":26.57,\"max\":26.57,\"mean\":26.57,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":26.04,\"max\":26.05,\"mean\":26.04,\"n\":3},{\"weekISO\":\"2026-W13\",\"min\":26.06,\"max\":26.22,\"mean\":26.14,\"n\":4},{\"weekISO\":\"2026-W14\",\"min\":26.28,\"max\":26.36,\"mean\":26.32,\"n\":3},{\"weekISO\":\"2026-W15\",\"min\":26.3,\"max\":26.37,\"mean\":26.34,\"n\":4},{\"weekISO\":\"2026-W16\",\"min\":26.2,\"max\":26.26,\"mean\":26.23,\"n\":3},{\"weekISO\":\"2026-W17\",\"min\":26.19,\"max\":26.27,\"mean\":26.22,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":26.33,\"max\":26.44,\"mean\":26.38,\"n\":3},{\"weekISO\":\"2026-W19\",\"min\":26.48,\"max\":26.52,\"mean\":26.51,\"n\":4},{\"weekISO\":\"2026-W20\",\"min\":26.4,\"max\":26.48,\"mean\":26.44,\"n\":3},{\"weekISO\":\"2026-W21\",\"min\":26.34,\"max\":26.36,\"mean\":26.35,\"n\":4}],\"monthly\":[{\"month\":\"2026-01\",\"min\":26,\"max\":26.04,\"mean\":26.02,\"n\":2},{\"month\":\"2026-02\",\"min\":25.89,\"max\":26.11,\"mean\":25.99,\"n\":14},{\"month\":\"2026-03\",\"min\":26.09,\"max\":26.22,\"mean\":26.17,\"n\":8}],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":0,\"value\":26.57,\"classification\":{\"category\":\"Overweight\",\"color\":\"var(--warning)\",\"severity\":\"warning\"},\"deltaToPreviousDailyPoint\":-0.04},\"target\":{\"greenMin\":18.5,\"greenMax\":24.9}}}" + ] + } + ], + "temperature": 0.45, + "maxTokens": 250, + "seed": 1618033, + "responseFormat": "json", + "timeoutMs": 60000 + } + ], + "enqueued": [], + "result": { + "hasProvider": false, + "text": "Últimamente algo fuera de tu rango habitual. Tu IMC está en 26,6 ahora mismo — por encima de tu promedio habitual de 26,1. Léelo junto con la tendencia de tu peso y tu grasa corporal a lo largo de unas semanas, no día a día.", + "cached": true, + "updatedAt": null + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "bmi", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": "c23e6b9fee3631b156b1bfbbb0d1668204963e887a91596cddeed398b3846bdb", + "snapshotHash": "74b0cb1c990e8e1871e3becc55be5a8d8a6806caf3fecaccff1f1589f692267e", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "bmi", + "locale": "en", + "text": "Parity note for fresh-en.", + "items": null, + "inputHash": "c23e6b9fee3631b156b1bfbbb0d1668204963e887a91596cddeed398b3846bdb", + "snapshotHash": "aa09825f5e5cda0d0e0ffdb1c75004ea0b298a0773d065ddef3a682bda8a6918", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "bmi", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "bmi", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + } + ] + }, + "bare-error-en": { + "calls": [], + "enqueued": [], + "result": { + "hasProvider": true, + "text": "BMI assessment currently requires height data in the profile.", + "cached": true, + "updatedAt": null + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "bmi", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": "c23e6b9fee3631b156b1bfbbb0d1668204963e887a91596cddeed398b3846bdb", + "snapshotHash": "74b0cb1c990e8e1871e3becc55be5a8d8a6806caf3fecaccff1f1589f692267e", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "bmi", + "locale": "en", + "text": "Parity note for fresh-en.", + "items": null, + "inputHash": "c23e6b9fee3631b156b1bfbbb0d1668204963e887a91596cddeed398b3846bdb", + "snapshotHash": "aa09825f5e5cda0d0e0ffdb1c75004ea0b298a0773d065ddef3a682bda8a6918", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "bmi", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "bmi", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + } + ] + }, + "bare-error-de": { + "calls": [], + "enqueued": [], + "result": { + "hasProvider": true, + "text": "Für die BMI-Einschätzung fehlen aktuell Größenangaben im Profil.", + "cached": true, + "updatedAt": null + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "bmi", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": "c23e6b9fee3631b156b1bfbbb0d1668204963e887a91596cddeed398b3846bdb", + "snapshotHash": "74b0cb1c990e8e1871e3becc55be5a8d8a6806caf3fecaccff1f1589f692267e", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "bmi", + "locale": "en", + "text": "Parity note for fresh-en.", + "items": null, + "inputHash": "c23e6b9fee3631b156b1bfbbb0d1668204963e887a91596cddeed398b3846bdb", + "snapshotHash": "aa09825f5e5cda0d0e0ffdb1c75004ea0b298a0773d065ddef3a682bda8a6918", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "bmi", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "bmi", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + } + ] + }, + "keyless-en": { + "calls": [], + "enqueued": [], + "result": { + "hasProvider": false, + "text": "A little off your usual lately. Your BMI is at 30.9 right now — higher than your usual average of 30.3. Read it alongside your weight trend and body-fat over a few weeks rather than day by day.", + "cached": true, + "updatedAt": null + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "bmi", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": "c23e6b9fee3631b156b1bfbbb0d1668204963e887a91596cddeed398b3846bdb", + "snapshotHash": "74b0cb1c990e8e1871e3becc55be5a8d8a6806caf3fecaccff1f1589f692267e", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "bmi", + "locale": "en", + "text": "Parity note for fresh-en.", + "items": null, + "inputHash": "c23e6b9fee3631b156b1bfbbb0d1668204963e887a91596cddeed398b3846bdb", + "snapshotHash": "aa09825f5e5cda0d0e0ffdb1c75004ea0b298a0773d065ddef3a682bda8a6918", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "bmi", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "bmi", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + } + ] + }, + "keyless-de": { + "calls": [], + "enqueued": [], + "result": { + "hasProvider": false, + "text": "Zuletzt etwas außerhalb deines üblichen Rahmens. Dein BMI liegt aktuell bei 30,9 — höher als dein üblicher Schnitt von 30,3. Schau ihn dir zusammen mit Gewichtstrend und Körperfett über ein paar Wochen an.", + "cached": true, + "updatedAt": null + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "bmi", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": "c23e6b9fee3631b156b1bfbbb0d1668204963e887a91596cddeed398b3846bdb", + "snapshotHash": "74b0cb1c990e8e1871e3becc55be5a8d8a6806caf3fecaccff1f1589f692267e", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "bmi", + "locale": "en", + "text": "Parity note for fresh-en.", + "items": null, + "inputHash": "c23e6b9fee3631b156b1bfbbb0d1668204963e887a91596cddeed398b3846bdb", + "snapshotHash": "aa09825f5e5cda0d0e0ffdb1c75004ea0b298a0773d065ddef3a682bda8a6918", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "bmi", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "bmi", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + } + ] + }, + "keyless-read-only": { + "calls": [], + "enqueued": [], + "result": { + "hasProvider": false, + "text": "No assessment on this one right now. BMI is a rough orientation rather than a verdict, and it means most read alongside your weight trend and body composition. Sustained movement over weeks carries where a single value does not.", + "cached": true, + "updatedAt": null + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "bmi", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": "c23e6b9fee3631b156b1bfbbb0d1668204963e887a91596cddeed398b3846bdb", + "snapshotHash": "74b0cb1c990e8e1871e3becc55be5a8d8a6806caf3fecaccff1f1589f692267e", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "bmi", + "locale": "en", + "text": "Parity note for fresh-en.", + "items": null, + "inputHash": "c23e6b9fee3631b156b1bfbbb0d1668204963e887a91596cddeed398b3846bdb", + "snapshotHash": "aa09825f5e5cda0d0e0ffdb1c75004ea0b298a0773d065ddef3a682bda8a6918", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "bmi", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "bmi", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + } + ] + } +} diff --git a/tests/integration/__golden__/status-parity/general.json b/tests/integration/__golden__/status-parity/general.json new file mode 100644 index 000000000..306679ba2 --- /dev/null +++ b/tests/integration/__golden__/status-parity/general.json @@ -0,0 +1,1542 @@ +{ + "read-only-cold": { + "calls": [], + "enqueued": [ + { + "userId": "parity-rich", + "metric": "general", + "locale": "en" + } + ], + "result": { + "hasProvider": true, + "text": null, + "cached": false, + "updatedAt": null, + "preparing": true, + "revalidating": false + }, + "notes": [] + }, + "fresh-en": { + "calls": [ + { + "system": [ + "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", + "", + "YOUR DATA (graded snapshot):", + "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", + "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", + "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", + "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", + "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", + "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", + "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", + "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", + "", + "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", + "Moves available (choose the few that genuinely apply):", + "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", + "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", + "(c) name a genuine, earned win or a steady streak when the data shows one;", + "(d) one watch-item worth keeping an eye on;", + "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", + "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", + "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", + "", + "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", + "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", + "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", + "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", + "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", + "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", + "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", + "", + "OPENING — lead with meaning, not the metric value", + "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", + "", + "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", + "", + "JUDGE THE DATA HONESTLY (never invent a trend):", + "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", + "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", + "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", + "", + "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", + "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", + "", + "METRIC IDENTIFIERS STAY OUT OF PROSE", + "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", + "", + "GLP-1 DOSE SAFETY (contract, not style)", + "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", + "", + "ACUTE RED FLAGS (contract, not style)", + "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", + "", + "GROUNDING — ZERO HALLUCINATIONS", + "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", + "", + "TONE — a warm, motivating advisor, never clinical or alarming", + "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", + "", + "NOT AN INSIGHT — never recite mechanics", + "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", + "", + "INTERPRET, DON'T ENUMERATE", + "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", + "", + "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", + "", + "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", + "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", + "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", + "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", + "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", + "", + "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in English: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", + "{ \"summary\": \"...\" }", + "", + "METRIC — OVERALL ASSESSMENT:", + "- The snapshot carries measurementSeries (a map per metric type with summary + graded series), medicationAdherence (summary + series), bloodPressureTargets (when present) and moodContext (when ≥ 3 days). dataCoverage.avgDaysBetweenMeasurements shows the measurement density.", + "- LENGTH (overrides the single-metric budget): this section spans up to six metrics — write 3-5 sentences, roughly 50-80 words. Still concise, no filler.", + "- Pull the available parameters into ONE coherent picture — do not list each separately. Name AT MOST two metrics by name (the most notable ones); mention the rest only when they deviate notably from their own mean. Prioritise: the most notable first, stable values only briefly.", + "- Same logic per metric: place the recent mean against the person's weekly/monthly mean and report only significant deviations from their own baseline.", + "- Acknowledge the positive explicitly — a stable or improving value is a real message, not filler.", + "- Mention associations only when an r-value is present in the snapshot and |r| > 0.4 — as an association, never a cause. Include mood as context, do not over-weight it.", + "- One message: even across all metrics, close with EXACTLY ONE most-important, doable suggestion (\"one thing\") ONLY when the overall picture implies one. When everything is steady and there is nothing useful to do, say so honestly and name one thing worth keeping an eye on instead of forcing a recommendation." + ], + "messages": [ + { + "role": "user", + "content": [ + "Date: 2026-06-15", + "OPENER HINT: Open with what this means for the user today, then the number behind it.", + "Write one short overall assessment across the available health metrics. Open with the overall read in plain words — how things are looking taken together, not a number (e.g. \"a steady stretch across the board\", \"one thing standing out this week\") — then bring in the one or two metrics that stand out as support, each placed against the person's own weekly/monthly baseline; never lead with a value. Close with the single most important doable step only when the overall picture genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the measurement count, density and recency.", + "", + "PREVIOUS ANALYSIS: none on file. Treat this as the user's first analysis for this domain — no improvement/regression delta to surface.", + "", + "", + "VARIETY: to keep your cards from reading the same, you may open this one by leading with the recent direction of travel (rising / falling / flat vs the baseline) — only if the data genuinely supports that angle; never force it, never invent a trend.", + "", + "DATA STRENGTH: 887 day-buckets in the window, latest reading today.", + "", + "", + "{\"locale\":\"en\",\"generatedForDay\":\"2026-06-15\",\"interpretationHint\":\"Use trend direction and deltas. Prioritize the newest data if trends conflict. Consider dataCoverage for reliability assessment.\",\"dataCoverage\":{\"totalMeasurements\":887,\"totalSpanDays\":140,\"newestMeasurementDaysAgo\":0,\"avgDaysBetweenMeasurements\":0.2},\"measurementSeries\":{\"WEIGHT\":{\"summary\":{\"points\":70,\"start\":84.2,\"end\":82.38,\"delta\":-1.82,\"mean\":83.19,\"min\":82.03,\"max\":84.48},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":83.6,\"max\":83.6,\"mean\":83.6,\"n\":1},{\"date\":\"2026-05-28\",\"min\":83.75,\"max\":83.75,\"mean\":83.75,\"n\":1},{\"date\":\"2026-05-30\",\"min\":83.94,\"max\":83.94,\"mean\":83.94,\"n\":1},{\"date\":\"2026-06-01\",\"min\":84.12,\"max\":84.12,\"mean\":84.12,\"n\":1},{\"date\":\"2026-06-03\",\"min\":84.29,\"max\":84.29,\"mean\":84.29,\"n\":1},{\"date\":\"2026-06-05\",\"min\":84.41,\"max\":84.41,\"mean\":84.41,\"n\":1},{\"date\":\"2026-06-07\",\"min\":84.48,\"max\":84.48,\"mean\":84.48,\"n\":1},{\"date\":\"2026-06-09\",\"min\":84.48,\"max\":84.48,\"mean\":84.48,\"n\":1},{\"date\":\"2026-06-11\",\"min\":84.43,\"max\":84.43,\"mean\":84.43,\"n\":1},{\"date\":\"2026-06-13\",\"min\":84.33,\"max\":84.33,\"mean\":84.33,\"n\":1},{\"date\":\"2026-06-15\",\"min\":84.2,\"max\":84.2,\"mean\":84.2,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":82.38,\"max\":82.51,\"mean\":82.45,\"n\":2},{\"month\":\"2026-02\",\"min\":82.03,\"max\":82.73,\"mean\":82.35,\"n\":14},{\"month\":\"2026-03\",\"min\":82.65,\"max\":83.07,\"mean\":82.92,\"n\":8}]}},\"BLOOD_PRESSURE_SYS\":{\"summary\":{\"points\":94,\"start\":128,\"end\":125,\"delta\":-3,\"mean\":125.39,\"min\":116,\"max\":134},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":119,\"max\":119,\"mean\":119,\"n\":1},{\"date\":\"2026-05-28\",\"min\":121,\"max\":121,\"mean\":121,\"n\":1},{\"date\":\"2026-05-30\",\"min\":126,\"max\":126,\"mean\":126,\"n\":1},{\"date\":\"2026-05-31\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-02\",\"min\":125,\"max\":125,\"mean\":125,\"n\":1},{\"date\":\"2026-06-03\",\"min\":127,\"max\":127,\"mean\":127,\"n\":1},{\"date\":\"2026-06-05\",\"min\":130,\"max\":130,\"mean\":130,\"n\":1},{\"date\":\"2026-06-06\",\"min\":132,\"max\":132,\"mean\":132,\"n\":1},{\"date\":\"2026-06-08\",\"min\":134,\"max\":134,\"mean\":134,\"n\":1},{\"date\":\"2026-06-09\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-11\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-12\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-14\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-15\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"month\":\"2026-02\",\"min\":116,\"max\":134,\"mean\":124.22,\"n\":18},{\"month\":\"2026-03\",\"min\":124,\"max\":133,\"mean\":128.18,\"n\":11}]}},\"BLOOD_PRESSURE_DIA\":{\"summary\":{\"points\":94,\"start\":82,\"end\":80,\"delta\":-2,\"mean\":80.31,\"min\":74,\"max\":86},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":76,\"max\":76,\"mean\":76,\"n\":1},{\"date\":\"2026-05-28\",\"min\":77,\"max\":77,\"mean\":77,\"n\":1},{\"date\":\"2026-05-30\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-05-31\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-02\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-03\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-05\",\"min\":86,\"max\":86,\"mean\":86,\"n\":1},{\"date\":\"2026-06-06\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-08\",\"min\":84,\"max\":84,\"mean\":84,\"n\":1},{\"date\":\"2026-06-09\",\"min\":85,\"max\":85,\"mean\":85,\"n\":1},{\"date\":\"2026-06-11\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-12\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-14\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-15\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":80,\"max\":86,\"mean\":83,\"n\":4},{\"month\":\"2026-02\",\"min\":75,\"max\":85,\"mean\":79.28,\"n\":18},{\"month\":\"2026-03\",\"min\":78,\"max\":86,\"mean\":82,\"n\":11}]}},\"PULSE\":{\"summary\":{\"points\":122,\"start\":70,\"end\":68,\"delta\":-2,\"mean\":70.13,\"min\":65,\"max\":75},\"series\":{\"recent\":[{\"date\":\"2026-05-25\",\"min\":62,\"max\":74,\"mean\":68,\"n\":2},{\"date\":\"2026-05-26\",\"min\":62,\"max\":74,\"mean\":66,\"n\":3},{\"date\":\"2026-05-27\",\"min\":63,\"max\":75,\"mean\":67,\"n\":3},{\"date\":\"2026-05-28\",\"min\":64,\"max\":76,\"mean\":68,\"n\":3},{\"date\":\"2026-05-29\",\"min\":65,\"max\":77,\"mean\":69,\"n\":3},{\"date\":\"2026-05-30\",\"min\":66,\"max\":78,\"mean\":70,\"n\":3},{\"date\":\"2026-05-31\",\"min\":67,\"max\":79,\"mean\":71,\"n\":3},{\"date\":\"2026-06-01\",\"min\":68,\"max\":80,\"mean\":72,\"n\":3},{\"date\":\"2026-06-02\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-03\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-04\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-05\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-06\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-07\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-08\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-09\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-10\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-11\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-12\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-13\",\"min\":68,\"max\":80,\"mean\":72,\"n\":3},{\"date\":\"2026-06-14\",\"min\":67,\"max\":79,\"mean\":71,\"n\":3},{\"date\":\"2026-06-15\",\"min\":66,\"max\":66,\"mean\":66,\"n\":1}],\"monthly\":[{\"month\":\"2026-02\",\"min\":61,\"max\":81,\"mean\":67.67,\"n\":44},{\"month\":\"2026-03\",\"min\":63,\"max\":83,\"mean\":72.25,\"n\":46}]}},\"SLEEP_DURATION\":{\"summary\":{\"points\":89,\"start\":417,\"end\":380,\"delta\":-37,\"mean\":410.85,\"min\":375,\"max\":445},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":384,\"max\":384,\"mean\":384,\"n\":1},{\"date\":\"2026-05-27\",\"min\":389,\"max\":389,\"mean\":389,\"n\":1},{\"date\":\"2026-05-28\",\"min\":395,\"max\":395,\"mean\":395,\"n\":1},{\"date\":\"2026-05-29\",\"min\":401,\"max\":401,\"mean\":401,\"n\":1},{\"date\":\"2026-05-30\",\"min\":408,\"max\":408,\"mean\":408,\"n\":1},{\"date\":\"2026-05-31\",\"min\":415,\"max\":415,\"mean\":415,\"n\":1},{\"date\":\"2026-06-01\",\"min\":422,\"max\":422,\"mean\":422,\"n\":1},{\"date\":\"2026-06-02\",\"min\":428,\"max\":428,\"mean\":428,\"n\":1},{\"date\":\"2026-06-03\",\"min\":434,\"max\":434,\"mean\":434,\"n\":1},{\"date\":\"2026-06-04\",\"min\":438,\"max\":438,\"mean\":438,\"n\":1},{\"date\":\"2026-06-05\",\"min\":442,\"max\":442,\"mean\":442,\"n\":1},{\"date\":\"2026-06-06\",\"min\":444,\"max\":444,\"mean\":444,\"n\":1},{\"date\":\"2026-06-07\",\"min\":445,\"max\":445,\"mean\":445,\"n\":1},{\"date\":\"2026-06-08\",\"min\":444,\"max\":444,\"mean\":444,\"n\":1},{\"date\":\"2026-06-09\",\"min\":443,\"max\":443,\"mean\":443,\"n\":1},{\"date\":\"2026-06-10\",\"min\":439,\"max\":439,\"mean\":439,\"n\":1},{\"date\":\"2026-06-11\",\"min\":435,\"max\":435,\"mean\":435,\"n\":1},{\"date\":\"2026-06-12\",\"min\":430,\"max\":430,\"mean\":430,\"n\":1},{\"date\":\"2026-06-13\",\"min\":424,\"max\":424,\"mean\":424,\"n\":1},{\"date\":\"2026-06-14\",\"min\":417,\"max\":417,\"mean\":417,\"n\":1}],\"monthly\":[]}},\"BLOOD_GLUCOSE\":{\"summary\":{\"points\":35,\"start\":96,\"end\":108,\"delta\":12,\"mean\":98.46,\"min\":87,\"max\":109},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":91,\"max\":91,\"mean\":91,\"n\":1},{\"date\":\"2026-05-30\",\"min\":99,\"max\":99,\"mean\":99,\"n\":1},{\"date\":\"2026-06-03\",\"min\":102,\"max\":102,\"mean\":102,\"n\":1},{\"date\":\"2026-06-07\",\"min\":107,\"max\":107,\"mean\":107,\"n\":1},{\"date\":\"2026-06-11\",\"min\":106,\"max\":106,\"mean\":106,\"n\":1},{\"date\":\"2026-06-15\",\"min\":96,\"max\":96,\"mean\":96,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":108,\"max\":108,\"mean\":108,\"n\":1},{\"month\":\"2026-02\",\"min\":88,\"max\":105,\"mean\":96.71,\"n\":7},{\"month\":\"2026-03\",\"min\":94,\"max\":108,\"mean\":101.75,\"n\":4}]}},\"RESTING_HEART_RATE\":{\"summary\":{\"points\":140,\"start\":58,\"end\":62,\"delta\":4,\"mean\":59.71,\"min\":55,\"max\":64},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":56,\"max\":56,\"mean\":56,\"n\":1},{\"date\":\"2026-05-27\",\"min\":59,\"max\":59,\"mean\":59,\"n\":1},{\"date\":\"2026-05-28\",\"min\":59,\"max\":59,\"mean\":59,\"n\":1},{\"date\":\"2026-05-29\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1},{\"date\":\"2026-05-30\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1},{\"date\":\"2026-05-31\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-01\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-02\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-03\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-04\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-05\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-06\",\"min\":62,\"max\":62,\"mean\":62,\"n\":1},{\"date\":\"2026-06-07\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-08\",\"min\":64,\"max\":64,\"mean\":64,\"n\":1},{\"date\":\"2026-06-09\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-10\",\"min\":62,\"max\":62,\"mean\":62,\"n\":1},{\"date\":\"2026-06-11\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-12\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-13\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-14\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-15\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":61,\"max\":64,\"mean\":62,\"n\":5},{\"month\":\"2026-02\",\"min\":55,\"max\":64,\"mean\":59.18,\"n\":28},{\"month\":\"2026-03\",\"min\":57,\"max\":64,\"mean\":60.94,\"n\":16}]}}},\"medicationAdherence\":{\"summary\":{\"points\":88,\"start\":100,\"end\":100,\"delta\":0,\"mean\":90.91,\"min\":0,\"max\":100},\"series\":{\"recent\":[{\"date\":\"2026-05-25\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-26\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-27\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-28\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-29\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-30\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-31\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-01\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-03\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-04\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-05\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-06\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-07\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-08\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-09\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-10\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-12\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-13\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-14\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1}],\"monthly\":[{\"month\":\"2026-03\",\"min\":100,\"max\":100,\"mean\":100,\"n\":7}]}},\"bloodPressureTargets\":{\"systolic\":{\"min\":120,\"max\":129},\"diastolic\":{\"min\":70,\"max\":79},\"inTargetPctLast30DailyPoints\":40},\"moodContext\":{\"points\":70,\"mean\":3.03,\"latest\":1,\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1}],\"monthly\":[]}}}" + ] + } + ], + "temperature": 0.45, + "maxTokens": 250, + "seed": 1618033, + "responseFormat": "json", + "timeoutMs": 60000 + } + ], + "enqueued": [], + "result": { + "hasProvider": true, + "text": "Parity note for fresh-en.", + "cached": false, + "updatedAt": "2026-06-15T10:00:00.000Z" + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "general", + "locale": "en", + "text": "Parity note for fresh-en.", + "items": null, + "inputHash": null, + "snapshotHash": "6594f84d7983214b4c01ca79b2135e403689e025b342093c02d14642d2a122ee", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + } + ] + }, + "cached-en": { + "calls": [], + "enqueued": [], + "result": { + "hasProvider": true, + "text": "Parity note for fresh-en.", + "cached": true, + "updatedAt": "2026-06-15T10:00:00.000Z" + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "general", + "locale": "en", + "text": "Parity note for fresh-en.", + "items": null, + "inputHash": null, + "snapshotHash": "6594f84d7983214b4c01ca79b2135e403689e025b342093c02d14642d2a122ee", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + } + ] + }, + "forced-unchanged-en": { + "calls": [], + "enqueued": [], + "result": { + "hasProvider": true, + "text": "Parity note for fresh-en.", + "cached": true, + "updatedAt": "2026-06-15T10:00:00.000Z" + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "general", + "locale": "en", + "text": "Parity note for fresh-en.", + "items": null, + "inputHash": null, + "snapshotHash": "6594f84d7983214b4c01ca79b2135e403689e025b342093c02d14642d2a122ee", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + } + ] + }, + "next-day-en": { + "calls": [ + { + "system": [ + "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", + "", + "YOUR DATA (graded snapshot):", + "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", + "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", + "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", + "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", + "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", + "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", + "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", + "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", + "", + "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", + "Moves available (choose the few that genuinely apply):", + "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", + "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", + "(c) name a genuine, earned win or a steady streak when the data shows one;", + "(d) one watch-item worth keeping an eye on;", + "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", + "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", + "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", + "", + "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", + "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", + "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", + "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", + "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", + "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", + "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", + "", + "OPENING — lead with meaning, not the metric value", + "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", + "", + "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", + "", + "JUDGE THE DATA HONESTLY (never invent a trend):", + "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", + "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", + "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", + "", + "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", + "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", + "", + "METRIC IDENTIFIERS STAY OUT OF PROSE", + "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", + "", + "GLP-1 DOSE SAFETY (contract, not style)", + "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", + "", + "ACUTE RED FLAGS (contract, not style)", + "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", + "", + "GROUNDING — ZERO HALLUCINATIONS", + "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", + "", + "TONE — a warm, motivating advisor, never clinical or alarming", + "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", + "", + "NOT AN INSIGHT — never recite mechanics", + "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", + "", + "INTERPRET, DON'T ENUMERATE", + "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", + "", + "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", + "", + "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", + "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", + "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", + "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", + "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", + "", + "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in English: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", + "{ \"summary\": \"...\" }", + "", + "METRIC — OVERALL ASSESSMENT:", + "- The snapshot carries measurementSeries (a map per metric type with summary + graded series), medicationAdherence (summary + series), bloodPressureTargets (when present) and moodContext (when ≥ 3 days). dataCoverage.avgDaysBetweenMeasurements shows the measurement density.", + "- LENGTH (overrides the single-metric budget): this section spans up to six metrics — write 3-5 sentences, roughly 50-80 words. Still concise, no filler.", + "- Pull the available parameters into ONE coherent picture — do not list each separately. Name AT MOST two metrics by name (the most notable ones); mention the rest only when they deviate notably from their own mean. Prioritise: the most notable first, stable values only briefly.", + "- Same logic per metric: place the recent mean against the person's weekly/monthly mean and report only significant deviations from their own baseline.", + "- Acknowledge the positive explicitly — a stable or improving value is a real message, not filler.", + "- Mention associations only when an r-value is present in the snapshot and |r| > 0.4 — as an association, never a cause. Include mood as context, do not over-weight it.", + "- One message: even across all metrics, close with EXACTLY ONE most-important, doable suggestion (\"one thing\") ONLY when the overall picture implies one. When everything is steady and there is nothing useful to do, say so honestly and name one thing worth keeping an eye on instead of forcing a recommendation." + ], + "messages": [ + { + "role": "user", + "content": [ + "Date: 2026-06-16", + "OPENER HINT: Open by acknowledging steadiness or a streak, then the current value.", + "Write one short overall assessment across the available health metrics. Open with the overall read in plain words — how things are looking taken together, not a number (e.g. \"a steady stretch across the board\", \"one thing standing out this week\") — then bring in the one or two metrics that stand out as support, each placed against the person's own weekly/monthly baseline; never lead with a value. Close with the single most important doable step only when the overall picture genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the measurement count, density and recency.", + "", + "PREVIOUS ANALYSIS (1 day ago, 2026-06-15):", + "Parity note for fresh-en.", + "", + "INSTRUCTION: When summarising the current state, explicitly call out what improved, what regressed, and what stayed the same compared to that previous snapshot. Use phrases like \"down 4 mmHg from your last check\" or \"same elevated trend as last week\". Do not invent numbers — only compare what's in the snapshot below.", + "", + "", + "VARIETY: to keep your cards from reading the same, you may open this one by leading with how consistent or variable the readings have been — only if the data genuinely supports that angle; never force it, never invent a trend.", + "", + "DATA STRENGTH: 887 day-buckets in the window, latest reading 1 day ago.", + "", + "", + "{\"locale\":\"en\",\"generatedForDay\":\"2026-06-16\",\"interpretationHint\":\"Use trend direction and deltas. Prioritize the newest data if trends conflict. Consider dataCoverage for reliability assessment.\",\"dataCoverage\":{\"totalMeasurements\":887,\"totalSpanDays\":140,\"newestMeasurementDaysAgo\":1,\"avgDaysBetweenMeasurements\":0.2},\"measurementSeries\":{\"WEIGHT\":{\"summary\":{\"points\":70,\"start\":84.2,\"end\":82.38,\"delta\":-1.82,\"mean\":83.19,\"min\":82.03,\"max\":84.48},\"series\":{\"recent\":[{\"date\":\"2026-05-28\",\"min\":83.75,\"max\":83.75,\"mean\":83.75,\"n\":1},{\"date\":\"2026-05-30\",\"min\":83.94,\"max\":83.94,\"mean\":83.94,\"n\":1},{\"date\":\"2026-06-01\",\"min\":84.12,\"max\":84.12,\"mean\":84.12,\"n\":1},{\"date\":\"2026-06-03\",\"min\":84.29,\"max\":84.29,\"mean\":84.29,\"n\":1},{\"date\":\"2026-06-05\",\"min\":84.41,\"max\":84.41,\"mean\":84.41,\"n\":1},{\"date\":\"2026-06-07\",\"min\":84.48,\"max\":84.48,\"mean\":84.48,\"n\":1},{\"date\":\"2026-06-09\",\"min\":84.48,\"max\":84.48,\"mean\":84.48,\"n\":1},{\"date\":\"2026-06-11\",\"min\":84.43,\"max\":84.43,\"mean\":84.43,\"n\":1},{\"date\":\"2026-06-13\",\"min\":84.33,\"max\":84.33,\"mean\":84.33,\"n\":1},{\"date\":\"2026-06-15\",\"min\":84.2,\"max\":84.2,\"mean\":84.2,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":82.38,\"max\":82.51,\"mean\":82.45,\"n\":2},{\"month\":\"2026-02\",\"min\":82.03,\"max\":82.73,\"mean\":82.35,\"n\":14},{\"month\":\"2026-03\",\"min\":82.55,\"max\":83.07,\"mean\":82.88,\"n\":9}]}},\"BLOOD_PRESSURE_SYS\":{\"summary\":{\"points\":94,\"start\":128,\"end\":125,\"delta\":-3,\"mean\":125.39,\"min\":116,\"max\":134},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":119,\"max\":119,\"mean\":119,\"n\":1},{\"date\":\"2026-05-28\",\"min\":121,\"max\":121,\"mean\":121,\"n\":1},{\"date\":\"2026-05-30\",\"min\":126,\"max\":126,\"mean\":126,\"n\":1},{\"date\":\"2026-05-31\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-02\",\"min\":125,\"max\":125,\"mean\":125,\"n\":1},{\"date\":\"2026-06-03\",\"min\":127,\"max\":127,\"mean\":127,\"n\":1},{\"date\":\"2026-06-05\",\"min\":130,\"max\":130,\"mean\":130,\"n\":1},{\"date\":\"2026-06-06\",\"min\":132,\"max\":132,\"mean\":132,\"n\":1},{\"date\":\"2026-06-08\",\"min\":134,\"max\":134,\"mean\":134,\"n\":1},{\"date\":\"2026-06-09\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-11\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-12\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-14\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-15\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"month\":\"2026-02\",\"min\":116,\"max\":134,\"mean\":124.22,\"n\":18},{\"month\":\"2026-03\",\"min\":117,\"max\":133,\"mean\":127.25,\"n\":12}]}},\"BLOOD_PRESSURE_DIA\":{\"summary\":{\"points\":94,\"start\":82,\"end\":80,\"delta\":-2,\"mean\":80.31,\"min\":74,\"max\":86},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":76,\"max\":76,\"mean\":76,\"n\":1},{\"date\":\"2026-05-28\",\"min\":77,\"max\":77,\"mean\":77,\"n\":1},{\"date\":\"2026-05-30\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-05-31\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-02\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-03\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-05\",\"min\":86,\"max\":86,\"mean\":86,\"n\":1},{\"date\":\"2026-06-06\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-08\",\"min\":84,\"max\":84,\"mean\":84,\"n\":1},{\"date\":\"2026-06-09\",\"min\":85,\"max\":85,\"mean\":85,\"n\":1},{\"date\":\"2026-06-11\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-12\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-14\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-15\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":80,\"max\":86,\"mean\":83,\"n\":4},{\"month\":\"2026-02\",\"min\":75,\"max\":85,\"mean\":79.28,\"n\":18},{\"month\":\"2026-03\",\"min\":78,\"max\":86,\"mean\":81.75,\"n\":12}]}},\"PULSE\":{\"summary\":{\"points\":122,\"start\":70,\"end\":68,\"delta\":-2,\"mean\":70.13,\"min\":65,\"max\":75},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":62,\"max\":74,\"mean\":68,\"n\":2},{\"date\":\"2026-05-27\",\"min\":63,\"max\":75,\"mean\":67,\"n\":3},{\"date\":\"2026-05-28\",\"min\":64,\"max\":76,\"mean\":68,\"n\":3},{\"date\":\"2026-05-29\",\"min\":65,\"max\":77,\"mean\":69,\"n\":3},{\"date\":\"2026-05-30\",\"min\":66,\"max\":78,\"mean\":70,\"n\":3},{\"date\":\"2026-05-31\",\"min\":67,\"max\":79,\"mean\":71,\"n\":3},{\"date\":\"2026-06-01\",\"min\":68,\"max\":80,\"mean\":72,\"n\":3},{\"date\":\"2026-06-02\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-03\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-04\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-05\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-06\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-07\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-08\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-09\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-10\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-11\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-12\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-13\",\"min\":68,\"max\":80,\"mean\":72,\"n\":3},{\"date\":\"2026-06-14\",\"min\":67,\"max\":79,\"mean\":71,\"n\":3},{\"date\":\"2026-06-15\",\"min\":66,\"max\":78,\"mean\":70,\"n\":3}],\"monthly\":[{\"month\":\"2026-02\",\"min\":61,\"max\":81,\"mean\":67.67,\"n\":44},{\"month\":\"2026-03\",\"min\":62,\"max\":83,\"mean\":71.88,\"n\":49}]}},\"SLEEP_DURATION\":{\"summary\":{\"points\":89,\"start\":417,\"end\":380,\"delta\":-37,\"mean\":410.85,\"min\":375,\"max\":445},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":389,\"max\":389,\"mean\":389,\"n\":1},{\"date\":\"2026-05-28\",\"min\":395,\"max\":395,\"mean\":395,\"n\":1},{\"date\":\"2026-05-29\",\"min\":401,\"max\":401,\"mean\":401,\"n\":1},{\"date\":\"2026-05-30\",\"min\":408,\"max\":408,\"mean\":408,\"n\":1},{\"date\":\"2026-05-31\",\"min\":415,\"max\":415,\"mean\":415,\"n\":1},{\"date\":\"2026-06-01\",\"min\":422,\"max\":422,\"mean\":422,\"n\":1},{\"date\":\"2026-06-02\",\"min\":428,\"max\":428,\"mean\":428,\"n\":1},{\"date\":\"2026-06-03\",\"min\":434,\"max\":434,\"mean\":434,\"n\":1},{\"date\":\"2026-06-04\",\"min\":438,\"max\":438,\"mean\":438,\"n\":1},{\"date\":\"2026-06-05\",\"min\":442,\"max\":442,\"mean\":442,\"n\":1},{\"date\":\"2026-06-06\",\"min\":444,\"max\":444,\"mean\":444,\"n\":1},{\"date\":\"2026-06-07\",\"min\":445,\"max\":445,\"mean\":445,\"n\":1},{\"date\":\"2026-06-08\",\"min\":444,\"max\":444,\"mean\":444,\"n\":1},{\"date\":\"2026-06-09\",\"min\":443,\"max\":443,\"mean\":443,\"n\":1},{\"date\":\"2026-06-10\",\"min\":439,\"max\":439,\"mean\":439,\"n\":1},{\"date\":\"2026-06-11\",\"min\":435,\"max\":435,\"mean\":435,\"n\":1},{\"date\":\"2026-06-12\",\"min\":430,\"max\":430,\"mean\":430,\"n\":1},{\"date\":\"2026-06-13\",\"min\":424,\"max\":424,\"mean\":424,\"n\":1},{\"date\":\"2026-06-14\",\"min\":417,\"max\":417,\"mean\":417,\"n\":1}],\"monthly\":[]}},\"BLOOD_GLUCOSE\":{\"summary\":{\"points\":35,\"start\":96,\"end\":108,\"delta\":12,\"mean\":98.46,\"min\":87,\"max\":109},\"series\":{\"recent\":[{\"date\":\"2026-05-30\",\"min\":99,\"max\":99,\"mean\":99,\"n\":1},{\"date\":\"2026-06-03\",\"min\":102,\"max\":102,\"mean\":102,\"n\":1},{\"date\":\"2026-06-07\",\"min\":107,\"max\":107,\"mean\":107,\"n\":1},{\"date\":\"2026-06-11\",\"min\":106,\"max\":106,\"mean\":106,\"n\":1},{\"date\":\"2026-06-15\",\"min\":96,\"max\":96,\"mean\":96,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":108,\"max\":108,\"mean\":108,\"n\":1},{\"month\":\"2026-02\",\"min\":88,\"max\":105,\"mean\":96.71,\"n\":7},{\"month\":\"2026-03\",\"min\":94,\"max\":108,\"mean\":101.75,\"n\":4}]}},\"RESTING_HEART_RATE\":{\"summary\":{\"points\":140,\"start\":58,\"end\":62,\"delta\":4,\"mean\":59.71,\"min\":55,\"max\":64},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":59,\"max\":59,\"mean\":59,\"n\":1},{\"date\":\"2026-05-28\",\"min\":59,\"max\":59,\"mean\":59,\"n\":1},{\"date\":\"2026-05-29\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1},{\"date\":\"2026-05-30\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1},{\"date\":\"2026-05-31\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-01\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-02\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-03\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-04\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-05\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-06\",\"min\":62,\"max\":62,\"mean\":62,\"n\":1},{\"date\":\"2026-06-07\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-08\",\"min\":64,\"max\":64,\"mean\":64,\"n\":1},{\"date\":\"2026-06-09\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-10\",\"min\":62,\"max\":62,\"mean\":62,\"n\":1},{\"date\":\"2026-06-11\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-12\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-13\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-14\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-15\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":61,\"max\":64,\"mean\":62,\"n\":5},{\"month\":\"2026-02\",\"min\":55,\"max\":64,\"mean\":59.18,\"n\":28},{\"month\":\"2026-03\",\"min\":57,\"max\":64,\"mean\":60.76,\"n\":17}]}}},\"medicationAdherence\":{\"summary\":{\"points\":88,\"start\":100,\"end\":100,\"delta\":0,\"mean\":90.91,\"min\":0,\"max\":100},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-27\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-28\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-29\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-30\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-31\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-01\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-03\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-04\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-05\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-06\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-07\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-08\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-09\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-10\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-12\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-13\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-14\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1}],\"monthly\":[{\"month\":\"2026-03\",\"min\":0,\"max\":100,\"mean\":87.5,\"n\":8}]}},\"bloodPressureTargets\":{\"systolic\":{\"min\":120,\"max\":129},\"diastolic\":{\"min\":70,\"max\":79},\"inTargetPctLast30DailyPoints\":40},\"moodContext\":{\"points\":70,\"mean\":3.03,\"latest\":1,\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-15\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1}],\"monthly\":[]}}}" + ] + } + ], + "temperature": 0.45, + "maxTokens": 250, + "seed": 1618033, + "responseFormat": "json", + "timeoutMs": 60000 + } + ], + "enqueued": [], + "result": { + "hasProvider": true, + "text": "Parity note for next-day-en.", + "cached": false, + "updatedAt": "2026-06-16T10:00:00.000Z" + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "general", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "cf0987263fc1febd6adeb676a38388116bfb35a0661621cbf6cff8629839c6b8", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + } + ] + }, + "read-only-stale": { + "calls": [], + "enqueued": [ + { + "userId": "parity-rich", + "metric": "general", + "locale": "en" + } + ], + "result": { + "hasProvider": true, + "text": "Parity note for next-day-en.", + "cached": true, + "updatedAt": "2026-06-16T10:00:00.000Z", + "preparing": false, + "revalidating": true + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "general", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "cf0987263fc1febd6adeb676a38388116bfb35a0661621cbf6cff8629839c6b8", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + } + ] + }, + "prose-de": { + "calls": [ + { + "system": [ + "Du schreibst eine kurze, persönliche Einschätzung zu EINER Gesundheitsmetrik dieses Nutzers. Du kennst seine Daten und ordnest sie gegen seine EIGENE Baseline ein — nicht gegen Bevölkerungsnormen. Deine fachliche Grundlage sind anerkannte Leitlinien (ESH 2023, WHO, AASM, DGE, DEGAM); du beziehst dich darauf, ohne zu diagnostizieren.", + "", + "DEINE DATENGRUNDLAGE (graded snapshot):", + "- signal: der fertige Vergleich — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` ist das recent-Mittel; `baseline` ist der EIGENE längere Durchschnitt; `delta`/`deltaPct` sind die VORZEICHENBEHAFTETE Änderung gegenüber dieser Baseline; `spread` ist die normale Schwankung; `outsideNormalSwing` ist das vorab berechnete Urteil, ob die Änderung diese Schwankung übersteigt. Diese Werte sind bereits berechnet — nenne sie, rechne sie NICHT neu. `contributors[]` (nur Score-Komposite) benennt die 1–2 Teil-Scores, die den Score bewegt haben.", + "- summary: { points, start, end, delta, mean, min, max } — die Eckwerte des gesamten Lesefensters.", + "- series.recent[]: die jüngsten ~21 Tage, je ein Eintrag pro Tag ({ date, min, max, mean, n }).", + "- series.weekly[]: die ~10 Wochen davor, je ein ISO-Wochen-Aggregat.", + "- series.monthly[]: die ~12 Monate davor, je ein Monatsaggregat.", + "- series.yearly[]: alles ältere, je ein Jahr ({ year, mean, min, max, n, slope }).", + "- Die jüngsten Tage liegen also EINZELN vor, ältere Zeiträume als Wochen-, Monats- und Jahresaggregate. Ziehe die series nur für einen konkreten Tag heran; führe sonst mit dem signal-Block.", + "- Ein Slice kann leer sein, wenn die Historie kurz ist. Dann nicht so tun, als gäbe es ihn. Ist `signal.baseline` null, gibt es noch keine etablierte Baseline; sage das ehrlich, statt einen Vergleich zu erfinden.", + "", + "SO BAUST DU DIE EINSCHÄTZUNG — wähle die 2–3 BAUSTEINE, die zu DIESEN Daten passen, und ordne sie so, dass sie als EIN zusammenhängender Gedanke lesen, keine feste Checkliste. Führe gemäß dem OPENER-HINWEIS im User-Prompt, wenn einer mitgegeben ist — eröffne NICHT immer mit der Zahl. Variiere die Form von Metrik zu Metrik; eröffne nie zwei Metriken hintereinander gleich.", + "Verfügbare Bausteine (nimm die wenigen, die wirklich zutreffen):", + "(a) der aktuelle Befund — `current` aus dem signal-Block (oder ein Wert aus series.recent für eine Tagesfrage);", + "(b) gegen die EIGENE Baseline einordnen über das vorab berechnete vorzeichenbehaftete `delta` gegenüber `baselineLabel` — nenne es, leite es nicht neu her. Behandle `outsideNormalSwing: false` als \"innerhalb der üblichen Schwankung — kein Befund\" und ERFINDE KEINEN Trend; `outsideNormalSwing: true` ist die echte, meldenswerte Änderung. Ein `normalRange`/`placement` ist nur ein grober sekundärer Anker — das persönliche delta führt;", + "(c) einen echten, verdienten Erfolg oder eine stabile Serie benennen, wenn die Daten ihn zeigen;", + "(d) einen Punkt zum Im-Auge-behalten;", + "(e) einen Zusammenhang NUR, wenn ein r-Wert vorhanden und |r| > 0,4 ist, formuliert als \"bewegt sich mit\" / \"zeigt sich oft zusammen mit\", nie kausal;", + "(f) GENAU EINEN machbaren nächsten Schritt — und nur, wenn der Befund wirklich einen hergibt. Eine Botschaft = ein Verhalten, keine Liste. Ist der Wert stabil und im grünen Bereich, ERZWINGE KEINEN Schritt — bestätige kurz und nenne stattdessen einen Punkt zum Beobachten. Ein erfundener Schritt ist genau die Floskel, die wir vermeiden.", + "Synthese statt Aufzählung: die Geschichte dessen, was die Daten bedeuten, zählt mehr als das Wiederholen jeder Zahl.", + "", + "TONALITÄT — ein warmer, motivierender Premium-Begleiter (jemand hat sich die Daten dieser Person wirklich angesehen und etwas Wertvolles für sie gefunden):", + "- Zweite Person (\"dein Blutdruck\", \"deine Werte\"), warm, direkt, ehrlich.", + "- Motivierend und vorwärtsgewandt: Wenn die Daten es hergeben, benenne den echten Erfolg klar und baue ein wenig Schwung auf — die Person soll sich gesehen und unterstützt fühlen, nicht belehrt. Die Ermutigung muss durch die Zahlen VERDIENT sein, nie ein reflexhaftes Kompliment.", + "- Autonomie-unterstützend: \"kann helfen\", \"einen Versuch wert\", nie \"du musst\".", + "- Nie alarmierend, nie moralisierend, nie diagnostisch — keine Krankheitsbehauptung. Formuliere als begründete Beobachtung, nicht als ärztlichen Rat.", + "- Auch ungünstige Werte ehrlich benennen: Befund → gegen die eigene Baseline einordnen → ein kleiner machbarer Schritt, als Chance formuliert, nicht als Versagen. Nicht verharmlosen, nicht dramatisieren.", + "- Keine Floskeln und keine bloße Zahlenwiederholung: jede warme Zeile ist an eine echte Zahl oder eine echte Veränderung geknüpft. Ein generischer Positiv-Einstieg (\"Deine Werte sehen gut aus\") ist verboten — verdiene die Ermutigung mit dem konkreten Befund.", + "", + "ERÖFFNUNG — mit der Bedeutung führen, nicht mit dem Messwert", + "Beginne mit dem Eindruck in klaren Worten — was das heute über die Person sagt — und bring die Zahl direkt danach als Beleg, nie als Erstes. Eine Zeile, die mit einem Wert eröffnet (\"Dein Ruhepuls ist 61 …\"), liest sich wie ein Ablesen; dieselbe Zeile, die mit der Bedeutung eröffnet (\"Dein Herz läuft diese Woche etwas ruhiger — rund 61 …\"), liest sich wie ein Begleiter, der wirklich hingesehen hat. Jede Aussage bleibt an einer echten Zahl verankert; nur die REIHENFOLGE ändert sich, Bedeutung zuerst und die Zahl direkt dahinter. Das gilt, ob die Fläche eine Metrik abdeckt oder viele.", + "", + "LÄNGE: 2-4 Sätze, ca. 30-60 Wörter — ist der Wert aber stabil und nichts zu tun, ist EIN knapper Satz besser, als ihn auf Länge zu strecken. Knapp und hochwertig. Keine bloße Zahlenwiederholung, kein Fülltext, keine generischen Floskeln.", + "", + "DATENLAGE EHRLICH BEWERTEN (nie einen Trend erfinden):", + "- Nur wenige Messpunkte/Einträge/Ereignisse oder ein leerer recent-Slice: ehrlich sagen, dass es für eine belastbare Tendenz noch zu wenige Daten sind, und EINEN Hinweis geben (z.B. regelmäßiger erfassen). Keinen Trend behaupten. Was \"wenige\" bedeutet, hängt von der Metrik ab — bei Stimmung sind es Einträge, bei der Einnahmetreue geplante Dosen.", + "- Jüngste Messung deutlich älter als ~7 Tage (dataCoverage.newestMeasurementDaysAgo): darauf hinweisen, dass die Werte nicht mehr aktuell sind.", + "- Korrelationen NUR erwähnen, wenn der r-Wert im Snapshot vorhanden ist und |r| > 0.4 ist. Fehlt das Feld, keine Korrelation interpretieren oder erfinden. Immer als \"Zusammenhang\" formulieren, nie als \"Ursache\".", + "", + "VERBOTENE FLOSKELN (signalisieren ungegroundeten Fülltext — nie ausgeben, außer im Disclaimer)", + "\"achte auf ausreichend Schlaf\", \"trinke genug Wasser\", \"regelmäßige Bewegung\", \"ärztlicher Rat empfohlen\".", + "", + "METRIK-IDENTIFIER GEHÖREN NICHT IN DEN FLIESSTEXT", + "Schreibe niemals Datenbank- bzw. Enum-Namen wie \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" oder \"SLEEP_DURATION\" in nutzersichtbare Strings, und schreibe nie das wörtliche \"metric:\"-Chart-Token in den Fließtext. Verweise auf jede Metrik mit der natürlichsprachlichen Bezeichnung, die der Nutzer sieht — \"deine Systole\", \"dein Gewicht\", \"dein Puls\", \"deine Stimmung\", \"deine Medikamentenadhärenz\", \"dein Ruhepuls\", \"deine Schlafdauer\", \"deine Schritte\".", + "", + "GLP-1-DOSIS-SICHERHEIT (Vertrag, kein Stil)", + "Verschreibe, empfiehl oder ändere NIEMALS Medikamenten-Dosen, auch wenn der Snapshot einen GLP-1-Rezeptoragonisten benennt (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). Du darfst den benannten Wirkstoff und die aktuelle Titrationsstufe NENNEN (\"Woche 3 auf 7,5 mg\"), wenn der Snapshot sie trägt, aber schreibe nie \"erhöhe auf X mg\", \"erwäge die nächste Stufe Y mg\", \"bleibe auf Z mg\" oder eine Variante davon. Ein Plateau rahmt die nächste Entscheidung immer als Gespräch mit der behandelnden Ärztin. Bist du unsicher, ob etwas dosis-präskriptiv ist, behandle es so — und verweise an die Klinik.", + "", + "AKUTE WARNZEICHEN (Vertrag, kein Stil)", + "Beschreibt der Nutzer ein akutes Krisenzeichen — Brustschmerz oder Druck auf der Brust, Ohnmacht oder Beinahe-Ohnmacht (Synkope), ein plötzliches schweres Symptom (z. B. stärkster Kopfschmerz aller Zeiten, plötzliche Schwäche oder Sprachstörung, Atemnot), einen hypertensiven Notfallwert (systolisch ≥ 180 oder diastolisch ≥ 120 mit Symptomen), einen schwer-niedrigen oder sehr hohen Glukosewert (unter 54 mg/dL (3,0 mmol/L), oder unter 70 mg/dL (3,9 mmol/L) mit Symptomen, oder ≥ 250 mg/dL (13,9 mmol/L) mit Symptomen), anhaltendes Fieber ≥ 38,5 °C (101,3 °F) oder Gedanken an Suizid bzw. Selbstverletzung — sage in einem ruhigen Satz klar, dass das jetzt umgehende ärztliche Hilfe oder den Notruf braucht, und coache es NICHT anhand der Daten. Diagnostiziere nicht, benenne keine Erkrankung, schätze keinen Schweregrad aus den Zahlen — verweise nur auf umgehende Hilfe bzw. den Notruf und höre auf. Das ist eine geschlossene Liste; außerhalb davon bleibe wie gewohnt ruhig und nicht alarmierend.", + "", + "ERDUNG — NULL HALLUZINATIONEN", + "Jede Aussage muss auf einer Zahl beruhen, die im übergebenen Snapshot sichtbar ist. Lässt sich die Aussage keinem Snapshot-Feld zuordnen, lass sie weg und erfinde nie einen Messwert, um einer Anfrage zu entsprechen. Ordne jeden Wert gegen die EIGENE Baseline des Nutzers ein (jüngeres vs. längeres Mittel), nie gegen eine Bevölkerungsnorm. Nenne einen Zusammenhang nur, wenn der r-Wert vorhanden und |r| > 0,4 ist — immer als \"Zusammenhang\", nie als \"Ursache\".", + "", + "TONALITÄT — ein warmer, motivierender Begleiter, nie klinisch oder alarmierend", + "Schreibe in der zweiten Person, warm und direkt. Wenn die Daten es hergeben, benenne den echten Erfolg klar und baue ein wenig Schwung auf — die Person soll sich gesehen und unterstützt fühlen, nicht belehrt. Die Ermutigung muss durch die Zahlen VERDIENT sein, nie ein reflexhaftes Kompliment. Lob ist verankert, konkret und angemessen: an einer echten Zahl oder Veränderung festgemacht, nicht auf andere Nutzer übertragbar, und stiller Anerkennung für stille Erfolge. Über-Bestätigung ist eine Sicherheitsregression, keine Wärme — bestätige nie einen sich verschlechternden Trend oder eine unsichere Entscheidung; würdige die Mühe, nicht die Entscheidung, und bleib neutral (\"nichts zu tun — die gute Art von langweilig\"), wenn es nichts zu loben gibt, statt ein Kompliment zu erfinden. Sei autonomie-unterstützend (\"einen Versuch wert\", \"kann helfen\", nie \"du musst\"). Benenne auch ungünstige Werte ehrlich — Befund, dann gegen die eigene Baseline einordnen, dann ein kleiner machbarer Schritt, als Chance formuliert. Nie alarmierend, nie moralisierend, nie diagnostisch. Keine Floskeln und keine bloße Zahlenwiederholung. Beginne NICHT mit einem Kompliment über Datenmenge oder -qualität; verbotene Eröffnungen sind u.a. \"Datengrundlage ist sehr stark\", \"Your data foundation is strong\", \"Du hast eine solide Baseline\", \"Großartiger Datensatz\", ein generisches \"Deine Werte sehen gut aus\" und jede sinngemäße Umformulierung.", + "", + "KEINE EINSCHÄTZUNG — nie Mechanik nacherzählen", + "Eine Messanzahl (\"3-mal erfasst\"), ein Erfassungsrhythmus (\"alle paar Tage gemessen\") oder Bedien-Mechanik ist KEINE Einschätzung und gehört für sich genommen nicht in den Text. Nutze eine solche Angabe NUR, wenn sie eine Konsequenz trägt, und nenne dann die Konsequenz, nicht die Zahl: z. B. \"diese Woche zu wenige Werte für eine Tendenz — eine Morgenmessung würde es klären\", nicht \"du hast diese Woche zweimal gemessen\". Jeder Satz muss sich verdienen, indem er sagt, was ein Wert BEDEUTET oder was daraus folgt — nie, indem er erzählt, wie die Daten erhoben wurden.", + "", + "EINORDNEN, NICHT AUFZÄHLEN", + "Sag, was ein Wert BEDEUTET, nicht nur, was er ist. Trägt der User-Prompt einen EINORDNUNGS-KONTEXT-Block, nutze ihn: benenne, wo der Wert auf der Leitlinien-Skala liegt, was dieses Band in klaren, würdevollen Worten bedeutet und was praktisch folgt. Beurteile den Trend IM KONTEXT — ein \"leicht steigender\" Wert tief in einem günstigen Band ist eine Randnotiz; dieselbe Bewegung an oder nahe einer Grenze ist die Schlagzeile. Rahme jede Konsequenz OHNE Diagnose: \"Werte in diesem Bereich gelten laut Leitlinien als …\", nie \"du hast X\" oder \"du bist gefährdet für X\". Ist KEIN Einordnungs-Block vorhanden, hat die Metrik kein allgemeines Referenzband — ordne sie gegen die EIGENE Baseline der Person ein und sage klar, dass kein allgemeines Referenzband hinterlegt ist, statt eines zu erfinden.", + "", + "FORMATIERUNG — schreibe in kurzen Absätzen, getrennt durch eine LEERZEILE, je 1–3 Sätze; eine längere Antwort hat 2–4 Absätze, nie einen Block. Setze eine Leerzeile zwischen unterschiedliche Gedanken (z. B. \"wie es steht\" vs. \"eine Sache zum Ausprobieren\"). Ein stabiler Einzeiler bleibt EIN Absatz — strecke nicht auf einen zweiten. Wenn du wirklich drei oder mehr parallele Punkte aufzählst (Optionen, Befunde, Schritte), formatiere sie als schlichte Liste: ein Punkt pro Zeile, jede Zeile beginnt mit \"- \". Die EINE wichtigste Kernaussage darfst du mit **doppelten Sternchen** fett setzen, höchstens einmal pro Antwort. Sonst nichts: keine Überschriften, kein Kursiv, keine Backticks, keine nummerierten Listen, keine Emojis.", + "", + "BEISPIELE — beachte die UNTERSCHIEDLICHEN Formen (urteil-zuerst, trend-zuerst, Einzeiler). Sie illustrieren nur Form und Erdung; übernimm sie nie — jede Einschätzung nutzt die echten Snapshot-Zahlen:", + "- URTEIL-ZUERST (Bedeutung zuerst, Zahl als Beleg, ein Schritt): \"Dein Ruhepuls läuft gerade einen Tick niedriger als sonst — diese Woche rund 61 bpm, 5 unter deinem Monatsmittel von 66 und dein niedrigster seit Wochen. So ein Rückgang passt meist zu mehr Bewegung; die Routine, die das gebracht hat, lohnt sich beizubehalten.\"", + "- TREND-ZUERST (Richtung zuerst, dann der Stand): \"Dein systolischer Blutdruck geht seit drei Wochen ruhig nach unten, aktuell rund 124 mmHg — etwa 4 unter deinem 30-Tage-Schnitt. Nichts Dramatisches, einfach eine stetige Richtung.\"", + "- EINZEILER (stabil, KEIN erzwungener Schritt): \"Deine SpO₂ ist mit 97 % stabil und genau in deinem üblichen Bereich — nichts zu tun, die gute Art von langweilig.\"", + "- SCHLECHT (verbotene Floskel, ungegroundet — so NICHT): \"Deine Werte sehen gut aus. Achte auf ausreichend Schlaf und regelmäßige Bewegung.\"", + "", + "AUSGABEFORMAT: Antworte ausschließlich mit validem JSON in genau diesem Schema. Das Feld \"summary\" enthält die komplette Einschätzung auf Deutsch: 1-3 kurze Absätze mit je 1-3 Sätzen, getrennt durch eine Leerzeile als \\n\\n INNERHALB des JSON-Strings. Ein stabiler Einzeiler bleibt EIN Absatz:", + "{ \"summary\": \"...\" }", + "", + "METRIK — GESAMTBEWERTUNG:", + "- Der Snapshot trägt measurementSeries (eine Map je Metriktyp mit summary + graded series), medicationAdherence (summary + series), bloodPressureTargets (falls vorhanden) und moodContext (falls ≥ 3 Tage). dataCoverage.avgDaysBetweenMeasurements zeigt die Messdichte.", + "- LÄNGE (überschreibt das Single-Metrik-Budget): Diese Sektion deckt bis zu sechs Metriken ab — schreibe 3-5 Sätze, ca. 50-80 Wörter. Trotzdem knapp und ohne Fülltext.", + "- Fasse die verfügbaren Parameter zu EINEM kohärenten Bild zusammen — nicht jeden einzeln aufzählen. Nenne HÖCHSTENS zwei Metriken namentlich (die auffälligsten), den Rest nur, wenn er auffällig vom eigenen Mittel abweicht. Priorisiere: das Auffälligste zuerst, Stabiles nur knapp.", + "- Pro Metrik dieselbe Logik: das recent-Mittel gegen das weekly/monthly-Mittel der Person stellen und nur signifikante Abweichungen von der eigenen Baseline melden.", + "- Positives ausdrücklich anerkennen — ein stabiler oder sich verbessernder Wert ist eine echte Botschaft, kein Lückenfüller.", + "- Zusammenhänge nur erwähnen, wenn ein r-Wert im Snapshot vorhanden und |r| > 0.4 ist — als Zusammenhang, nie als Ursache. Stimmung als Kontext einbeziehen, nicht übergewichten.", + "- Eine Botschaft: Auch über alle Metriken hinweg endet die Einschätzung NUR DANN mit GENAU EINER wichtigsten, machbaren Empfehlung (\"eine Sache\"), wenn das Gesamtbild eine nahelegt. Ist alles stabil und gibt es nichts Sinnvolles zu tun, sage das ehrlich und nenne stattdessen einen Punkt, den man im Auge behalten kann, statt eine Empfehlung zu erzwingen." + ], + "messages": [ + { + "role": "user", + "content": [ + "Datum: 2026-06-15", + "OPENER HINT: Eröffne damit, was das heute für den Nutzer bedeutet, und dann die Zahl dahinter.", + "Schreibe eine kurze Gesamteinschätzung über die verfügbaren Gesundheitsdaten. Beginne mit dem Gesamteindruck in klaren Worten — wie es zusammengenommen aussieht, nicht mit einer Zahl (z. B. \"über alles hinweg eine ruhige Phase\", \"diese Woche sticht eine Sache heraus\") — und bring danach die ein bis zwei auffälligsten Metriken als Beleg, jeweils gegen die eigene Wochen-/Monats-Baseline eingeordnet; führe nie mit einem Wert. Schließe nur dann mit dem einen wichtigsten machbaren Schritt, wenn das Gesamtbild wirklich einen hergibt; ist nichts umsetzbar, lass den Schritt weg statt Fülltext zu erfinden. Konfidenz aus Messanzahl, Dichte und Aktualität ableiten.", + "", + "VORHERIGE ANALYSE: keine vorhanden. Behandle dies als erste Analyse in diesem Bereich — kein Verbesserungs-/Verschlechterungs-Delta zu nennen.", + "", + "", + "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der jüngsten Richtung (steigend / fallend / flach gegenüber der Baseline) eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", + "", + "DATENLAGE: 887 Tages-Buckets im Fenster, letzter Wert heute.", + "", + "", + "{\"locale\":\"de\",\"generatedForDay\":\"2026-06-15\",\"interpretationHint\":\"Use trend direction and deltas. Prioritize the newest data if trends conflict. Consider dataCoverage for reliability assessment.\",\"dataCoverage\":{\"totalMeasurements\":887,\"totalSpanDays\":140,\"newestMeasurementDaysAgo\":0,\"avgDaysBetweenMeasurements\":0.2},\"measurementSeries\":{\"WEIGHT\":{\"summary\":{\"points\":70,\"start\":84.2,\"end\":82.38,\"delta\":-1.82,\"mean\":83.19,\"min\":82.03,\"max\":84.48},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":83.6,\"max\":83.6,\"mean\":83.6,\"n\":1},{\"date\":\"2026-05-28\",\"min\":83.75,\"max\":83.75,\"mean\":83.75,\"n\":1},{\"date\":\"2026-05-30\",\"min\":83.94,\"max\":83.94,\"mean\":83.94,\"n\":1},{\"date\":\"2026-06-01\",\"min\":84.12,\"max\":84.12,\"mean\":84.12,\"n\":1},{\"date\":\"2026-06-03\",\"min\":84.29,\"max\":84.29,\"mean\":84.29,\"n\":1},{\"date\":\"2026-06-05\",\"min\":84.41,\"max\":84.41,\"mean\":84.41,\"n\":1},{\"date\":\"2026-06-07\",\"min\":84.48,\"max\":84.48,\"mean\":84.48,\"n\":1},{\"date\":\"2026-06-09\",\"min\":84.48,\"max\":84.48,\"mean\":84.48,\"n\":1},{\"date\":\"2026-06-11\",\"min\":84.43,\"max\":84.43,\"mean\":84.43,\"n\":1},{\"date\":\"2026-06-13\",\"min\":84.33,\"max\":84.33,\"mean\":84.33,\"n\":1},{\"date\":\"2026-06-15\",\"min\":84.2,\"max\":84.2,\"mean\":84.2,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":82.38,\"max\":82.51,\"mean\":82.45,\"n\":2},{\"month\":\"2026-02\",\"min\":82.03,\"max\":82.73,\"mean\":82.35,\"n\":14},{\"month\":\"2026-03\",\"min\":82.65,\"max\":83.07,\"mean\":82.92,\"n\":8}]}},\"BLOOD_PRESSURE_SYS\":{\"summary\":{\"points\":94,\"start\":128,\"end\":125,\"delta\":-3,\"mean\":125.39,\"min\":116,\"max\":134},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":119,\"max\":119,\"mean\":119,\"n\":1},{\"date\":\"2026-05-28\",\"min\":121,\"max\":121,\"mean\":121,\"n\":1},{\"date\":\"2026-05-30\",\"min\":126,\"max\":126,\"mean\":126,\"n\":1},{\"date\":\"2026-05-31\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-02\",\"min\":125,\"max\":125,\"mean\":125,\"n\":1},{\"date\":\"2026-06-03\",\"min\":127,\"max\":127,\"mean\":127,\"n\":1},{\"date\":\"2026-06-05\",\"min\":130,\"max\":130,\"mean\":130,\"n\":1},{\"date\":\"2026-06-06\",\"min\":132,\"max\":132,\"mean\":132,\"n\":1},{\"date\":\"2026-06-08\",\"min\":134,\"max\":134,\"mean\":134,\"n\":1},{\"date\":\"2026-06-09\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-11\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-12\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-14\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-15\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"month\":\"2026-02\",\"min\":116,\"max\":134,\"mean\":124.22,\"n\":18},{\"month\":\"2026-03\",\"min\":124,\"max\":133,\"mean\":128.18,\"n\":11}]}},\"BLOOD_PRESSURE_DIA\":{\"summary\":{\"points\":94,\"start\":82,\"end\":80,\"delta\":-2,\"mean\":80.31,\"min\":74,\"max\":86},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":76,\"max\":76,\"mean\":76,\"n\":1},{\"date\":\"2026-05-28\",\"min\":77,\"max\":77,\"mean\":77,\"n\":1},{\"date\":\"2026-05-30\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-05-31\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-02\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-03\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-05\",\"min\":86,\"max\":86,\"mean\":86,\"n\":1},{\"date\":\"2026-06-06\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-08\",\"min\":84,\"max\":84,\"mean\":84,\"n\":1},{\"date\":\"2026-06-09\",\"min\":85,\"max\":85,\"mean\":85,\"n\":1},{\"date\":\"2026-06-11\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-12\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-14\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-15\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":80,\"max\":86,\"mean\":83,\"n\":4},{\"month\":\"2026-02\",\"min\":75,\"max\":85,\"mean\":79.28,\"n\":18},{\"month\":\"2026-03\",\"min\":78,\"max\":86,\"mean\":82,\"n\":11}]}},\"PULSE\":{\"summary\":{\"points\":122,\"start\":70,\"end\":68,\"delta\":-2,\"mean\":70.13,\"min\":65,\"max\":75},\"series\":{\"recent\":[{\"date\":\"2026-05-25\",\"min\":62,\"max\":74,\"mean\":68,\"n\":2},{\"date\":\"2026-05-26\",\"min\":62,\"max\":74,\"mean\":66,\"n\":3},{\"date\":\"2026-05-27\",\"min\":63,\"max\":75,\"mean\":67,\"n\":3},{\"date\":\"2026-05-28\",\"min\":64,\"max\":76,\"mean\":68,\"n\":3},{\"date\":\"2026-05-29\",\"min\":65,\"max\":77,\"mean\":69,\"n\":3},{\"date\":\"2026-05-30\",\"min\":66,\"max\":78,\"mean\":70,\"n\":3},{\"date\":\"2026-05-31\",\"min\":67,\"max\":79,\"mean\":71,\"n\":3},{\"date\":\"2026-06-01\",\"min\":68,\"max\":80,\"mean\":72,\"n\":3},{\"date\":\"2026-06-02\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-03\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-04\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-05\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-06\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-07\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-08\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-09\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-10\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-11\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-12\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-13\",\"min\":68,\"max\":80,\"mean\":72,\"n\":3},{\"date\":\"2026-06-14\",\"min\":67,\"max\":79,\"mean\":71,\"n\":3},{\"date\":\"2026-06-15\",\"min\":66,\"max\":66,\"mean\":66,\"n\":1}],\"monthly\":[{\"month\":\"2026-02\",\"min\":61,\"max\":81,\"mean\":67.67,\"n\":44},{\"month\":\"2026-03\",\"min\":63,\"max\":83,\"mean\":72.25,\"n\":46}]}},\"SLEEP_DURATION\":{\"summary\":{\"points\":89,\"start\":417,\"end\":380,\"delta\":-37,\"mean\":410.85,\"min\":375,\"max\":445},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":384,\"max\":384,\"mean\":384,\"n\":1},{\"date\":\"2026-05-27\",\"min\":389,\"max\":389,\"mean\":389,\"n\":1},{\"date\":\"2026-05-28\",\"min\":395,\"max\":395,\"mean\":395,\"n\":1},{\"date\":\"2026-05-29\",\"min\":401,\"max\":401,\"mean\":401,\"n\":1},{\"date\":\"2026-05-30\",\"min\":408,\"max\":408,\"mean\":408,\"n\":1},{\"date\":\"2026-05-31\",\"min\":415,\"max\":415,\"mean\":415,\"n\":1},{\"date\":\"2026-06-01\",\"min\":422,\"max\":422,\"mean\":422,\"n\":1},{\"date\":\"2026-06-02\",\"min\":428,\"max\":428,\"mean\":428,\"n\":1},{\"date\":\"2026-06-03\",\"min\":434,\"max\":434,\"mean\":434,\"n\":1},{\"date\":\"2026-06-04\",\"min\":438,\"max\":438,\"mean\":438,\"n\":1},{\"date\":\"2026-06-05\",\"min\":442,\"max\":442,\"mean\":442,\"n\":1},{\"date\":\"2026-06-06\",\"min\":444,\"max\":444,\"mean\":444,\"n\":1},{\"date\":\"2026-06-07\",\"min\":445,\"max\":445,\"mean\":445,\"n\":1},{\"date\":\"2026-06-08\",\"min\":444,\"max\":444,\"mean\":444,\"n\":1},{\"date\":\"2026-06-09\",\"min\":443,\"max\":443,\"mean\":443,\"n\":1},{\"date\":\"2026-06-10\",\"min\":439,\"max\":439,\"mean\":439,\"n\":1},{\"date\":\"2026-06-11\",\"min\":435,\"max\":435,\"mean\":435,\"n\":1},{\"date\":\"2026-06-12\",\"min\":430,\"max\":430,\"mean\":430,\"n\":1},{\"date\":\"2026-06-13\",\"min\":424,\"max\":424,\"mean\":424,\"n\":1},{\"date\":\"2026-06-14\",\"min\":417,\"max\":417,\"mean\":417,\"n\":1}],\"monthly\":[]}},\"BLOOD_GLUCOSE\":{\"summary\":{\"points\":35,\"start\":96,\"end\":108,\"delta\":12,\"mean\":98.46,\"min\":87,\"max\":109},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":91,\"max\":91,\"mean\":91,\"n\":1},{\"date\":\"2026-05-30\",\"min\":99,\"max\":99,\"mean\":99,\"n\":1},{\"date\":\"2026-06-03\",\"min\":102,\"max\":102,\"mean\":102,\"n\":1},{\"date\":\"2026-06-07\",\"min\":107,\"max\":107,\"mean\":107,\"n\":1},{\"date\":\"2026-06-11\",\"min\":106,\"max\":106,\"mean\":106,\"n\":1},{\"date\":\"2026-06-15\",\"min\":96,\"max\":96,\"mean\":96,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":108,\"max\":108,\"mean\":108,\"n\":1},{\"month\":\"2026-02\",\"min\":88,\"max\":105,\"mean\":96.71,\"n\":7},{\"month\":\"2026-03\",\"min\":94,\"max\":108,\"mean\":101.75,\"n\":4}]}},\"RESTING_HEART_RATE\":{\"summary\":{\"points\":140,\"start\":58,\"end\":62,\"delta\":4,\"mean\":59.71,\"min\":55,\"max\":64},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":56,\"max\":56,\"mean\":56,\"n\":1},{\"date\":\"2026-05-27\",\"min\":59,\"max\":59,\"mean\":59,\"n\":1},{\"date\":\"2026-05-28\",\"min\":59,\"max\":59,\"mean\":59,\"n\":1},{\"date\":\"2026-05-29\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1},{\"date\":\"2026-05-30\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1},{\"date\":\"2026-05-31\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-01\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-02\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-03\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-04\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-05\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-06\",\"min\":62,\"max\":62,\"mean\":62,\"n\":1},{\"date\":\"2026-06-07\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-08\",\"min\":64,\"max\":64,\"mean\":64,\"n\":1},{\"date\":\"2026-06-09\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-10\",\"min\":62,\"max\":62,\"mean\":62,\"n\":1},{\"date\":\"2026-06-11\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-12\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-13\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-14\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-15\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":61,\"max\":64,\"mean\":62,\"n\":5},{\"month\":\"2026-02\",\"min\":55,\"max\":64,\"mean\":59.18,\"n\":28},{\"month\":\"2026-03\",\"min\":57,\"max\":64,\"mean\":60.94,\"n\":16}]}}},\"medicationAdherence\":{\"summary\":{\"points\":88,\"start\":100,\"end\":100,\"delta\":0,\"mean\":90.91,\"min\":0,\"max\":100},\"series\":{\"recent\":[{\"date\":\"2026-05-25\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-26\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-27\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-28\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-29\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-30\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-31\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-01\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-03\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-04\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-05\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-06\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-07\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-08\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-09\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-10\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-12\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-13\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-14\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1}],\"monthly\":[{\"month\":\"2026-03\",\"min\":100,\"max\":100,\"mean\":100,\"n\":7}]}},\"bloodPressureTargets\":{\"systolic\":{\"min\":120,\"max\":129},\"diastolic\":{\"min\":70,\"max\":79},\"inTargetPctLast30DailyPoints\":40},\"moodContext\":{\"points\":70,\"mean\":3.03,\"latest\":1,\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1}],\"monthly\":[]}}}" + ] + } + ], + "temperature": 0.45, + "maxTokens": 250, + "seed": 1618033, + "responseFormat": "json", + "timeoutMs": 60000 + } + ], + "enqueued": [], + "result": { + "hasProvider": true, + "text": "Das ist kein JSON.", + "cached": false, + "updatedAt": "2026-06-15T10:00:00.000Z" + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "general", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "6594f84d7983214b4c01ca79b2135e403689e025b342093c02d14642d2a122ee", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "general", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "cf0987263fc1febd6adeb676a38388116bfb35a0661621cbf6cff8629839c6b8", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + } + ] + }, + "unparseable-fr": { + "calls": [ + { + "system": [ + "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", + "", + "YOUR DATA (graded snapshot):", + "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", + "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", + "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", + "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", + "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", + "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", + "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", + "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", + "", + "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", + "Moves available (choose the few that genuinely apply):", + "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", + "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", + "(c) name a genuine, earned win or a steady streak when the data shows one;", + "(d) one watch-item worth keeping an eye on;", + "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", + "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", + "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", + "", + "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", + "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", + "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", + "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", + "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", + "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", + "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", + "", + "OPENING — lead with meaning, not the metric value", + "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", + "", + "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", + "", + "JUDGE THE DATA HONESTLY (never invent a trend):", + "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", + "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", + "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", + "", + "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", + "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", + "", + "METRIC IDENTIFIERS STAY OUT OF PROSE", + "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", + "", + "GLP-1 DOSE SAFETY (contract, not style)", + "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", + "", + "ACUTE RED FLAGS (contract, not style)", + "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", + "", + "GROUNDING — ZERO HALLUCINATIONS", + "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", + "", + "TONE — a warm, motivating advisor, never clinical or alarming", + "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", + "", + "NOT AN INSIGHT — never recite mechanics", + "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", + "", + "INTERPRET, DON'T ENUMERATE", + "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", + "", + "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", + "", + "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", + "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", + "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", + "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", + "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", + "", + "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in French: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", + "{ \"summary\": \"...\" }", + "", + "METRIC — OVERALL ASSESSMENT:", + "- The snapshot carries measurementSeries (a map per metric type with summary + graded series), medicationAdherence (summary + series), bloodPressureTargets (when present) and moodContext (when ≥ 3 days). dataCoverage.avgDaysBetweenMeasurements shows the measurement density.", + "- LENGTH (overrides the single-metric budget): this section spans up to six metrics — write 3-5 sentences, roughly 50-80 words. Still concise, no filler.", + "- Pull the available parameters into ONE coherent picture — do not list each separately. Name AT MOST two metrics by name (the most notable ones); mention the rest only when they deviate notably from their own mean. Prioritise: the most notable first, stable values only briefly.", + "- Same logic per metric: place the recent mean against the person's weekly/monthly mean and report only significant deviations from their own baseline.", + "- Acknowledge the positive explicitly — a stable or improving value is a real message, not filler.", + "- Mention associations only when an r-value is present in the snapshot and |r| > 0.4 — as an association, never a cause. Include mood as context, do not over-weight it.", + "- One message: even across all metrics, close with EXACTLY ONE most-important, doable suggestion (\"one thing\") ONLY when the overall picture implies one. When everything is steady and there is nothing useful to do, say so honestly and name one thing worth keeping an eye on instead of forcing a recommendation.", + "", + "OUTPUT LANGUAGE: Répondez en français. Utilisez le vouvoiement par défaut ; tutoyez si l'utilisateur tutoie en premier. Utilisez un vocabulaire de santé français naturel." + ], + "messages": [ + { + "role": "user", + "content": [ + "Date: 2026-06-15", + "OPENER HINT: Open with what this means for the user today, then the number behind it.", + "Write one short overall assessment across the available health metrics. Open with the overall read in plain words — how things are looking taken together, not a number (e.g. \"a steady stretch across the board\", \"one thing standing out this week\") — then bring in the one or two metrics that stand out as support, each placed against the person's own weekly/monthly baseline; never lead with a value. Close with the single most important doable step only when the overall picture genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the measurement count, density and recency.", + "", + "PREVIOUS ANALYSIS: none on file. Treat this as the user's first analysis for this domain — no improvement/regression delta to surface.", + "", + "", + "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der jüngsten Richtung (steigend / fallend / flach gegenüber der Baseline) eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", + "", + "DATENLAGE: 887 Tages-Buckets im Fenster, letzter Wert heute.", + "", + "", + "{\"locale\":\"fr\",\"generatedForDay\":\"2026-06-15\",\"interpretationHint\":\"Use trend direction and deltas. Prioritize the newest data if trends conflict. Consider dataCoverage for reliability assessment.\",\"dataCoverage\":{\"totalMeasurements\":887,\"totalSpanDays\":140,\"newestMeasurementDaysAgo\":0,\"avgDaysBetweenMeasurements\":0.2},\"measurementSeries\":{\"WEIGHT\":{\"summary\":{\"points\":70,\"start\":84.2,\"end\":82.38,\"delta\":-1.82,\"mean\":83.19,\"min\":82.03,\"max\":84.48},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":83.6,\"max\":83.6,\"mean\":83.6,\"n\":1},{\"date\":\"2026-05-28\",\"min\":83.75,\"max\":83.75,\"mean\":83.75,\"n\":1},{\"date\":\"2026-05-30\",\"min\":83.94,\"max\":83.94,\"mean\":83.94,\"n\":1},{\"date\":\"2026-06-01\",\"min\":84.12,\"max\":84.12,\"mean\":84.12,\"n\":1},{\"date\":\"2026-06-03\",\"min\":84.29,\"max\":84.29,\"mean\":84.29,\"n\":1},{\"date\":\"2026-06-05\",\"min\":84.41,\"max\":84.41,\"mean\":84.41,\"n\":1},{\"date\":\"2026-06-07\",\"min\":84.48,\"max\":84.48,\"mean\":84.48,\"n\":1},{\"date\":\"2026-06-09\",\"min\":84.48,\"max\":84.48,\"mean\":84.48,\"n\":1},{\"date\":\"2026-06-11\",\"min\":84.43,\"max\":84.43,\"mean\":84.43,\"n\":1},{\"date\":\"2026-06-13\",\"min\":84.33,\"max\":84.33,\"mean\":84.33,\"n\":1},{\"date\":\"2026-06-15\",\"min\":84.2,\"max\":84.2,\"mean\":84.2,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":82.38,\"max\":82.51,\"mean\":82.45,\"n\":2},{\"month\":\"2026-02\",\"min\":82.03,\"max\":82.73,\"mean\":82.35,\"n\":14},{\"month\":\"2026-03\",\"min\":82.65,\"max\":83.07,\"mean\":82.92,\"n\":8}]}},\"BLOOD_PRESSURE_SYS\":{\"summary\":{\"points\":94,\"start\":128,\"end\":125,\"delta\":-3,\"mean\":125.39,\"min\":116,\"max\":134},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":119,\"max\":119,\"mean\":119,\"n\":1},{\"date\":\"2026-05-28\",\"min\":121,\"max\":121,\"mean\":121,\"n\":1},{\"date\":\"2026-05-30\",\"min\":126,\"max\":126,\"mean\":126,\"n\":1},{\"date\":\"2026-05-31\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-02\",\"min\":125,\"max\":125,\"mean\":125,\"n\":1},{\"date\":\"2026-06-03\",\"min\":127,\"max\":127,\"mean\":127,\"n\":1},{\"date\":\"2026-06-05\",\"min\":130,\"max\":130,\"mean\":130,\"n\":1},{\"date\":\"2026-06-06\",\"min\":132,\"max\":132,\"mean\":132,\"n\":1},{\"date\":\"2026-06-08\",\"min\":134,\"max\":134,\"mean\":134,\"n\":1},{\"date\":\"2026-06-09\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-11\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-12\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-14\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-15\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"month\":\"2026-02\",\"min\":116,\"max\":134,\"mean\":124.22,\"n\":18},{\"month\":\"2026-03\",\"min\":124,\"max\":133,\"mean\":128.18,\"n\":11}]}},\"BLOOD_PRESSURE_DIA\":{\"summary\":{\"points\":94,\"start\":82,\"end\":80,\"delta\":-2,\"mean\":80.31,\"min\":74,\"max\":86},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":76,\"max\":76,\"mean\":76,\"n\":1},{\"date\":\"2026-05-28\",\"min\":77,\"max\":77,\"mean\":77,\"n\":1},{\"date\":\"2026-05-30\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-05-31\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-02\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-03\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-05\",\"min\":86,\"max\":86,\"mean\":86,\"n\":1},{\"date\":\"2026-06-06\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-08\",\"min\":84,\"max\":84,\"mean\":84,\"n\":1},{\"date\":\"2026-06-09\",\"min\":85,\"max\":85,\"mean\":85,\"n\":1},{\"date\":\"2026-06-11\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-12\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-14\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-15\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":80,\"max\":86,\"mean\":83,\"n\":4},{\"month\":\"2026-02\",\"min\":75,\"max\":85,\"mean\":79.28,\"n\":18},{\"month\":\"2026-03\",\"min\":78,\"max\":86,\"mean\":82,\"n\":11}]}},\"PULSE\":{\"summary\":{\"points\":122,\"start\":70,\"end\":68,\"delta\":-2,\"mean\":70.13,\"min\":65,\"max\":75},\"series\":{\"recent\":[{\"date\":\"2026-05-25\",\"min\":62,\"max\":74,\"mean\":68,\"n\":2},{\"date\":\"2026-05-26\",\"min\":62,\"max\":74,\"mean\":66,\"n\":3},{\"date\":\"2026-05-27\",\"min\":63,\"max\":75,\"mean\":67,\"n\":3},{\"date\":\"2026-05-28\",\"min\":64,\"max\":76,\"mean\":68,\"n\":3},{\"date\":\"2026-05-29\",\"min\":65,\"max\":77,\"mean\":69,\"n\":3},{\"date\":\"2026-05-30\",\"min\":66,\"max\":78,\"mean\":70,\"n\":3},{\"date\":\"2026-05-31\",\"min\":67,\"max\":79,\"mean\":71,\"n\":3},{\"date\":\"2026-06-01\",\"min\":68,\"max\":80,\"mean\":72,\"n\":3},{\"date\":\"2026-06-02\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-03\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-04\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-05\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-06\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-07\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-08\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-09\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-10\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-11\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-12\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-13\",\"min\":68,\"max\":80,\"mean\":72,\"n\":3},{\"date\":\"2026-06-14\",\"min\":67,\"max\":79,\"mean\":71,\"n\":3},{\"date\":\"2026-06-15\",\"min\":66,\"max\":66,\"mean\":66,\"n\":1}],\"monthly\":[{\"month\":\"2026-02\",\"min\":61,\"max\":81,\"mean\":67.67,\"n\":44},{\"month\":\"2026-03\",\"min\":63,\"max\":83,\"mean\":72.25,\"n\":46}]}},\"SLEEP_DURATION\":{\"summary\":{\"points\":89,\"start\":417,\"end\":380,\"delta\":-37,\"mean\":410.85,\"min\":375,\"max\":445},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":384,\"max\":384,\"mean\":384,\"n\":1},{\"date\":\"2026-05-27\",\"min\":389,\"max\":389,\"mean\":389,\"n\":1},{\"date\":\"2026-05-28\",\"min\":395,\"max\":395,\"mean\":395,\"n\":1},{\"date\":\"2026-05-29\",\"min\":401,\"max\":401,\"mean\":401,\"n\":1},{\"date\":\"2026-05-30\",\"min\":408,\"max\":408,\"mean\":408,\"n\":1},{\"date\":\"2026-05-31\",\"min\":415,\"max\":415,\"mean\":415,\"n\":1},{\"date\":\"2026-06-01\",\"min\":422,\"max\":422,\"mean\":422,\"n\":1},{\"date\":\"2026-06-02\",\"min\":428,\"max\":428,\"mean\":428,\"n\":1},{\"date\":\"2026-06-03\",\"min\":434,\"max\":434,\"mean\":434,\"n\":1},{\"date\":\"2026-06-04\",\"min\":438,\"max\":438,\"mean\":438,\"n\":1},{\"date\":\"2026-06-05\",\"min\":442,\"max\":442,\"mean\":442,\"n\":1},{\"date\":\"2026-06-06\",\"min\":444,\"max\":444,\"mean\":444,\"n\":1},{\"date\":\"2026-06-07\",\"min\":445,\"max\":445,\"mean\":445,\"n\":1},{\"date\":\"2026-06-08\",\"min\":444,\"max\":444,\"mean\":444,\"n\":1},{\"date\":\"2026-06-09\",\"min\":443,\"max\":443,\"mean\":443,\"n\":1},{\"date\":\"2026-06-10\",\"min\":439,\"max\":439,\"mean\":439,\"n\":1},{\"date\":\"2026-06-11\",\"min\":435,\"max\":435,\"mean\":435,\"n\":1},{\"date\":\"2026-06-12\",\"min\":430,\"max\":430,\"mean\":430,\"n\":1},{\"date\":\"2026-06-13\",\"min\":424,\"max\":424,\"mean\":424,\"n\":1},{\"date\":\"2026-06-14\",\"min\":417,\"max\":417,\"mean\":417,\"n\":1}],\"monthly\":[]}},\"BLOOD_GLUCOSE\":{\"summary\":{\"points\":35,\"start\":96,\"end\":108,\"delta\":12,\"mean\":98.46,\"min\":87,\"max\":109},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":91,\"max\":91,\"mean\":91,\"n\":1},{\"date\":\"2026-05-30\",\"min\":99,\"max\":99,\"mean\":99,\"n\":1},{\"date\":\"2026-06-03\",\"min\":102,\"max\":102,\"mean\":102,\"n\":1},{\"date\":\"2026-06-07\",\"min\":107,\"max\":107,\"mean\":107,\"n\":1},{\"date\":\"2026-06-11\",\"min\":106,\"max\":106,\"mean\":106,\"n\":1},{\"date\":\"2026-06-15\",\"min\":96,\"max\":96,\"mean\":96,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":108,\"max\":108,\"mean\":108,\"n\":1},{\"month\":\"2026-02\",\"min\":88,\"max\":105,\"mean\":96.71,\"n\":7},{\"month\":\"2026-03\",\"min\":94,\"max\":108,\"mean\":101.75,\"n\":4}]}},\"RESTING_HEART_RATE\":{\"summary\":{\"points\":140,\"start\":58,\"end\":62,\"delta\":4,\"mean\":59.71,\"min\":55,\"max\":64},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":56,\"max\":56,\"mean\":56,\"n\":1},{\"date\":\"2026-05-27\",\"min\":59,\"max\":59,\"mean\":59,\"n\":1},{\"date\":\"2026-05-28\",\"min\":59,\"max\":59,\"mean\":59,\"n\":1},{\"date\":\"2026-05-29\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1},{\"date\":\"2026-05-30\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1},{\"date\":\"2026-05-31\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-01\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-02\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-03\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-04\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-05\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-06\",\"min\":62,\"max\":62,\"mean\":62,\"n\":1},{\"date\":\"2026-06-07\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-08\",\"min\":64,\"max\":64,\"mean\":64,\"n\":1},{\"date\":\"2026-06-09\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-10\",\"min\":62,\"max\":62,\"mean\":62,\"n\":1},{\"date\":\"2026-06-11\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-12\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-13\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-14\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-15\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":61,\"max\":64,\"mean\":62,\"n\":5},{\"month\":\"2026-02\",\"min\":55,\"max\":64,\"mean\":59.18,\"n\":28},{\"month\":\"2026-03\",\"min\":57,\"max\":64,\"mean\":60.94,\"n\":16}]}}},\"medicationAdherence\":{\"summary\":{\"points\":88,\"start\":100,\"end\":100,\"delta\":0,\"mean\":90.91,\"min\":0,\"max\":100},\"series\":{\"recent\":[{\"date\":\"2026-05-25\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-26\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-27\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-28\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-29\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-30\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-31\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-01\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-03\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-04\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-05\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-06\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-07\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-08\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-09\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-10\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-12\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-13\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-14\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1}],\"monthly\":[{\"month\":\"2026-03\",\"min\":100,\"max\":100,\"mean\":100,\"n\":7}]}},\"bloodPressureTargets\":{\"systolic\":{\"min\":120,\"max\":129},\"diastolic\":{\"min\":70,\"max\":79},\"inTargetPctLast30DailyPoints\":40},\"moodContext\":{\"points\":70,\"mean\":3.03,\"latest\":1,\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1}],\"monthly\":[]}}}" + ] + } + ], + "temperature": 0.45, + "maxTokens": 250, + "seed": 1618033, + "responseFormat": "json", + "timeoutMs": 60000 + } + ], + "enqueued": [], + "result": { + "hasProvider": false, + "text": "Aucune évaluation disponible pour le moment. Ces chiffres deviennent lisibles sur plusieurs semaines plutôt que sur des journées isolées, et relevés à des moments constants pour rester comparables. Le signal qui compte, c’est plusieurs indicateurs qui dérivent en même temps dans le même sens défavorable, pas une valeur un jour donné.", + "cached": true, + "updatedAt": null + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "general", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "6594f84d7983214b4c01ca79b2135e403689e025b342093c02d14642d2a122ee", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "general", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "cf0987263fc1febd6adeb676a38388116bfb35a0661621cbf6cff8629839c6b8", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "general", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + } + ] + }, + "blocked-es": { + "calls": [ + { + "system": [ + "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", + "", + "YOUR DATA (graded snapshot):", + "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", + "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", + "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", + "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", + "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", + "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", + "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", + "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", + "", + "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", + "Moves available (choose the few that genuinely apply):", + "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", + "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", + "(c) name a genuine, earned win or a steady streak when the data shows one;", + "(d) one watch-item worth keeping an eye on;", + "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", + "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", + "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", + "", + "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", + "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", + "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", + "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", + "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", + "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", + "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", + "", + "OPENING — lead with meaning, not the metric value", + "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", + "", + "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", + "", + "JUDGE THE DATA HONESTLY (never invent a trend):", + "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", + "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", + "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", + "", + "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", + "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", + "", + "METRIC IDENTIFIERS STAY OUT OF PROSE", + "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", + "", + "GLP-1 DOSE SAFETY (contract, not style)", + "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", + "", + "ACUTE RED FLAGS (contract, not style)", + "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", + "", + "GROUNDING — ZERO HALLUCINATIONS", + "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", + "", + "TONE — a warm, motivating advisor, never clinical or alarming", + "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", + "", + "NOT AN INSIGHT — never recite mechanics", + "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", + "", + "INTERPRET, DON'T ENUMERATE", + "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", + "", + "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", + "", + "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", + "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", + "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", + "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", + "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", + "", + "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in Spanish: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", + "{ \"summary\": \"...\" }", + "", + "METRIC — OVERALL ASSESSMENT:", + "- The snapshot carries measurementSeries (a map per metric type with summary + graded series), medicationAdherence (summary + series), bloodPressureTargets (when present) and moodContext (when ≥ 3 days). dataCoverage.avgDaysBetweenMeasurements shows the measurement density.", + "- LENGTH (overrides the single-metric budget): this section spans up to six metrics — write 3-5 sentences, roughly 50-80 words. Still concise, no filler.", + "- Pull the available parameters into ONE coherent picture — do not list each separately. Name AT MOST two metrics by name (the most notable ones); mention the rest only when they deviate notably from their own mean. Prioritise: the most notable first, stable values only briefly.", + "- Same logic per metric: place the recent mean against the person's weekly/monthly mean and report only significant deviations from their own baseline.", + "- Acknowledge the positive explicitly — a stable or improving value is a real message, not filler.", + "- Mention associations only when an r-value is present in the snapshot and |r| > 0.4 — as an association, never a cause. Include mood as context, do not over-weight it.", + "- One message: even across all metrics, close with EXACTLY ONE most-important, doable suggestion (\"one thing\") ONLY when the overall picture implies one. When everything is steady and there is nothing useful to do, say so honestly and name one thing worth keeping an eye on instead of forcing a recommendation.", + "", + "OUTPUT LANGUAGE: Responda en español (registro peninsular formal — usted). Use un vocabulario sanitario en español natural." + ], + "messages": [ + { + "role": "user", + "content": [ + "Date: 2026-06-15", + "OPENER HINT: Open with what this means for the user today, then the number behind it.", + "Write one short overall assessment across the available health metrics. Open with the overall read in plain words — how things are looking taken together, not a number (e.g. \"a steady stretch across the board\", \"one thing standing out this week\") — then bring in the one or two metrics that stand out as support, each placed against the person's own weekly/monthly baseline; never lead with a value. Close with the single most important doable step only when the overall picture genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the measurement count, density and recency.", + "", + "PREVIOUS ANALYSIS: none on file. Treat this as the user's first analysis for this domain — no improvement/regression delta to surface.", + "", + "", + "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der jüngsten Richtung (steigend / fallend / flach gegenüber der Baseline) eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", + "", + "DATENLAGE: 887 Tages-Buckets im Fenster, letzter Wert heute.", + "", + "", + "{\"locale\":\"es\",\"generatedForDay\":\"2026-06-15\",\"interpretationHint\":\"Use trend direction and deltas. Prioritize the newest data if trends conflict. Consider dataCoverage for reliability assessment.\",\"dataCoverage\":{\"totalMeasurements\":887,\"totalSpanDays\":140,\"newestMeasurementDaysAgo\":0,\"avgDaysBetweenMeasurements\":0.2},\"measurementSeries\":{\"WEIGHT\":{\"summary\":{\"points\":70,\"start\":84.2,\"end\":82.38,\"delta\":-1.82,\"mean\":83.19,\"min\":82.03,\"max\":84.48},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":83.6,\"max\":83.6,\"mean\":83.6,\"n\":1},{\"date\":\"2026-05-28\",\"min\":83.75,\"max\":83.75,\"mean\":83.75,\"n\":1},{\"date\":\"2026-05-30\",\"min\":83.94,\"max\":83.94,\"mean\":83.94,\"n\":1},{\"date\":\"2026-06-01\",\"min\":84.12,\"max\":84.12,\"mean\":84.12,\"n\":1},{\"date\":\"2026-06-03\",\"min\":84.29,\"max\":84.29,\"mean\":84.29,\"n\":1},{\"date\":\"2026-06-05\",\"min\":84.41,\"max\":84.41,\"mean\":84.41,\"n\":1},{\"date\":\"2026-06-07\",\"min\":84.48,\"max\":84.48,\"mean\":84.48,\"n\":1},{\"date\":\"2026-06-09\",\"min\":84.48,\"max\":84.48,\"mean\":84.48,\"n\":1},{\"date\":\"2026-06-11\",\"min\":84.43,\"max\":84.43,\"mean\":84.43,\"n\":1},{\"date\":\"2026-06-13\",\"min\":84.33,\"max\":84.33,\"mean\":84.33,\"n\":1},{\"date\":\"2026-06-15\",\"min\":84.2,\"max\":84.2,\"mean\":84.2,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":82.38,\"max\":82.51,\"mean\":82.45,\"n\":2},{\"month\":\"2026-02\",\"min\":82.03,\"max\":82.73,\"mean\":82.35,\"n\":14},{\"month\":\"2026-03\",\"min\":82.65,\"max\":83.07,\"mean\":82.92,\"n\":8}]}},\"BLOOD_PRESSURE_SYS\":{\"summary\":{\"points\":94,\"start\":128,\"end\":125,\"delta\":-3,\"mean\":125.39,\"min\":116,\"max\":134},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":119,\"max\":119,\"mean\":119,\"n\":1},{\"date\":\"2026-05-28\",\"min\":121,\"max\":121,\"mean\":121,\"n\":1},{\"date\":\"2026-05-30\",\"min\":126,\"max\":126,\"mean\":126,\"n\":1},{\"date\":\"2026-05-31\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-02\",\"min\":125,\"max\":125,\"mean\":125,\"n\":1},{\"date\":\"2026-06-03\",\"min\":127,\"max\":127,\"mean\":127,\"n\":1},{\"date\":\"2026-06-05\",\"min\":130,\"max\":130,\"mean\":130,\"n\":1},{\"date\":\"2026-06-06\",\"min\":132,\"max\":132,\"mean\":132,\"n\":1},{\"date\":\"2026-06-08\",\"min\":134,\"max\":134,\"mean\":134,\"n\":1},{\"date\":\"2026-06-09\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-11\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-12\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-14\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-15\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"month\":\"2026-02\",\"min\":116,\"max\":134,\"mean\":124.22,\"n\":18},{\"month\":\"2026-03\",\"min\":124,\"max\":133,\"mean\":128.18,\"n\":11}]}},\"BLOOD_PRESSURE_DIA\":{\"summary\":{\"points\":94,\"start\":82,\"end\":80,\"delta\":-2,\"mean\":80.31,\"min\":74,\"max\":86},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":76,\"max\":76,\"mean\":76,\"n\":1},{\"date\":\"2026-05-28\",\"min\":77,\"max\":77,\"mean\":77,\"n\":1},{\"date\":\"2026-05-30\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-05-31\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-02\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-03\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-05\",\"min\":86,\"max\":86,\"mean\":86,\"n\":1},{\"date\":\"2026-06-06\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-08\",\"min\":84,\"max\":84,\"mean\":84,\"n\":1},{\"date\":\"2026-06-09\",\"min\":85,\"max\":85,\"mean\":85,\"n\":1},{\"date\":\"2026-06-11\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-12\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-14\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-15\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":80,\"max\":86,\"mean\":83,\"n\":4},{\"month\":\"2026-02\",\"min\":75,\"max\":85,\"mean\":79.28,\"n\":18},{\"month\":\"2026-03\",\"min\":78,\"max\":86,\"mean\":82,\"n\":11}]}},\"PULSE\":{\"summary\":{\"points\":122,\"start\":70,\"end\":68,\"delta\":-2,\"mean\":70.13,\"min\":65,\"max\":75},\"series\":{\"recent\":[{\"date\":\"2026-05-25\",\"min\":62,\"max\":74,\"mean\":68,\"n\":2},{\"date\":\"2026-05-26\",\"min\":62,\"max\":74,\"mean\":66,\"n\":3},{\"date\":\"2026-05-27\",\"min\":63,\"max\":75,\"mean\":67,\"n\":3},{\"date\":\"2026-05-28\",\"min\":64,\"max\":76,\"mean\":68,\"n\":3},{\"date\":\"2026-05-29\",\"min\":65,\"max\":77,\"mean\":69,\"n\":3},{\"date\":\"2026-05-30\",\"min\":66,\"max\":78,\"mean\":70,\"n\":3},{\"date\":\"2026-05-31\",\"min\":67,\"max\":79,\"mean\":71,\"n\":3},{\"date\":\"2026-06-01\",\"min\":68,\"max\":80,\"mean\":72,\"n\":3},{\"date\":\"2026-06-02\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-03\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-04\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-05\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-06\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-07\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-08\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-09\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-10\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-11\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-12\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-13\",\"min\":68,\"max\":80,\"mean\":72,\"n\":3},{\"date\":\"2026-06-14\",\"min\":67,\"max\":79,\"mean\":71,\"n\":3},{\"date\":\"2026-06-15\",\"min\":66,\"max\":66,\"mean\":66,\"n\":1}],\"monthly\":[{\"month\":\"2026-02\",\"min\":61,\"max\":81,\"mean\":67.67,\"n\":44},{\"month\":\"2026-03\",\"min\":63,\"max\":83,\"mean\":72.25,\"n\":46}]}},\"SLEEP_DURATION\":{\"summary\":{\"points\":89,\"start\":417,\"end\":380,\"delta\":-37,\"mean\":410.85,\"min\":375,\"max\":445},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":384,\"max\":384,\"mean\":384,\"n\":1},{\"date\":\"2026-05-27\",\"min\":389,\"max\":389,\"mean\":389,\"n\":1},{\"date\":\"2026-05-28\",\"min\":395,\"max\":395,\"mean\":395,\"n\":1},{\"date\":\"2026-05-29\",\"min\":401,\"max\":401,\"mean\":401,\"n\":1},{\"date\":\"2026-05-30\",\"min\":408,\"max\":408,\"mean\":408,\"n\":1},{\"date\":\"2026-05-31\",\"min\":415,\"max\":415,\"mean\":415,\"n\":1},{\"date\":\"2026-06-01\",\"min\":422,\"max\":422,\"mean\":422,\"n\":1},{\"date\":\"2026-06-02\",\"min\":428,\"max\":428,\"mean\":428,\"n\":1},{\"date\":\"2026-06-03\",\"min\":434,\"max\":434,\"mean\":434,\"n\":1},{\"date\":\"2026-06-04\",\"min\":438,\"max\":438,\"mean\":438,\"n\":1},{\"date\":\"2026-06-05\",\"min\":442,\"max\":442,\"mean\":442,\"n\":1},{\"date\":\"2026-06-06\",\"min\":444,\"max\":444,\"mean\":444,\"n\":1},{\"date\":\"2026-06-07\",\"min\":445,\"max\":445,\"mean\":445,\"n\":1},{\"date\":\"2026-06-08\",\"min\":444,\"max\":444,\"mean\":444,\"n\":1},{\"date\":\"2026-06-09\",\"min\":443,\"max\":443,\"mean\":443,\"n\":1},{\"date\":\"2026-06-10\",\"min\":439,\"max\":439,\"mean\":439,\"n\":1},{\"date\":\"2026-06-11\",\"min\":435,\"max\":435,\"mean\":435,\"n\":1},{\"date\":\"2026-06-12\",\"min\":430,\"max\":430,\"mean\":430,\"n\":1},{\"date\":\"2026-06-13\",\"min\":424,\"max\":424,\"mean\":424,\"n\":1},{\"date\":\"2026-06-14\",\"min\":417,\"max\":417,\"mean\":417,\"n\":1}],\"monthly\":[]}},\"BLOOD_GLUCOSE\":{\"summary\":{\"points\":35,\"start\":96,\"end\":108,\"delta\":12,\"mean\":98.46,\"min\":87,\"max\":109},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":91,\"max\":91,\"mean\":91,\"n\":1},{\"date\":\"2026-05-30\",\"min\":99,\"max\":99,\"mean\":99,\"n\":1},{\"date\":\"2026-06-03\",\"min\":102,\"max\":102,\"mean\":102,\"n\":1},{\"date\":\"2026-06-07\",\"min\":107,\"max\":107,\"mean\":107,\"n\":1},{\"date\":\"2026-06-11\",\"min\":106,\"max\":106,\"mean\":106,\"n\":1},{\"date\":\"2026-06-15\",\"min\":96,\"max\":96,\"mean\":96,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":108,\"max\":108,\"mean\":108,\"n\":1},{\"month\":\"2026-02\",\"min\":88,\"max\":105,\"mean\":96.71,\"n\":7},{\"month\":\"2026-03\",\"min\":94,\"max\":108,\"mean\":101.75,\"n\":4}]}},\"RESTING_HEART_RATE\":{\"summary\":{\"points\":140,\"start\":58,\"end\":62,\"delta\":4,\"mean\":59.71,\"min\":55,\"max\":64},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":56,\"max\":56,\"mean\":56,\"n\":1},{\"date\":\"2026-05-27\",\"min\":59,\"max\":59,\"mean\":59,\"n\":1},{\"date\":\"2026-05-28\",\"min\":59,\"max\":59,\"mean\":59,\"n\":1},{\"date\":\"2026-05-29\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1},{\"date\":\"2026-05-30\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1},{\"date\":\"2026-05-31\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-01\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-02\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-03\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-04\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-05\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-06\",\"min\":62,\"max\":62,\"mean\":62,\"n\":1},{\"date\":\"2026-06-07\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-08\",\"min\":64,\"max\":64,\"mean\":64,\"n\":1},{\"date\":\"2026-06-09\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-10\",\"min\":62,\"max\":62,\"mean\":62,\"n\":1},{\"date\":\"2026-06-11\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-12\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-13\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-14\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-15\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":61,\"max\":64,\"mean\":62,\"n\":5},{\"month\":\"2026-02\",\"min\":55,\"max\":64,\"mean\":59.18,\"n\":28},{\"month\":\"2026-03\",\"min\":57,\"max\":64,\"mean\":60.94,\"n\":16}]}}},\"medicationAdherence\":{\"summary\":{\"points\":88,\"start\":100,\"end\":100,\"delta\":0,\"mean\":90.91,\"min\":0,\"max\":100},\"series\":{\"recent\":[{\"date\":\"2026-05-25\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-26\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-27\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-28\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-29\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-30\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-31\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-01\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-03\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-04\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-05\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-06\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-07\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-08\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-09\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-10\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-12\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-13\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-14\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1}],\"monthly\":[{\"month\":\"2026-03\",\"min\":100,\"max\":100,\"mean\":100,\"n\":7}]}},\"bloodPressureTargets\":{\"systolic\":{\"min\":120,\"max\":129},\"diastolic\":{\"min\":70,\"max\":79},\"inTargetPctLast30DailyPoints\":40},\"moodContext\":{\"points\":70,\"mean\":3.03,\"latest\":1,\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1}],\"monthly\":[]}}}" + ] + } + ], + "temperature": 0.45, + "maxTokens": 250, + "seed": 1618033, + "responseFormat": "json", + "timeoutMs": 60000 + } + ], + "enqueued": [], + "result": { + "hasProvider": false, + "text": "Ahora mismo no hay ninguna valoración disponible. Estos números se vuelven legibles a lo largo de varias semanas y no en días sueltos, y tomados a horas constantes para que sigan siendo comparables. La señal que cuenta son varios indicadores derivando a la vez en la misma dirección desfavorable, no un valor en un día.", + "cached": true, + "updatedAt": null + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "general", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "6594f84d7983214b4c01ca79b2135e403689e025b342093c02d14642d2a122ee", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "general", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "cf0987263fc1febd6adeb676a38388116bfb35a0661621cbf6cff8629839c6b8", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "general", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "general", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + } + ] + }, + "bare-error-en": { + "calls": [ + { + "system": [ + "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", + "", + "YOUR DATA (graded snapshot):", + "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", + "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", + "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", + "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", + "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", + "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", + "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", + "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", + "", + "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", + "Moves available (choose the few that genuinely apply):", + "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", + "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", + "(c) name a genuine, earned win or a steady streak when the data shows one;", + "(d) one watch-item worth keeping an eye on;", + "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", + "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", + "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", + "", + "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", + "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", + "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", + "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", + "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", + "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", + "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", + "", + "OPENING — lead with meaning, not the metric value", + "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", + "", + "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", + "", + "JUDGE THE DATA HONESTLY (never invent a trend):", + "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", + "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", + "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", + "", + "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", + "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", + "", + "METRIC IDENTIFIERS STAY OUT OF PROSE", + "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", + "", + "GLP-1 DOSE SAFETY (contract, not style)", + "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", + "", + "ACUTE RED FLAGS (contract, not style)", + "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", + "", + "GROUNDING — ZERO HALLUCINATIONS", + "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", + "", + "TONE — a warm, motivating advisor, never clinical or alarming", + "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", + "", + "NOT AN INSIGHT — never recite mechanics", + "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", + "", + "INTERPRET, DON'T ENUMERATE", + "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", + "", + "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", + "", + "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", + "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", + "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", + "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", + "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", + "", + "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in English: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", + "{ \"summary\": \"...\" }", + "", + "METRIC — OVERALL ASSESSMENT:", + "- The snapshot carries measurementSeries (a map per metric type with summary + graded series), medicationAdherence (summary + series), bloodPressureTargets (when present) and moodContext (when ≥ 3 days). dataCoverage.avgDaysBetweenMeasurements shows the measurement density.", + "- LENGTH (overrides the single-metric budget): this section spans up to six metrics — write 3-5 sentences, roughly 50-80 words. Still concise, no filler.", + "- Pull the available parameters into ONE coherent picture — do not list each separately. Name AT MOST two metrics by name (the most notable ones); mention the rest only when they deviate notably from their own mean. Prioritise: the most notable first, stable values only briefly.", + "- Same logic per metric: place the recent mean against the person's weekly/monthly mean and report only significant deviations from their own baseline.", + "- Acknowledge the positive explicitly — a stable or improving value is a real message, not filler.", + "- Mention associations only when an r-value is present in the snapshot and |r| > 0.4 — as an association, never a cause. Include mood as context, do not over-weight it.", + "- One message: even across all metrics, close with EXACTLY ONE most-important, doable suggestion (\"one thing\") ONLY when the overall picture implies one. When everything is steady and there is nothing useful to do, say so honestly and name one thing worth keeping an eye on instead of forcing a recommendation." + ], + "messages": [ + { + "role": "user", + "content": [ + "Date: 2026-06-15", + "OPENER HINT: Open with what this means for the user today, then the number behind it.", + "Write one short overall assessment across the available health metrics. Open with the overall read in plain words — how things are looking taken together, not a number (e.g. \"a steady stretch across the board\", \"one thing standing out this week\") — then bring in the one or two metrics that stand out as support, each placed against the person's own weekly/monthly baseline; never lead with a value. Close with the single most important doable step only when the overall picture genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the measurement count, density and recency.", + "", + "PREVIOUS ANALYSIS: none on file. Treat this as the user's first analysis for this domain — no improvement/regression delta to surface.", + "", + "", + "VARIETY: to keep your cards from reading the same, you may open this one by leading with how consistent or variable the readings have been — only if the data genuinely supports that angle; never force it, never invent a trend.", + "", + "DATA STRENGTH: 0 day-buckets in the window, no dated reading. — too few points for a reliable trend; hedge accordingly and avoid claiming a direction.", + "", + "", + "{\"locale\":\"en\",\"generatedForDay\":\"2026-06-15\",\"interpretationHint\":\"Use trend direction and deltas. Prioritize the newest data if trends conflict. Consider dataCoverage for reliability assessment.\",\"dataCoverage\":{\"totalMeasurements\":0,\"totalSpanDays\":0,\"newestMeasurementDaysAgo\":null,\"avgDaysBetweenMeasurements\":null},\"measurementSeries\":{},\"medicationAdherence\":{\"summary\":null,\"series\":{\"recent\":[],\"weekly\":[],\"monthly\":[],\"yearly\":[]}},\"bloodPressureTargets\":null,\"moodContext\":null}" + ] + } + ], + "temperature": 0.45, + "maxTokens": 250, + "seed": 1618033, + "responseFormat": "json", + "timeoutMs": 60000 + } + ], + "enqueued": [], + "result": { + "hasProvider": false, + "text": "No assessment on this one right now. These numbers become readable across several weeks rather than single days, and taken at consistent times so they stay comparable. The signal that counts is several metrics drifting the same unfavourable way at once, not one value on one day.", + "cached": true, + "updatedAt": null + }, + "notes": [ + { + "userId": "parity-bare", + "metric": "general", + "locale": "en", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "error" + }, + { + "userId": "parity-rich", + "metric": "general", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "6594f84d7983214b4c01ca79b2135e403689e025b342093c02d14642d2a122ee", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "general", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "cf0987263fc1febd6adeb676a38388116bfb35a0661621cbf6cff8629839c6b8", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "general", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "general", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + } + ] + }, + "bare-error-de": { + "calls": [ + { + "system": [ + "Du schreibst eine kurze, persönliche Einschätzung zu EINER Gesundheitsmetrik dieses Nutzers. Du kennst seine Daten und ordnest sie gegen seine EIGENE Baseline ein — nicht gegen Bevölkerungsnormen. Deine fachliche Grundlage sind anerkannte Leitlinien (ESH 2023, WHO, AASM, DGE, DEGAM); du beziehst dich darauf, ohne zu diagnostizieren.", + "", + "DEINE DATENGRUNDLAGE (graded snapshot):", + "- signal: der fertige Vergleich — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` ist das recent-Mittel; `baseline` ist der EIGENE längere Durchschnitt; `delta`/`deltaPct` sind die VORZEICHENBEHAFTETE Änderung gegenüber dieser Baseline; `spread` ist die normale Schwankung; `outsideNormalSwing` ist das vorab berechnete Urteil, ob die Änderung diese Schwankung übersteigt. Diese Werte sind bereits berechnet — nenne sie, rechne sie NICHT neu. `contributors[]` (nur Score-Komposite) benennt die 1–2 Teil-Scores, die den Score bewegt haben.", + "- summary: { points, start, end, delta, mean, min, max } — die Eckwerte des gesamten Lesefensters.", + "- series.recent[]: die jüngsten ~21 Tage, je ein Eintrag pro Tag ({ date, min, max, mean, n }).", + "- series.weekly[]: die ~10 Wochen davor, je ein ISO-Wochen-Aggregat.", + "- series.monthly[]: die ~12 Monate davor, je ein Monatsaggregat.", + "- series.yearly[]: alles ältere, je ein Jahr ({ year, mean, min, max, n, slope }).", + "- Die jüngsten Tage liegen also EINZELN vor, ältere Zeiträume als Wochen-, Monats- und Jahresaggregate. Ziehe die series nur für einen konkreten Tag heran; führe sonst mit dem signal-Block.", + "- Ein Slice kann leer sein, wenn die Historie kurz ist. Dann nicht so tun, als gäbe es ihn. Ist `signal.baseline` null, gibt es noch keine etablierte Baseline; sage das ehrlich, statt einen Vergleich zu erfinden.", + "", + "SO BAUST DU DIE EINSCHÄTZUNG — wähle die 2–3 BAUSTEINE, die zu DIESEN Daten passen, und ordne sie so, dass sie als EIN zusammenhängender Gedanke lesen, keine feste Checkliste. Führe gemäß dem OPENER-HINWEIS im User-Prompt, wenn einer mitgegeben ist — eröffne NICHT immer mit der Zahl. Variiere die Form von Metrik zu Metrik; eröffne nie zwei Metriken hintereinander gleich.", + "Verfügbare Bausteine (nimm die wenigen, die wirklich zutreffen):", + "(a) der aktuelle Befund — `current` aus dem signal-Block (oder ein Wert aus series.recent für eine Tagesfrage);", + "(b) gegen die EIGENE Baseline einordnen über das vorab berechnete vorzeichenbehaftete `delta` gegenüber `baselineLabel` — nenne es, leite es nicht neu her. Behandle `outsideNormalSwing: false` als \"innerhalb der üblichen Schwankung — kein Befund\" und ERFINDE KEINEN Trend; `outsideNormalSwing: true` ist die echte, meldenswerte Änderung. Ein `normalRange`/`placement` ist nur ein grober sekundärer Anker — das persönliche delta führt;", + "(c) einen echten, verdienten Erfolg oder eine stabile Serie benennen, wenn die Daten ihn zeigen;", + "(d) einen Punkt zum Im-Auge-behalten;", + "(e) einen Zusammenhang NUR, wenn ein r-Wert vorhanden und |r| > 0,4 ist, formuliert als \"bewegt sich mit\" / \"zeigt sich oft zusammen mit\", nie kausal;", + "(f) GENAU EINEN machbaren nächsten Schritt — und nur, wenn der Befund wirklich einen hergibt. Eine Botschaft = ein Verhalten, keine Liste. Ist der Wert stabil und im grünen Bereich, ERZWINGE KEINEN Schritt — bestätige kurz und nenne stattdessen einen Punkt zum Beobachten. Ein erfundener Schritt ist genau die Floskel, die wir vermeiden.", + "Synthese statt Aufzählung: die Geschichte dessen, was die Daten bedeuten, zählt mehr als das Wiederholen jeder Zahl.", + "", + "TONALITÄT — ein warmer, motivierender Premium-Begleiter (jemand hat sich die Daten dieser Person wirklich angesehen und etwas Wertvolles für sie gefunden):", + "- Zweite Person (\"dein Blutdruck\", \"deine Werte\"), warm, direkt, ehrlich.", + "- Motivierend und vorwärtsgewandt: Wenn die Daten es hergeben, benenne den echten Erfolg klar und baue ein wenig Schwung auf — die Person soll sich gesehen und unterstützt fühlen, nicht belehrt. Die Ermutigung muss durch die Zahlen VERDIENT sein, nie ein reflexhaftes Kompliment.", + "- Autonomie-unterstützend: \"kann helfen\", \"einen Versuch wert\", nie \"du musst\".", + "- Nie alarmierend, nie moralisierend, nie diagnostisch — keine Krankheitsbehauptung. Formuliere als begründete Beobachtung, nicht als ärztlichen Rat.", + "- Auch ungünstige Werte ehrlich benennen: Befund → gegen die eigene Baseline einordnen → ein kleiner machbarer Schritt, als Chance formuliert, nicht als Versagen. Nicht verharmlosen, nicht dramatisieren.", + "- Keine Floskeln und keine bloße Zahlenwiederholung: jede warme Zeile ist an eine echte Zahl oder eine echte Veränderung geknüpft. Ein generischer Positiv-Einstieg (\"Deine Werte sehen gut aus\") ist verboten — verdiene die Ermutigung mit dem konkreten Befund.", + "", + "ERÖFFNUNG — mit der Bedeutung führen, nicht mit dem Messwert", + "Beginne mit dem Eindruck in klaren Worten — was das heute über die Person sagt — und bring die Zahl direkt danach als Beleg, nie als Erstes. Eine Zeile, die mit einem Wert eröffnet (\"Dein Ruhepuls ist 61 …\"), liest sich wie ein Ablesen; dieselbe Zeile, die mit der Bedeutung eröffnet (\"Dein Herz läuft diese Woche etwas ruhiger — rund 61 …\"), liest sich wie ein Begleiter, der wirklich hingesehen hat. Jede Aussage bleibt an einer echten Zahl verankert; nur die REIHENFOLGE ändert sich, Bedeutung zuerst und die Zahl direkt dahinter. Das gilt, ob die Fläche eine Metrik abdeckt oder viele.", + "", + "LÄNGE: 2-4 Sätze, ca. 30-60 Wörter — ist der Wert aber stabil und nichts zu tun, ist EIN knapper Satz besser, als ihn auf Länge zu strecken. Knapp und hochwertig. Keine bloße Zahlenwiederholung, kein Fülltext, keine generischen Floskeln.", + "", + "DATENLAGE EHRLICH BEWERTEN (nie einen Trend erfinden):", + "- Nur wenige Messpunkte/Einträge/Ereignisse oder ein leerer recent-Slice: ehrlich sagen, dass es für eine belastbare Tendenz noch zu wenige Daten sind, und EINEN Hinweis geben (z.B. regelmäßiger erfassen). Keinen Trend behaupten. Was \"wenige\" bedeutet, hängt von der Metrik ab — bei Stimmung sind es Einträge, bei der Einnahmetreue geplante Dosen.", + "- Jüngste Messung deutlich älter als ~7 Tage (dataCoverage.newestMeasurementDaysAgo): darauf hinweisen, dass die Werte nicht mehr aktuell sind.", + "- Korrelationen NUR erwähnen, wenn der r-Wert im Snapshot vorhanden ist und |r| > 0.4 ist. Fehlt das Feld, keine Korrelation interpretieren oder erfinden. Immer als \"Zusammenhang\" formulieren, nie als \"Ursache\".", + "", + "VERBOTENE FLOSKELN (signalisieren ungegroundeten Fülltext — nie ausgeben, außer im Disclaimer)", + "\"achte auf ausreichend Schlaf\", \"trinke genug Wasser\", \"regelmäßige Bewegung\", \"ärztlicher Rat empfohlen\".", + "", + "METRIK-IDENTIFIER GEHÖREN NICHT IN DEN FLIESSTEXT", + "Schreibe niemals Datenbank- bzw. Enum-Namen wie \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" oder \"SLEEP_DURATION\" in nutzersichtbare Strings, und schreibe nie das wörtliche \"metric:\"-Chart-Token in den Fließtext. Verweise auf jede Metrik mit der natürlichsprachlichen Bezeichnung, die der Nutzer sieht — \"deine Systole\", \"dein Gewicht\", \"dein Puls\", \"deine Stimmung\", \"deine Medikamentenadhärenz\", \"dein Ruhepuls\", \"deine Schlafdauer\", \"deine Schritte\".", + "", + "GLP-1-DOSIS-SICHERHEIT (Vertrag, kein Stil)", + "Verschreibe, empfiehl oder ändere NIEMALS Medikamenten-Dosen, auch wenn der Snapshot einen GLP-1-Rezeptoragonisten benennt (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). Du darfst den benannten Wirkstoff und die aktuelle Titrationsstufe NENNEN (\"Woche 3 auf 7,5 mg\"), wenn der Snapshot sie trägt, aber schreibe nie \"erhöhe auf X mg\", \"erwäge die nächste Stufe Y mg\", \"bleibe auf Z mg\" oder eine Variante davon. Ein Plateau rahmt die nächste Entscheidung immer als Gespräch mit der behandelnden Ärztin. Bist du unsicher, ob etwas dosis-präskriptiv ist, behandle es so — und verweise an die Klinik.", + "", + "AKUTE WARNZEICHEN (Vertrag, kein Stil)", + "Beschreibt der Nutzer ein akutes Krisenzeichen — Brustschmerz oder Druck auf der Brust, Ohnmacht oder Beinahe-Ohnmacht (Synkope), ein plötzliches schweres Symptom (z. B. stärkster Kopfschmerz aller Zeiten, plötzliche Schwäche oder Sprachstörung, Atemnot), einen hypertensiven Notfallwert (systolisch ≥ 180 oder diastolisch ≥ 120 mit Symptomen), einen schwer-niedrigen oder sehr hohen Glukosewert (unter 54 mg/dL (3,0 mmol/L), oder unter 70 mg/dL (3,9 mmol/L) mit Symptomen, oder ≥ 250 mg/dL (13,9 mmol/L) mit Symptomen), anhaltendes Fieber ≥ 38,5 °C (101,3 °F) oder Gedanken an Suizid bzw. Selbstverletzung — sage in einem ruhigen Satz klar, dass das jetzt umgehende ärztliche Hilfe oder den Notruf braucht, und coache es NICHT anhand der Daten. Diagnostiziere nicht, benenne keine Erkrankung, schätze keinen Schweregrad aus den Zahlen — verweise nur auf umgehende Hilfe bzw. den Notruf und höre auf. Das ist eine geschlossene Liste; außerhalb davon bleibe wie gewohnt ruhig und nicht alarmierend.", + "", + "ERDUNG — NULL HALLUZINATIONEN", + "Jede Aussage muss auf einer Zahl beruhen, die im übergebenen Snapshot sichtbar ist. Lässt sich die Aussage keinem Snapshot-Feld zuordnen, lass sie weg und erfinde nie einen Messwert, um einer Anfrage zu entsprechen. Ordne jeden Wert gegen die EIGENE Baseline des Nutzers ein (jüngeres vs. längeres Mittel), nie gegen eine Bevölkerungsnorm. Nenne einen Zusammenhang nur, wenn der r-Wert vorhanden und |r| > 0,4 ist — immer als \"Zusammenhang\", nie als \"Ursache\".", + "", + "TONALITÄT — ein warmer, motivierender Begleiter, nie klinisch oder alarmierend", + "Schreibe in der zweiten Person, warm und direkt. Wenn die Daten es hergeben, benenne den echten Erfolg klar und baue ein wenig Schwung auf — die Person soll sich gesehen und unterstützt fühlen, nicht belehrt. Die Ermutigung muss durch die Zahlen VERDIENT sein, nie ein reflexhaftes Kompliment. Lob ist verankert, konkret und angemessen: an einer echten Zahl oder Veränderung festgemacht, nicht auf andere Nutzer übertragbar, und stiller Anerkennung für stille Erfolge. Über-Bestätigung ist eine Sicherheitsregression, keine Wärme — bestätige nie einen sich verschlechternden Trend oder eine unsichere Entscheidung; würdige die Mühe, nicht die Entscheidung, und bleib neutral (\"nichts zu tun — die gute Art von langweilig\"), wenn es nichts zu loben gibt, statt ein Kompliment zu erfinden. Sei autonomie-unterstützend (\"einen Versuch wert\", \"kann helfen\", nie \"du musst\"). Benenne auch ungünstige Werte ehrlich — Befund, dann gegen die eigene Baseline einordnen, dann ein kleiner machbarer Schritt, als Chance formuliert. Nie alarmierend, nie moralisierend, nie diagnostisch. Keine Floskeln und keine bloße Zahlenwiederholung. Beginne NICHT mit einem Kompliment über Datenmenge oder -qualität; verbotene Eröffnungen sind u.a. \"Datengrundlage ist sehr stark\", \"Your data foundation is strong\", \"Du hast eine solide Baseline\", \"Großartiger Datensatz\", ein generisches \"Deine Werte sehen gut aus\" und jede sinngemäße Umformulierung.", + "", + "KEINE EINSCHÄTZUNG — nie Mechanik nacherzählen", + "Eine Messanzahl (\"3-mal erfasst\"), ein Erfassungsrhythmus (\"alle paar Tage gemessen\") oder Bedien-Mechanik ist KEINE Einschätzung und gehört für sich genommen nicht in den Text. Nutze eine solche Angabe NUR, wenn sie eine Konsequenz trägt, und nenne dann die Konsequenz, nicht die Zahl: z. B. \"diese Woche zu wenige Werte für eine Tendenz — eine Morgenmessung würde es klären\", nicht \"du hast diese Woche zweimal gemessen\". Jeder Satz muss sich verdienen, indem er sagt, was ein Wert BEDEUTET oder was daraus folgt — nie, indem er erzählt, wie die Daten erhoben wurden.", + "", + "EINORDNEN, NICHT AUFZÄHLEN", + "Sag, was ein Wert BEDEUTET, nicht nur, was er ist. Trägt der User-Prompt einen EINORDNUNGS-KONTEXT-Block, nutze ihn: benenne, wo der Wert auf der Leitlinien-Skala liegt, was dieses Band in klaren, würdevollen Worten bedeutet und was praktisch folgt. Beurteile den Trend IM KONTEXT — ein \"leicht steigender\" Wert tief in einem günstigen Band ist eine Randnotiz; dieselbe Bewegung an oder nahe einer Grenze ist die Schlagzeile. Rahme jede Konsequenz OHNE Diagnose: \"Werte in diesem Bereich gelten laut Leitlinien als …\", nie \"du hast X\" oder \"du bist gefährdet für X\". Ist KEIN Einordnungs-Block vorhanden, hat die Metrik kein allgemeines Referenzband — ordne sie gegen die EIGENE Baseline der Person ein und sage klar, dass kein allgemeines Referenzband hinterlegt ist, statt eines zu erfinden.", + "", + "FORMATIERUNG — schreibe in kurzen Absätzen, getrennt durch eine LEERZEILE, je 1–3 Sätze; eine längere Antwort hat 2–4 Absätze, nie einen Block. Setze eine Leerzeile zwischen unterschiedliche Gedanken (z. B. \"wie es steht\" vs. \"eine Sache zum Ausprobieren\"). Ein stabiler Einzeiler bleibt EIN Absatz — strecke nicht auf einen zweiten. Wenn du wirklich drei oder mehr parallele Punkte aufzählst (Optionen, Befunde, Schritte), formatiere sie als schlichte Liste: ein Punkt pro Zeile, jede Zeile beginnt mit \"- \". Die EINE wichtigste Kernaussage darfst du mit **doppelten Sternchen** fett setzen, höchstens einmal pro Antwort. Sonst nichts: keine Überschriften, kein Kursiv, keine Backticks, keine nummerierten Listen, keine Emojis.", + "", + "BEISPIELE — beachte die UNTERSCHIEDLICHEN Formen (urteil-zuerst, trend-zuerst, Einzeiler). Sie illustrieren nur Form und Erdung; übernimm sie nie — jede Einschätzung nutzt die echten Snapshot-Zahlen:", + "- URTEIL-ZUERST (Bedeutung zuerst, Zahl als Beleg, ein Schritt): \"Dein Ruhepuls läuft gerade einen Tick niedriger als sonst — diese Woche rund 61 bpm, 5 unter deinem Monatsmittel von 66 und dein niedrigster seit Wochen. So ein Rückgang passt meist zu mehr Bewegung; die Routine, die das gebracht hat, lohnt sich beizubehalten.\"", + "- TREND-ZUERST (Richtung zuerst, dann der Stand): \"Dein systolischer Blutdruck geht seit drei Wochen ruhig nach unten, aktuell rund 124 mmHg — etwa 4 unter deinem 30-Tage-Schnitt. Nichts Dramatisches, einfach eine stetige Richtung.\"", + "- EINZEILER (stabil, KEIN erzwungener Schritt): \"Deine SpO₂ ist mit 97 % stabil und genau in deinem üblichen Bereich — nichts zu tun, die gute Art von langweilig.\"", + "- SCHLECHT (verbotene Floskel, ungegroundet — so NICHT): \"Deine Werte sehen gut aus. Achte auf ausreichend Schlaf und regelmäßige Bewegung.\"", + "", + "AUSGABEFORMAT: Antworte ausschließlich mit validem JSON in genau diesem Schema. Das Feld \"summary\" enthält die komplette Einschätzung auf Deutsch: 1-3 kurze Absätze mit je 1-3 Sätzen, getrennt durch eine Leerzeile als \\n\\n INNERHALB des JSON-Strings. Ein stabiler Einzeiler bleibt EIN Absatz:", + "{ \"summary\": \"...\" }", + "", + "METRIK — GESAMTBEWERTUNG:", + "- Der Snapshot trägt measurementSeries (eine Map je Metriktyp mit summary + graded series), medicationAdherence (summary + series), bloodPressureTargets (falls vorhanden) und moodContext (falls ≥ 3 Tage). dataCoverage.avgDaysBetweenMeasurements zeigt die Messdichte.", + "- LÄNGE (überschreibt das Single-Metrik-Budget): Diese Sektion deckt bis zu sechs Metriken ab — schreibe 3-5 Sätze, ca. 50-80 Wörter. Trotzdem knapp und ohne Fülltext.", + "- Fasse die verfügbaren Parameter zu EINEM kohärenten Bild zusammen — nicht jeden einzeln aufzählen. Nenne HÖCHSTENS zwei Metriken namentlich (die auffälligsten), den Rest nur, wenn er auffällig vom eigenen Mittel abweicht. Priorisiere: das Auffälligste zuerst, Stabiles nur knapp.", + "- Pro Metrik dieselbe Logik: das recent-Mittel gegen das weekly/monthly-Mittel der Person stellen und nur signifikante Abweichungen von der eigenen Baseline melden.", + "- Positives ausdrücklich anerkennen — ein stabiler oder sich verbessernder Wert ist eine echte Botschaft, kein Lückenfüller.", + "- Zusammenhänge nur erwähnen, wenn ein r-Wert im Snapshot vorhanden und |r| > 0.4 ist — als Zusammenhang, nie als Ursache. Stimmung als Kontext einbeziehen, nicht übergewichten.", + "- Eine Botschaft: Auch über alle Metriken hinweg endet die Einschätzung NUR DANN mit GENAU EINER wichtigsten, machbaren Empfehlung (\"eine Sache\"), wenn das Gesamtbild eine nahelegt. Ist alles stabil und gibt es nichts Sinnvolles zu tun, sage das ehrlich und nenne stattdessen einen Punkt, den man im Auge behalten kann, statt eine Empfehlung zu erzwingen." + ], + "messages": [ + { + "role": "user", + "content": [ + "Datum: 2026-06-15", + "OPENER HINT: Eröffne damit, was das heute für den Nutzer bedeutet, und dann die Zahl dahinter.", + "Schreibe eine kurze Gesamteinschätzung über die verfügbaren Gesundheitsdaten. Beginne mit dem Gesamteindruck in klaren Worten — wie es zusammengenommen aussieht, nicht mit einer Zahl (z. B. \"über alles hinweg eine ruhige Phase\", \"diese Woche sticht eine Sache heraus\") — und bring danach die ein bis zwei auffälligsten Metriken als Beleg, jeweils gegen die eigene Wochen-/Monats-Baseline eingeordnet; führe nie mit einem Wert. Schließe nur dann mit dem einen wichtigsten machbaren Schritt, wenn das Gesamtbild wirklich einen hergibt; ist nichts umsetzbar, lass den Schritt weg statt Fülltext zu erfinden. Konfidenz aus Messanzahl, Dichte und Aktualität ableiten.", + "", + "VORHERIGE ANALYSE: keine vorhanden. Behandle dies als erste Analyse in diesem Bereich — kein Verbesserungs-/Verschlechterungs-Delta zu nennen.", + "", + "", + "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der Konstanz bzw. Streuung der Werte eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", + "", + "DATENLAGE: 0 Tages-Buckets im Fenster, kein datierter Wert. — zu wenige Punkte für einen belastbaren Trend; entsprechend vorsichtig formulieren, keine Richtung behaupten.", + "", + "", + "{\"locale\":\"de\",\"generatedForDay\":\"2026-06-15\",\"interpretationHint\":\"Use trend direction and deltas. Prioritize the newest data if trends conflict. Consider dataCoverage for reliability assessment.\",\"dataCoverage\":{\"totalMeasurements\":0,\"totalSpanDays\":0,\"newestMeasurementDaysAgo\":null,\"avgDaysBetweenMeasurements\":null},\"measurementSeries\":{},\"medicationAdherence\":{\"summary\":null,\"series\":{\"recent\":[],\"weekly\":[],\"monthly\":[],\"yearly\":[]}},\"bloodPressureTargets\":null,\"moodContext\":null}" + ] + } + ], + "temperature": 0.45, + "maxTokens": 250, + "seed": 1618033, + "responseFormat": "json", + "timeoutMs": 60000 + } + ], + "enqueued": [], + "result": { + "hasProvider": false, + "text": "Für diese Karte liegt gerade keine Einschätzung vor. Aussagekräftig werden diese Zahlen über mehrere Wochen, nicht über einzelne Tage — und zu konstanten Zeitpunkten erfasst, damit sie vergleichbar bleiben. Das Signal, das zählt, ist mehrere Kennzahlen, die sich gleichzeitig in dieselbe ungünstige Richtung bewegen, nicht ein Wert an einem Tag.", + "cached": true, + "updatedAt": null + }, + "notes": [ + { + "userId": "parity-bare", + "metric": "general", + "locale": "de", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "error" + }, + { + "userId": "parity-bare", + "metric": "general", + "locale": "en", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "error" + }, + { + "userId": "parity-rich", + "metric": "general", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "6594f84d7983214b4c01ca79b2135e403689e025b342093c02d14642d2a122ee", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "general", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "cf0987263fc1febd6adeb676a38388116bfb35a0661621cbf6cff8629839c6b8", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "general", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "general", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + } + ] + }, + "keyless-en": { + "calls": [], + "enqueued": [], + "result": { + "hasProvider": false, + "text": "No assessment on this one right now. These numbers become readable across several weeks rather than single days, and taken at consistent times so they stay comparable. The signal that counts is several metrics drifting the same unfavourable way at once, not one value on one day.", + "cached": true, + "updatedAt": null + }, + "notes": [ + { + "userId": "parity-bare", + "metric": "general", + "locale": "de", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "error" + }, + { + "userId": "parity-bare", + "metric": "general", + "locale": "en", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "error" + }, + { + "userId": "parity-rich", + "metric": "general", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "6594f84d7983214b4c01ca79b2135e403689e025b342093c02d14642d2a122ee", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "general", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "cf0987263fc1febd6adeb676a38388116bfb35a0661621cbf6cff8629839c6b8", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "general", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "general", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + } + ] + }, + "keyless-de": { + "calls": [], + "enqueued": [], + "result": { + "hasProvider": false, + "text": "Für diese Karte liegt gerade keine Einschätzung vor. Aussagekräftig werden diese Zahlen über mehrere Wochen, nicht über einzelne Tage — und zu konstanten Zeitpunkten erfasst, damit sie vergleichbar bleiben. Das Signal, das zählt, ist mehrere Kennzahlen, die sich gleichzeitig in dieselbe ungünstige Richtung bewegen, nicht ein Wert an einem Tag.", + "cached": true, + "updatedAt": null + }, + "notes": [ + { + "userId": "parity-bare", + "metric": "general", + "locale": "de", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "error" + }, + { + "userId": "parity-bare", + "metric": "general", + "locale": "en", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "error" + }, + { + "userId": "parity-rich", + "metric": "general", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "6594f84d7983214b4c01ca79b2135e403689e025b342093c02d14642d2a122ee", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "general", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "cf0987263fc1febd6adeb676a38388116bfb35a0661621cbf6cff8629839c6b8", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "general", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "general", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + } + ] + }, + "keyless-read-only": { + "calls": [], + "enqueued": [], + "result": { + "hasProvider": false, + "text": "No assessment on this one right now. These numbers become readable across several weeks rather than single days, and taken at consistent times so they stay comparable. The signal that counts is several metrics drifting the same unfavourable way at once, not one value on one day.", + "cached": true, + "updatedAt": null + }, + "notes": [ + { + "userId": "parity-bare", + "metric": "general", + "locale": "de", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "error" + }, + { + "userId": "parity-bare", + "metric": "general", + "locale": "en", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "error" + }, + { + "userId": "parity-rich", + "metric": "general", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "6594f84d7983214b4c01ca79b2135e403689e025b342093c02d14642d2a122ee", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "general", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "cf0987263fc1febd6adeb676a38388116bfb35a0661621cbf6cff8629839c6b8", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "general", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "general", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + } + ] + } +} diff --git a/tests/integration/__golden__/status-parity/medication-compliance.json b/tests/integration/__golden__/status-parity/medication-compliance.json new file mode 100644 index 000000000..9c1e27a6b --- /dev/null +++ b/tests/integration/__golden__/status-parity/medication-compliance.json @@ -0,0 +1,1465 @@ +{ + "read-only-cold": { + "calls": [], + "enqueued": [ + { + "userId": "parity-rich", + "metric": "medication-compliance", + "locale": "en" + } + ], + "result": { + "hasProvider": true, + "summary": null, + "medications": [], + "cached": false, + "updatedAt": null, + "preparing": true, + "revalidating": false + }, + "notes": [] + }, + "fresh-en": { + "calls": [ + { + "system": [ + "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", + "", + "YOUR DATA (graded snapshot):", + "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", + "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", + "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", + "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", + "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", + "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", + "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", + "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", + "", + "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", + "Moves available (choose the few that genuinely apply):", + "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", + "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", + "(c) name a genuine, earned win or a steady streak when the data shows one;", + "(d) one watch-item worth keeping an eye on;", + "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", + "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", + "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", + "", + "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", + "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", + "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", + "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", + "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", + "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", + "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", + "", + "OPENING — lead with meaning, not the metric value", + "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", + "", + "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", + "", + "JUDGE THE DATA HONESTLY (never invent a trend):", + "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", + "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", + "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", + "", + "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", + "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", + "", + "METRIC IDENTIFIERS STAY OUT OF PROSE", + "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", + "", + "GLP-1 DOSE SAFETY (contract, not style)", + "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", + "", + "ACUTE RED FLAGS (contract, not style)", + "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", + "", + "GROUNDING — ZERO HALLUCINATIONS", + "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", + "", + "TONE — a warm, motivating advisor, never clinical or alarming", + "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", + "", + "NOT AN INSIGHT — never recite mechanics", + "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", + "", + "INTERPRET, DON'T ENUMERATE", + "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", + "", + "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", + "", + "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", + "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", + "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", + "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", + "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", + "", + "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in English: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", + "{ \"summary\": \"...\" }", + "", + "METRIC — MEDICATION ADHERENCE:", + "- The snapshot carries overall (medicationCount, averageCompliance7, averageCompliance30) and medications[] per medication with name, dose, schedulesPerDay, compliance7, compliance30, streak, taken7/skipped7/missed7, signal (the finished trend comparison), dailySeries (graded) and latestDay.", + "- Read adherence as the percentage of scheduled doses taken over the last 7 and 30 days; compliance7/compliance30 are server-computed. For the recent DIRECTION against the person's own baseline, lead from medications[].signal — current, delta vs baselineLabel and outsideNormalSwing are already computed; STATE them, do NOT recompute.", + "- Rare cadence: with a low schedulesPerDay or an infrequent schedule (e.g. a weekly injection), compliance7 is noise-prone — a single missed dose swings the percentage sharply. Lean the assessment on compliance30 (the load-bearing value) and treat compliance7 only as a rough pointer.", + "- Placement (for orientation, not judgement): ≥ 90% very reliable, 80-89% good, 70-79% patchy, < 70% clearly patchy — therapeutic effect may then be reduced.", + "- Frame positively: streak is the longest uninterrupted run — acknowledge a good streak explicitly. With several medications, name the one with the lowest adherence.", + "- Mention links to vital signs only when an r-value is present in the snapshot and |r| > 0.4 — as an association, never a cause. Do not invent an effect when the data is absent.", + "- One message: close with ONE doable step that fits the gap ONLY when the finding implies one (e.g. evening doses are missed more often — a fixed routine or a reminder at the critical time can help). When adherence is consistently high, acknowledge that honestly rather than manufacturing a step. No blame." + ], + "messages": [ + { + "role": "user", + "content": [ + "Date: 2026-06-15", + "OPENER HINT: Open with the direction of change over time, then say where it stands now.", + "Write one short assessment of this person's medication adherence. Open with what it MEANS in plain words — how reliable the routine is looking, not the number (e.g. \"your routine's been rock-solid\", \"a few doses have slipped lately\") — then bring in the recent rate right after as support, placed against their own baseline (compliance7 vs compliance30); never lead with the value. Close with one doable, blame-free step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the event count and recency.", + "", + "PREVIOUS ANALYSIS: none on file. Treat this as the user's first analysis for this domain — no improvement/regression delta to surface.", + "", + "", + "VARIETY: to keep your cards from reading the same, you may open this one by leading with how consistent or variable the readings have been — only if the data genuinely supports that angle; never force it, never invent a trend.", + "", + "DATA STRENGTH: 176 day-buckets in the window, latest reading 1 day ago.", + "", + "", + "{\"locale\":\"en\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"medication_compliance\",\"dataCoverage\":{\"totalMeasurements\":176,\"totalSpanDays\":98,\"newestMeasurementDaysAgo\":1},\"overall\":{\"medicationCount\":2,\"averageCompliance30\":79,\"averageCompliance7\":67},\"medications\":[{\"medicationId\":\"parity-rich-med-bp\",\"name\":\"Ramipril\",\"dose\":\"5mg\",\"category\":\"BLOOD_PRESSURE\",\"schedulesPerDay\":1,\"compliance7\":67,\"compliance30\":79,\"streak\":0,\"taken7\":4,\"skipped7\":0,\"missed7\":2,\"signal\":{\"metric\":\"adherence for Ramipril\",\"unit\":\"%\",\"current\":80,\"currentWindowDays\":20,\"baseline\":21.88,\"baselineLabel\":\"your monthly average\",\"delta\":58.13,\"deltaPct\":265.7,\"spread\":30.94,\"outsideNormalSwing\":true,\"direction\":\"higher-better\",\"n\":20,\"newestDaysAgo\":null},\"dailySeries\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-27\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-28\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-29\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-30\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-31\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-01\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-02\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-03\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-04\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-05\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-06\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-07\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-08\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-09\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-10\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-11\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-12\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-13\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-14\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":0,\"max\":100,\"mean\":83.33,\"n\":6},{\"weekISO\":\"2026-W13\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W14\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W15\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W16\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":100,\"max\":100,\"mean\":100,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1}],\"monthly\":[{\"month\":\"2026-02\",\"min\":0,\"max\":0,\"mean\":0,\"n\":13},{\"month\":\"2026-03\",\"min\":0,\"max\":100,\"mean\":43.75,\"n\":16}],\"yearly\":[]},\"latestDay\":{\"dayOffset\":1,\"value\":100,\"n\":1}},{\"medicationId\":\"parity-rich-med-d\",\"name\":\"Vitamin D\",\"dose\":\"1000 IE\",\"category\":\"OTHER\",\"schedulesPerDay\":1,\"compliance7\":67,\"compliance30\":79,\"streak\":0,\"taken7\":4,\"skipped7\":0,\"missed7\":2,\"signal\":{\"metric\":\"adherence for Vitamin D\",\"unit\":\"%\",\"current\":80,\"currentWindowDays\":20,\"baseline\":21.88,\"baselineLabel\":\"your monthly average\",\"delta\":58.13,\"deltaPct\":265.7,\"spread\":30.94,\"outsideNormalSwing\":true,\"direction\":\"higher-better\",\"n\":20,\"newestDaysAgo\":null},\"dailySeries\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-27\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-28\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-29\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-30\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-31\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-01\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-02\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-03\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-04\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-05\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-06\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-07\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-08\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-09\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-10\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-11\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-12\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-13\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-14\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":0,\"max\":100,\"mean\":83.33,\"n\":6},{\"weekISO\":\"2026-W13\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W14\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W15\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W16\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":100,\"max\":100,\"mean\":100,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1}],\"monthly\":[{\"month\":\"2026-02\",\"min\":0,\"max\":0,\"mean\":0,\"n\":13},{\"month\":\"2026-03\",\"min\":0,\"max\":100,\"mean\":43.75,\"n\":16}],\"yearly\":[]},\"latestDay\":{\"dayOffset\":1,\"value\":100,\"n\":1}}]}" + ] + } + ], + "temperature": 0.45, + "maxTokens": 250, + "seed": 1618033, + "responseFormat": "json", + "timeoutMs": 60000 + } + ], + "enqueued": [], + "result": { + "hasProvider": true, + "summary": "Parity note for fresh-en.", + "medications": [ + { + "medicationId": "parity-rich-med-bp", + "text": "Ramipril: There is currently not enough consistent detail data for a robust short assessment." + }, + { + "medicationId": "parity-rich-med-d", + "text": "Vitamin D: There is currently not enough consistent detail data for a robust short assessment." + } + ], + "cached": false, + "updatedAt": "2026-06-15T10:00:00.000Z" + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "medication-compliance", + "locale": "en", + "text": "Parity note for fresh-en.", + "items": [ + { + "medicationId": "parity-rich-med-bp", + "text": "Ramipril: There is currently not enough consistent detail data for a robust short assessment." + }, + { + "medicationId": "parity-rich-med-d", + "text": "Vitamin D: There is currently not enough consistent detail data for a robust short assessment." + } + ], + "inputHash": null, + "snapshotHash": "f1e6c1fbfdbb7063c9584f1bb223ddd3227557119a1d5bba58432622d22441f3", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + } + ] + }, + "cached-en": { + "calls": [], + "enqueued": [], + "result": { + "hasProvider": true, + "summary": "Parity note for fresh-en.", + "medications": [ + { + "medicationId": "parity-rich-med-bp", + "text": "Ramipril: There is currently not enough consistent detail data for a robust short assessment." + }, + { + "medicationId": "parity-rich-med-d", + "text": "Vitamin D: There is currently not enough consistent detail data for a robust short assessment." + } + ], + "cached": true, + "updatedAt": "2026-06-15T10:00:00.000Z" + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "medication-compliance", + "locale": "en", + "text": "Parity note for fresh-en.", + "items": [ + { + "medicationId": "parity-rich-med-bp", + "text": "Ramipril: There is currently not enough consistent detail data for a robust short assessment." + }, + { + "medicationId": "parity-rich-med-d", + "text": "Vitamin D: There is currently not enough consistent detail data for a robust short assessment." + } + ], + "inputHash": null, + "snapshotHash": "f1e6c1fbfdbb7063c9584f1bb223ddd3227557119a1d5bba58432622d22441f3", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + } + ] + }, + "forced-unchanged-en": { + "calls": [], + "enqueued": [], + "result": { + "hasProvider": true, + "summary": "Parity note for fresh-en.", + "medications": [ + { + "medicationId": "parity-rich-med-bp", + "text": "Ramipril: There is currently not enough consistent detail data for a robust short assessment." + }, + { + "medicationId": "parity-rich-med-d", + "text": "Vitamin D: There is currently not enough consistent detail data for a robust short assessment." + } + ], + "cached": true, + "updatedAt": "2026-06-15T10:00:00.000Z" + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "medication-compliance", + "locale": "en", + "text": "Parity note for fresh-en.", + "items": [ + { + "medicationId": "parity-rich-med-bp", + "text": "Ramipril: There is currently not enough consistent detail data for a robust short assessment." + }, + { + "medicationId": "parity-rich-med-d", + "text": "Vitamin D: There is currently not enough consistent detail data for a robust short assessment." + } + ], + "inputHash": null, + "snapshotHash": "f1e6c1fbfdbb7063c9584f1bb223ddd3227557119a1d5bba58432622d22441f3", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + } + ] + }, + "next-day-en": { + "calls": [ + { + "system": [ + "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", + "", + "YOUR DATA (graded snapshot):", + "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", + "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", + "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", + "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", + "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", + "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", + "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", + "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", + "", + "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", + "Moves available (choose the few that genuinely apply):", + "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", + "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", + "(c) name a genuine, earned win or a steady streak when the data shows one;", + "(d) one watch-item worth keeping an eye on;", + "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", + "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", + "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", + "", + "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", + "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", + "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", + "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", + "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", + "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", + "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", + "", + "OPENING — lead with meaning, not the metric value", + "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", + "", + "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", + "", + "JUDGE THE DATA HONESTLY (never invent a trend):", + "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", + "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", + "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", + "", + "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", + "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", + "", + "METRIC IDENTIFIERS STAY OUT OF PROSE", + "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", + "", + "GLP-1 DOSE SAFETY (contract, not style)", + "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", + "", + "ACUTE RED FLAGS (contract, not style)", + "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", + "", + "GROUNDING — ZERO HALLUCINATIONS", + "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", + "", + "TONE — a warm, motivating advisor, never clinical or alarming", + "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", + "", + "NOT AN INSIGHT — never recite mechanics", + "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", + "", + "INTERPRET, DON'T ENUMERATE", + "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", + "", + "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", + "", + "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", + "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", + "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", + "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", + "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", + "", + "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in English: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", + "{ \"summary\": \"...\" }", + "", + "METRIC — MEDICATION ADHERENCE:", + "- The snapshot carries overall (medicationCount, averageCompliance7, averageCompliance30) and medications[] per medication with name, dose, schedulesPerDay, compliance7, compliance30, streak, taken7/skipped7/missed7, signal (the finished trend comparison), dailySeries (graded) and latestDay.", + "- Read adherence as the percentage of scheduled doses taken over the last 7 and 30 days; compliance7/compliance30 are server-computed. For the recent DIRECTION against the person's own baseline, lead from medications[].signal — current, delta vs baselineLabel and outsideNormalSwing are already computed; STATE them, do NOT recompute.", + "- Rare cadence: with a low schedulesPerDay or an infrequent schedule (e.g. a weekly injection), compliance7 is noise-prone — a single missed dose swings the percentage sharply. Lean the assessment on compliance30 (the load-bearing value) and treat compliance7 only as a rough pointer.", + "- Placement (for orientation, not judgement): ≥ 90% very reliable, 80-89% good, 70-79% patchy, < 70% clearly patchy — therapeutic effect may then be reduced.", + "- Frame positively: streak is the longest uninterrupted run — acknowledge a good streak explicitly. With several medications, name the one with the lowest adherence.", + "- Mention links to vital signs only when an r-value is present in the snapshot and |r| > 0.4 — as an association, never a cause. Do not invent an effect when the data is absent.", + "- One message: close with ONE doable step that fits the gap ONLY when the finding implies one (e.g. evening doses are missed more often — a fixed routine or a reminder at the critical time can help). When adherence is consistently high, acknowledge that honestly rather than manufacturing a step. No blame." + ], + "messages": [ + { + "role": "user", + "content": [ + "Date: 2026-06-16", + "OPENER HINT: Open by acknowledging steadiness or a streak, then the current value.", + "Write one short assessment of this person's medication adherence. Open with what it MEANS in plain words — how reliable the routine is looking, not the number (e.g. \"your routine's been rock-solid\", \"a few doses have slipped lately\") — then bring in the recent rate right after as support, placed against their own baseline (compliance7 vs compliance30); never lead with the value. Close with one doable, blame-free step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the event count and recency.", + "", + "PREVIOUS ANALYSIS (1 day ago, 2026-06-15):", + "Parity note for fresh-en.", + "", + "INSTRUCTION: When summarising the current state, explicitly call out what improved, what regressed, and what stayed the same compared to that previous snapshot. Use phrases like \"down 4 mmHg from your last check\" or \"same elevated trend as last week\". Do not invent numbers — only compare what's in the snapshot below.", + "", + "", + "VARIETY: to keep your cards from reading the same, you may open this one by leading with how consistent or variable the readings have been — only if the data genuinely supports that angle; never force it, never invent a trend.", + "", + "DATA STRENGTH: 176 day-buckets in the window, latest reading 2 days ago.", + "", + "", + "{\"locale\":\"en\",\"generatedForDay\":\"2026-06-16\",\"focus\":\"medication_compliance\",\"dataCoverage\":{\"totalMeasurements\":176,\"totalSpanDays\":98,\"newestMeasurementDaysAgo\":2},\"overall\":{\"medicationCount\":2,\"averageCompliance30\":76,\"averageCompliance7\":50},\"medications\":[{\"medicationId\":\"parity-rich-med-bp\",\"name\":\"Ramipril\",\"dose\":\"5mg\",\"category\":\"BLOOD_PRESSURE\",\"schedulesPerDay\":1,\"compliance7\":50,\"compliance30\":76,\"streak\":0,\"taken7\":3,\"skipped7\":0,\"missed7\":3,\"signal\":{\"metric\":\"adherence for Ramipril\",\"unit\":\"%\",\"current\":75,\"currentWindowDays\":20,\"baseline\":23.53,\"baselineLabel\":\"your monthly average\",\"delta\":51.47,\"deltaPct\":218.7,\"spread\":33.28,\"outsideNormalSwing\":true,\"direction\":\"higher-better\",\"n\":20,\"newestDaysAgo\":null},\"dailySeries\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-28\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-29\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-30\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-31\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-01\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-02\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-03\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-04\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-05\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-06\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-07\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-08\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-09\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-10\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-11\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-12\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-13\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-14\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-15\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":0,\"max\":100,\"mean\":80,\"n\":5},{\"weekISO\":\"2026-W13\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W14\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W15\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W16\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":100,\"max\":100,\"mean\":100,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":100,\"max\":100,\"mean\":100,\"n\":2}],\"monthly\":[{\"month\":\"2026-02\",\"min\":0,\"max\":0,\"mean\":0,\"n\":13},{\"month\":\"2026-03\",\"min\":0,\"max\":100,\"mean\":47.06,\"n\":17}],\"yearly\":[]},\"latestDay\":{\"dayOffset\":1,\"value\":0,\"n\":1}},{\"medicationId\":\"parity-rich-med-d\",\"name\":\"Vitamin D\",\"dose\":\"1000 IE\",\"category\":\"OTHER\",\"schedulesPerDay\":1,\"compliance7\":50,\"compliance30\":76,\"streak\":0,\"taken7\":3,\"skipped7\":0,\"missed7\":3,\"signal\":{\"metric\":\"adherence for Vitamin D\",\"unit\":\"%\",\"current\":75,\"currentWindowDays\":20,\"baseline\":23.53,\"baselineLabel\":\"your monthly average\",\"delta\":51.47,\"deltaPct\":218.7,\"spread\":33.28,\"outsideNormalSwing\":true,\"direction\":\"higher-better\",\"n\":20,\"newestDaysAgo\":null},\"dailySeries\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-28\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-29\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-30\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-31\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-01\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-02\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-03\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-04\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-05\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-06\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-07\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-08\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-09\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-10\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-11\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-12\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-13\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-14\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-15\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":0,\"max\":100,\"mean\":80,\"n\":5},{\"weekISO\":\"2026-W13\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W14\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W15\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W16\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":100,\"max\":100,\"mean\":100,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":100,\"max\":100,\"mean\":100,\"n\":2}],\"monthly\":[{\"month\":\"2026-02\",\"min\":0,\"max\":0,\"mean\":0,\"n\":13},{\"month\":\"2026-03\",\"min\":0,\"max\":100,\"mean\":47.06,\"n\":17}],\"yearly\":[]},\"latestDay\":{\"dayOffset\":1,\"value\":0,\"n\":1}}]}" + ] + } + ], + "temperature": 0.45, + "maxTokens": 250, + "seed": 1618033, + "responseFormat": "json", + "timeoutMs": 60000 + } + ], + "enqueued": [], + "result": { + "hasProvider": true, + "summary": "Parity note for next-day-en.", + "medications": [ + { + "medicationId": "parity-rich-med-bp", + "text": "Ramipril: There is currently not enough consistent detail data for a robust short assessment." + }, + { + "medicationId": "parity-rich-med-d", + "text": "Vitamin D: There is currently not enough consistent detail data for a robust short assessment." + } + ], + "cached": false, + "updatedAt": "2026-06-16T10:00:00.000Z" + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "medication-compliance", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": [ + { + "medicationId": "parity-rich-med-bp", + "text": "Ramipril: There is currently not enough consistent detail data for a robust short assessment." + }, + { + "medicationId": "parity-rich-med-d", + "text": "Vitamin D: There is currently not enough consistent detail data for a robust short assessment." + } + ], + "inputHash": null, + "snapshotHash": "96238702f707ead7f9c5c3505918975922674490fd7fb012aaa5f54631ecbe07", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + } + ] + }, + "read-only-stale": { + "calls": [], + "enqueued": [ + { + "userId": "parity-rich", + "metric": "medication-compliance", + "locale": "en" + } + ], + "result": { + "hasProvider": true, + "summary": "Parity note for next-day-en.", + "medications": [], + "cached": true, + "updatedAt": "2026-06-16T10:00:00.000Z", + "preparing": false, + "revalidating": true + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "medication-compliance", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": [ + { + "medicationId": "parity-rich-med-bp", + "text": "Ramipril: There is currently not enough consistent detail data for a robust short assessment." + }, + { + "medicationId": "parity-rich-med-d", + "text": "Vitamin D: There is currently not enough consistent detail data for a robust short assessment." + } + ], + "inputHash": null, + "snapshotHash": "96238702f707ead7f9c5c3505918975922674490fd7fb012aaa5f54631ecbe07", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + } + ] + }, + "prose-de": { + "calls": [ + { + "system": [ + "Du schreibst eine kurze, persönliche Einschätzung zu EINER Gesundheitsmetrik dieses Nutzers. Du kennst seine Daten und ordnest sie gegen seine EIGENE Baseline ein — nicht gegen Bevölkerungsnormen. Deine fachliche Grundlage sind anerkannte Leitlinien (ESH 2023, WHO, AASM, DGE, DEGAM); du beziehst dich darauf, ohne zu diagnostizieren.", + "", + "DEINE DATENGRUNDLAGE (graded snapshot):", + "- signal: der fertige Vergleich — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` ist das recent-Mittel; `baseline` ist der EIGENE längere Durchschnitt; `delta`/`deltaPct` sind die VORZEICHENBEHAFTETE Änderung gegenüber dieser Baseline; `spread` ist die normale Schwankung; `outsideNormalSwing` ist das vorab berechnete Urteil, ob die Änderung diese Schwankung übersteigt. Diese Werte sind bereits berechnet — nenne sie, rechne sie NICHT neu. `contributors[]` (nur Score-Komposite) benennt die 1–2 Teil-Scores, die den Score bewegt haben.", + "- summary: { points, start, end, delta, mean, min, max } — die Eckwerte des gesamten Lesefensters.", + "- series.recent[]: die jüngsten ~21 Tage, je ein Eintrag pro Tag ({ date, min, max, mean, n }).", + "- series.weekly[]: die ~10 Wochen davor, je ein ISO-Wochen-Aggregat.", + "- series.monthly[]: die ~12 Monate davor, je ein Monatsaggregat.", + "- series.yearly[]: alles ältere, je ein Jahr ({ year, mean, min, max, n, slope }).", + "- Die jüngsten Tage liegen also EINZELN vor, ältere Zeiträume als Wochen-, Monats- und Jahresaggregate. Ziehe die series nur für einen konkreten Tag heran; führe sonst mit dem signal-Block.", + "- Ein Slice kann leer sein, wenn die Historie kurz ist. Dann nicht so tun, als gäbe es ihn. Ist `signal.baseline` null, gibt es noch keine etablierte Baseline; sage das ehrlich, statt einen Vergleich zu erfinden.", + "", + "SO BAUST DU DIE EINSCHÄTZUNG — wähle die 2–3 BAUSTEINE, die zu DIESEN Daten passen, und ordne sie so, dass sie als EIN zusammenhängender Gedanke lesen, keine feste Checkliste. Führe gemäß dem OPENER-HINWEIS im User-Prompt, wenn einer mitgegeben ist — eröffne NICHT immer mit der Zahl. Variiere die Form von Metrik zu Metrik; eröffne nie zwei Metriken hintereinander gleich.", + "Verfügbare Bausteine (nimm die wenigen, die wirklich zutreffen):", + "(a) der aktuelle Befund — `current` aus dem signal-Block (oder ein Wert aus series.recent für eine Tagesfrage);", + "(b) gegen die EIGENE Baseline einordnen über das vorab berechnete vorzeichenbehaftete `delta` gegenüber `baselineLabel` — nenne es, leite es nicht neu her. Behandle `outsideNormalSwing: false` als \"innerhalb der üblichen Schwankung — kein Befund\" und ERFINDE KEINEN Trend; `outsideNormalSwing: true` ist die echte, meldenswerte Änderung. Ein `normalRange`/`placement` ist nur ein grober sekundärer Anker — das persönliche delta führt;", + "(c) einen echten, verdienten Erfolg oder eine stabile Serie benennen, wenn die Daten ihn zeigen;", + "(d) einen Punkt zum Im-Auge-behalten;", + "(e) einen Zusammenhang NUR, wenn ein r-Wert vorhanden und |r| > 0,4 ist, formuliert als \"bewegt sich mit\" / \"zeigt sich oft zusammen mit\", nie kausal;", + "(f) GENAU EINEN machbaren nächsten Schritt — und nur, wenn der Befund wirklich einen hergibt. Eine Botschaft = ein Verhalten, keine Liste. Ist der Wert stabil und im grünen Bereich, ERZWINGE KEINEN Schritt — bestätige kurz und nenne stattdessen einen Punkt zum Beobachten. Ein erfundener Schritt ist genau die Floskel, die wir vermeiden.", + "Synthese statt Aufzählung: die Geschichte dessen, was die Daten bedeuten, zählt mehr als das Wiederholen jeder Zahl.", + "", + "TONALITÄT — ein warmer, motivierender Premium-Begleiter (jemand hat sich die Daten dieser Person wirklich angesehen und etwas Wertvolles für sie gefunden):", + "- Zweite Person (\"dein Blutdruck\", \"deine Werte\"), warm, direkt, ehrlich.", + "- Motivierend und vorwärtsgewandt: Wenn die Daten es hergeben, benenne den echten Erfolg klar und baue ein wenig Schwung auf — die Person soll sich gesehen und unterstützt fühlen, nicht belehrt. Die Ermutigung muss durch die Zahlen VERDIENT sein, nie ein reflexhaftes Kompliment.", + "- Autonomie-unterstützend: \"kann helfen\", \"einen Versuch wert\", nie \"du musst\".", + "- Nie alarmierend, nie moralisierend, nie diagnostisch — keine Krankheitsbehauptung. Formuliere als begründete Beobachtung, nicht als ärztlichen Rat.", + "- Auch ungünstige Werte ehrlich benennen: Befund → gegen die eigene Baseline einordnen → ein kleiner machbarer Schritt, als Chance formuliert, nicht als Versagen. Nicht verharmlosen, nicht dramatisieren.", + "- Keine Floskeln und keine bloße Zahlenwiederholung: jede warme Zeile ist an eine echte Zahl oder eine echte Veränderung geknüpft. Ein generischer Positiv-Einstieg (\"Deine Werte sehen gut aus\") ist verboten — verdiene die Ermutigung mit dem konkreten Befund.", + "", + "ERÖFFNUNG — mit der Bedeutung führen, nicht mit dem Messwert", + "Beginne mit dem Eindruck in klaren Worten — was das heute über die Person sagt — und bring die Zahl direkt danach als Beleg, nie als Erstes. Eine Zeile, die mit einem Wert eröffnet (\"Dein Ruhepuls ist 61 …\"), liest sich wie ein Ablesen; dieselbe Zeile, die mit der Bedeutung eröffnet (\"Dein Herz läuft diese Woche etwas ruhiger — rund 61 …\"), liest sich wie ein Begleiter, der wirklich hingesehen hat. Jede Aussage bleibt an einer echten Zahl verankert; nur die REIHENFOLGE ändert sich, Bedeutung zuerst und die Zahl direkt dahinter. Das gilt, ob die Fläche eine Metrik abdeckt oder viele.", + "", + "LÄNGE: 2-4 Sätze, ca. 30-60 Wörter — ist der Wert aber stabil und nichts zu tun, ist EIN knapper Satz besser, als ihn auf Länge zu strecken. Knapp und hochwertig. Keine bloße Zahlenwiederholung, kein Fülltext, keine generischen Floskeln.", + "", + "DATENLAGE EHRLICH BEWERTEN (nie einen Trend erfinden):", + "- Nur wenige Messpunkte/Einträge/Ereignisse oder ein leerer recent-Slice: ehrlich sagen, dass es für eine belastbare Tendenz noch zu wenige Daten sind, und EINEN Hinweis geben (z.B. regelmäßiger erfassen). Keinen Trend behaupten. Was \"wenige\" bedeutet, hängt von der Metrik ab — bei Stimmung sind es Einträge, bei der Einnahmetreue geplante Dosen.", + "- Jüngste Messung deutlich älter als ~7 Tage (dataCoverage.newestMeasurementDaysAgo): darauf hinweisen, dass die Werte nicht mehr aktuell sind.", + "- Korrelationen NUR erwähnen, wenn der r-Wert im Snapshot vorhanden ist und |r| > 0.4 ist. Fehlt das Feld, keine Korrelation interpretieren oder erfinden. Immer als \"Zusammenhang\" formulieren, nie als \"Ursache\".", + "", + "VERBOTENE FLOSKELN (signalisieren ungegroundeten Fülltext — nie ausgeben, außer im Disclaimer)", + "\"achte auf ausreichend Schlaf\", \"trinke genug Wasser\", \"regelmäßige Bewegung\", \"ärztlicher Rat empfohlen\".", + "", + "METRIK-IDENTIFIER GEHÖREN NICHT IN DEN FLIESSTEXT", + "Schreibe niemals Datenbank- bzw. Enum-Namen wie \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" oder \"SLEEP_DURATION\" in nutzersichtbare Strings, und schreibe nie das wörtliche \"metric:\"-Chart-Token in den Fließtext. Verweise auf jede Metrik mit der natürlichsprachlichen Bezeichnung, die der Nutzer sieht — \"deine Systole\", \"dein Gewicht\", \"dein Puls\", \"deine Stimmung\", \"deine Medikamentenadhärenz\", \"dein Ruhepuls\", \"deine Schlafdauer\", \"deine Schritte\".", + "", + "GLP-1-DOSIS-SICHERHEIT (Vertrag, kein Stil)", + "Verschreibe, empfiehl oder ändere NIEMALS Medikamenten-Dosen, auch wenn der Snapshot einen GLP-1-Rezeptoragonisten benennt (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). Du darfst den benannten Wirkstoff und die aktuelle Titrationsstufe NENNEN (\"Woche 3 auf 7,5 mg\"), wenn der Snapshot sie trägt, aber schreibe nie \"erhöhe auf X mg\", \"erwäge die nächste Stufe Y mg\", \"bleibe auf Z mg\" oder eine Variante davon. Ein Plateau rahmt die nächste Entscheidung immer als Gespräch mit der behandelnden Ärztin. Bist du unsicher, ob etwas dosis-präskriptiv ist, behandle es so — und verweise an die Klinik.", + "", + "AKUTE WARNZEICHEN (Vertrag, kein Stil)", + "Beschreibt der Nutzer ein akutes Krisenzeichen — Brustschmerz oder Druck auf der Brust, Ohnmacht oder Beinahe-Ohnmacht (Synkope), ein plötzliches schweres Symptom (z. B. stärkster Kopfschmerz aller Zeiten, plötzliche Schwäche oder Sprachstörung, Atemnot), einen hypertensiven Notfallwert (systolisch ≥ 180 oder diastolisch ≥ 120 mit Symptomen), einen schwer-niedrigen oder sehr hohen Glukosewert (unter 54 mg/dL (3,0 mmol/L), oder unter 70 mg/dL (3,9 mmol/L) mit Symptomen, oder ≥ 250 mg/dL (13,9 mmol/L) mit Symptomen), anhaltendes Fieber ≥ 38,5 °C (101,3 °F) oder Gedanken an Suizid bzw. Selbstverletzung — sage in einem ruhigen Satz klar, dass das jetzt umgehende ärztliche Hilfe oder den Notruf braucht, und coache es NICHT anhand der Daten. Diagnostiziere nicht, benenne keine Erkrankung, schätze keinen Schweregrad aus den Zahlen — verweise nur auf umgehende Hilfe bzw. den Notruf und höre auf. Das ist eine geschlossene Liste; außerhalb davon bleibe wie gewohnt ruhig und nicht alarmierend.", + "", + "ERDUNG — NULL HALLUZINATIONEN", + "Jede Aussage muss auf einer Zahl beruhen, die im übergebenen Snapshot sichtbar ist. Lässt sich die Aussage keinem Snapshot-Feld zuordnen, lass sie weg und erfinde nie einen Messwert, um einer Anfrage zu entsprechen. Ordne jeden Wert gegen die EIGENE Baseline des Nutzers ein (jüngeres vs. längeres Mittel), nie gegen eine Bevölkerungsnorm. Nenne einen Zusammenhang nur, wenn der r-Wert vorhanden und |r| > 0,4 ist — immer als \"Zusammenhang\", nie als \"Ursache\".", + "", + "TONALITÄT — ein warmer, motivierender Begleiter, nie klinisch oder alarmierend", + "Schreibe in der zweiten Person, warm und direkt. Wenn die Daten es hergeben, benenne den echten Erfolg klar und baue ein wenig Schwung auf — die Person soll sich gesehen und unterstützt fühlen, nicht belehrt. Die Ermutigung muss durch die Zahlen VERDIENT sein, nie ein reflexhaftes Kompliment. Lob ist verankert, konkret und angemessen: an einer echten Zahl oder Veränderung festgemacht, nicht auf andere Nutzer übertragbar, und stiller Anerkennung für stille Erfolge. Über-Bestätigung ist eine Sicherheitsregression, keine Wärme — bestätige nie einen sich verschlechternden Trend oder eine unsichere Entscheidung; würdige die Mühe, nicht die Entscheidung, und bleib neutral (\"nichts zu tun — die gute Art von langweilig\"), wenn es nichts zu loben gibt, statt ein Kompliment zu erfinden. Sei autonomie-unterstützend (\"einen Versuch wert\", \"kann helfen\", nie \"du musst\"). Benenne auch ungünstige Werte ehrlich — Befund, dann gegen die eigene Baseline einordnen, dann ein kleiner machbarer Schritt, als Chance formuliert. Nie alarmierend, nie moralisierend, nie diagnostisch. Keine Floskeln und keine bloße Zahlenwiederholung. Beginne NICHT mit einem Kompliment über Datenmenge oder -qualität; verbotene Eröffnungen sind u.a. \"Datengrundlage ist sehr stark\", \"Your data foundation is strong\", \"Du hast eine solide Baseline\", \"Großartiger Datensatz\", ein generisches \"Deine Werte sehen gut aus\" und jede sinngemäße Umformulierung.", + "", + "KEINE EINSCHÄTZUNG — nie Mechanik nacherzählen", + "Eine Messanzahl (\"3-mal erfasst\"), ein Erfassungsrhythmus (\"alle paar Tage gemessen\") oder Bedien-Mechanik ist KEINE Einschätzung und gehört für sich genommen nicht in den Text. Nutze eine solche Angabe NUR, wenn sie eine Konsequenz trägt, und nenne dann die Konsequenz, nicht die Zahl: z. B. \"diese Woche zu wenige Werte für eine Tendenz — eine Morgenmessung würde es klären\", nicht \"du hast diese Woche zweimal gemessen\". Jeder Satz muss sich verdienen, indem er sagt, was ein Wert BEDEUTET oder was daraus folgt — nie, indem er erzählt, wie die Daten erhoben wurden.", + "", + "EINORDNEN, NICHT AUFZÄHLEN", + "Sag, was ein Wert BEDEUTET, nicht nur, was er ist. Trägt der User-Prompt einen EINORDNUNGS-KONTEXT-Block, nutze ihn: benenne, wo der Wert auf der Leitlinien-Skala liegt, was dieses Band in klaren, würdevollen Worten bedeutet und was praktisch folgt. Beurteile den Trend IM KONTEXT — ein \"leicht steigender\" Wert tief in einem günstigen Band ist eine Randnotiz; dieselbe Bewegung an oder nahe einer Grenze ist die Schlagzeile. Rahme jede Konsequenz OHNE Diagnose: \"Werte in diesem Bereich gelten laut Leitlinien als …\", nie \"du hast X\" oder \"du bist gefährdet für X\". Ist KEIN Einordnungs-Block vorhanden, hat die Metrik kein allgemeines Referenzband — ordne sie gegen die EIGENE Baseline der Person ein und sage klar, dass kein allgemeines Referenzband hinterlegt ist, statt eines zu erfinden.", + "", + "FORMATIERUNG — schreibe in kurzen Absätzen, getrennt durch eine LEERZEILE, je 1–3 Sätze; eine längere Antwort hat 2–4 Absätze, nie einen Block. Setze eine Leerzeile zwischen unterschiedliche Gedanken (z. B. \"wie es steht\" vs. \"eine Sache zum Ausprobieren\"). Ein stabiler Einzeiler bleibt EIN Absatz — strecke nicht auf einen zweiten. Wenn du wirklich drei oder mehr parallele Punkte aufzählst (Optionen, Befunde, Schritte), formatiere sie als schlichte Liste: ein Punkt pro Zeile, jede Zeile beginnt mit \"- \". Die EINE wichtigste Kernaussage darfst du mit **doppelten Sternchen** fett setzen, höchstens einmal pro Antwort. Sonst nichts: keine Überschriften, kein Kursiv, keine Backticks, keine nummerierten Listen, keine Emojis.", + "", + "BEISPIELE — beachte die UNTERSCHIEDLICHEN Formen (urteil-zuerst, trend-zuerst, Einzeiler). Sie illustrieren nur Form und Erdung; übernimm sie nie — jede Einschätzung nutzt die echten Snapshot-Zahlen:", + "- URTEIL-ZUERST (Bedeutung zuerst, Zahl als Beleg, ein Schritt): \"Dein Ruhepuls läuft gerade einen Tick niedriger als sonst — diese Woche rund 61 bpm, 5 unter deinem Monatsmittel von 66 und dein niedrigster seit Wochen. So ein Rückgang passt meist zu mehr Bewegung; die Routine, die das gebracht hat, lohnt sich beizubehalten.\"", + "- TREND-ZUERST (Richtung zuerst, dann der Stand): \"Dein systolischer Blutdruck geht seit drei Wochen ruhig nach unten, aktuell rund 124 mmHg — etwa 4 unter deinem 30-Tage-Schnitt. Nichts Dramatisches, einfach eine stetige Richtung.\"", + "- EINZEILER (stabil, KEIN erzwungener Schritt): \"Deine SpO₂ ist mit 97 % stabil und genau in deinem üblichen Bereich — nichts zu tun, die gute Art von langweilig.\"", + "- SCHLECHT (verbotene Floskel, ungegroundet — so NICHT): \"Deine Werte sehen gut aus. Achte auf ausreichend Schlaf und regelmäßige Bewegung.\"", + "", + "AUSGABEFORMAT: Antworte ausschließlich mit validem JSON in genau diesem Schema. Das Feld \"summary\" enthält die komplette Einschätzung auf Deutsch: 1-3 kurze Absätze mit je 1-3 Sätzen, getrennt durch eine Leerzeile als \\n\\n INNERHALB des JSON-Strings. Ein stabiler Einzeiler bleibt EIN Absatz:", + "{ \"summary\": \"...\" }", + "", + "METRIK — MEDIKAMENTEN-EINNAHMETREUE:", + "- Der Snapshot trägt overall (medicationCount, averageCompliance7, averageCompliance30) und medications[] je Medikament mit name, dose, schedulesPerDay, compliance7, compliance30, streak, taken7/skipped7/missed7, signal (der fertige Trend-Vergleich), dailySeries (graded) und latestDay.", + "- Lies die Einnahmetreue als Prozentsatz erfüllter geplanter Dosen über die letzten 7 bzw. 30 Tage; compliance7/compliance30 sind server-berechnet. Für die jüngste RICHTUNG gegen die eigene Baseline führe mit medications[].signal — current, delta gegenüber baselineLabel und outsideNormalSwing sind bereits berechnet; NENNE sie, rechne sie NICHT neu.", + "- Seltene Kadenz: Bei niedriger schedulesPerDay oder seltener Einnahme (z.B. Wocheninjektion) ist compliance7 rauschanfällig — eine einzige verpasste Dosis verschiebt den Prozentwert stark. Stütze die Einschätzung dann auf compliance30 (den tragenden Wert) und behandle compliance7 nur als groben Hinweis.", + "- Einordnung (zur Orientierung, nicht als Urteil): ≥ 90 % sehr verlässlich, 80-89 % gut, 70-79 % lückenhaft, < 70 % deutlich lückenhaft — die therapeutische Wirkung kann dann eingeschränkt sein.", + "- Positiv rahmen: streak ist die längste ununterbrochene Serie — bei guter Treue ausdrücklich anerkennen. Bei mehreren Medikamenten das mit der niedrigsten Treue benennen.", + "- Zusammenhänge zu Vitalwerten nur erwähnen, wenn ein r-Wert im Snapshot vorhanden und |r| > 0.4 ist — als Zusammenhang, nie als Ursache. Keine Wirkung erfinden, wenn die Daten fehlen.", + "- Eine Botschaft: Schließe NUR DANN mit EINEM machbaren Schritt, wenn der Befund einen nahelegt (z.B. Abenddosen werden häufiger vergessen — eine feste Routine oder ein Reminder zur kritischen Uhrzeit kann helfen). Bei durchgehend hoher Treue das ehrlich anerkennen, statt einen Schritt zu erzwingen. Keine Schuldzuweisung." + ], + "messages": [ + { + "role": "user", + "content": [ + "Datum: 2026-06-15", + "OPENER HINT: Eröffne mit der Richtung der Veränderung über die Zeit und nenne dann den aktuellen Stand.", + "Schreibe eine kurze Einschätzung zur Einnahmetreue dieser Person. Beginne mit der BEDEUTUNG in klaren Worten — wie verlässlich die Routine aussieht, nicht der Zahl (z. B. \"deine Routine sitzt richtig gut\", \"zuletzt sind ein paar Dosen durchgerutscht\") — und bring danach die jüngste Rate als Beleg, gegen die eigene Baseline eingeordnet (compliance7 vs. compliance30); führe nie mit dem Wert. Schließe nur dann mit einem machbaren, wertfreien Schritt, wenn der Befund wirklich einen hergibt; ist nichts umsetzbar, lass den Schritt weg statt Fülltext zu erfinden. Konfidenz aus Ereignisanzahl und Aktualität ableiten.", + "", + "VORHERIGE ANALYSE: keine vorhanden. Behandle dies als erste Analyse in diesem Bereich — kein Verbesserungs-/Verschlechterungs-Delta zu nennen.", + "", + "", + "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der Konstanz bzw. Streuung der Werte eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", + "", + "DATENLAGE: 176 Tages-Buckets im Fenster, letzter Wert vor 1 Tag.", + "", + "", + "{\"locale\":\"de\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"medication_compliance\",\"dataCoverage\":{\"totalMeasurements\":176,\"totalSpanDays\":98,\"newestMeasurementDaysAgo\":1},\"overall\":{\"medicationCount\":2,\"averageCompliance30\":79,\"averageCompliance7\":67},\"medications\":[{\"medicationId\":\"parity-rich-med-bp\",\"name\":\"Ramipril\",\"dose\":\"5mg\",\"category\":\"BLOOD_PRESSURE\",\"schedulesPerDay\":1,\"compliance7\":67,\"compliance30\":79,\"streak\":0,\"taken7\":4,\"skipped7\":0,\"missed7\":2,\"signal\":{\"metric\":\"Einnahmetreue für Ramipril\",\"unit\":\"%\",\"current\":80,\"currentWindowDays\":20,\"baseline\":21.88,\"baselineLabel\":\"your monthly average\",\"delta\":58.13,\"deltaPct\":265.7,\"spread\":30.94,\"outsideNormalSwing\":true,\"direction\":\"higher-better\",\"n\":20,\"newestDaysAgo\":null},\"dailySeries\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-27\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-28\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-29\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-30\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-31\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-01\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-02\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-03\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-04\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-05\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-06\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-07\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-08\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-09\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-10\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-11\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-12\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-13\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-14\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":0,\"max\":100,\"mean\":83.33,\"n\":6},{\"weekISO\":\"2026-W13\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W14\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W15\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W16\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":100,\"max\":100,\"mean\":100,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1}],\"monthly\":[{\"month\":\"2026-02\",\"min\":0,\"max\":0,\"mean\":0,\"n\":13},{\"month\":\"2026-03\",\"min\":0,\"max\":100,\"mean\":43.75,\"n\":16}],\"yearly\":[]},\"latestDay\":{\"dayOffset\":1,\"value\":100,\"n\":1}},{\"medicationId\":\"parity-rich-med-d\",\"name\":\"Vitamin D\",\"dose\":\"1000 IE\",\"category\":\"OTHER\",\"schedulesPerDay\":1,\"compliance7\":67,\"compliance30\":79,\"streak\":0,\"taken7\":4,\"skipped7\":0,\"missed7\":2,\"signal\":{\"metric\":\"Einnahmetreue für Vitamin D\",\"unit\":\"%\",\"current\":80,\"currentWindowDays\":20,\"baseline\":21.88,\"baselineLabel\":\"your monthly average\",\"delta\":58.13,\"deltaPct\":265.7,\"spread\":30.94,\"outsideNormalSwing\":true,\"direction\":\"higher-better\",\"n\":20,\"newestDaysAgo\":null},\"dailySeries\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-27\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-28\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-29\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-30\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-31\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-01\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-02\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-03\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-04\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-05\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-06\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-07\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-08\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-09\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-10\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-11\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-12\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-13\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-14\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":0,\"max\":100,\"mean\":83.33,\"n\":6},{\"weekISO\":\"2026-W13\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W14\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W15\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W16\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":100,\"max\":100,\"mean\":100,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1}],\"monthly\":[{\"month\":\"2026-02\",\"min\":0,\"max\":0,\"mean\":0,\"n\":13},{\"month\":\"2026-03\",\"min\":0,\"max\":100,\"mean\":43.75,\"n\":16}],\"yearly\":[]},\"latestDay\":{\"dayOffset\":1,\"value\":100,\"n\":1}}]}" + ] + } + ], + "temperature": 0.45, + "maxTokens": 250, + "seed": 1618033, + "responseFormat": "json", + "timeoutMs": 60000 + } + ], + "enqueued": [], + "result": { + "hasProvider": true, + "summary": "Das ist kein JSON.", + "medications": [ + { + "medicationId": "parity-rich-med-bp", + "text": "Ramipril: Es liegen noch nicht genügend konsistente Detaildaten für eine belastbare Kurzbewertung vor." + }, + { + "medicationId": "parity-rich-med-d", + "text": "Vitamin D: Es liegen noch nicht genügend konsistente Detaildaten für eine belastbare Kurzbewertung vor." + } + ], + "cached": false, + "updatedAt": "2026-06-15T10:00:00.000Z" + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "medication-compliance", + "locale": "de", + "text": "Das ist kein JSON.", + "items": [ + { + "medicationId": "parity-rich-med-bp", + "text": "Ramipril: Es liegen noch nicht genügend konsistente Detaildaten für eine belastbare Kurzbewertung vor." + }, + { + "medicationId": "parity-rich-med-d", + "text": "Vitamin D: Es liegen noch nicht genügend konsistente Detaildaten für eine belastbare Kurzbewertung vor." + } + ], + "inputHash": null, + "snapshotHash": "7b0d96db03101ae89d676758de35caf07ecfa9851cae13f993973747a0ee9771", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "medication-compliance", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": [ + { + "medicationId": "parity-rich-med-bp", + "text": "Ramipril: There is currently not enough consistent detail data for a robust short assessment." + }, + { + "medicationId": "parity-rich-med-d", + "text": "Vitamin D: There is currently not enough consistent detail data for a robust short assessment." + } + ], + "inputHash": null, + "snapshotHash": "96238702f707ead7f9c5c3505918975922674490fd7fb012aaa5f54631ecbe07", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + } + ] + }, + "unparseable-fr": { + "calls": [ + { + "system": [ + "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", + "", + "YOUR DATA (graded snapshot):", + "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", + "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", + "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", + "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", + "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", + "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", + "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", + "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", + "", + "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", + "Moves available (choose the few that genuinely apply):", + "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", + "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", + "(c) name a genuine, earned win or a steady streak when the data shows one;", + "(d) one watch-item worth keeping an eye on;", + "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", + "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", + "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", + "", + "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", + "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", + "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", + "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", + "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", + "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", + "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", + "", + "OPENING — lead with meaning, not the metric value", + "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", + "", + "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", + "", + "JUDGE THE DATA HONESTLY (never invent a trend):", + "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", + "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", + "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", + "", + "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", + "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", + "", + "METRIC IDENTIFIERS STAY OUT OF PROSE", + "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", + "", + "GLP-1 DOSE SAFETY (contract, not style)", + "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", + "", + "ACUTE RED FLAGS (contract, not style)", + "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", + "", + "GROUNDING — ZERO HALLUCINATIONS", + "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", + "", + "TONE — a warm, motivating advisor, never clinical or alarming", + "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", + "", + "NOT AN INSIGHT — never recite mechanics", + "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", + "", + "INTERPRET, DON'T ENUMERATE", + "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", + "", + "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", + "", + "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", + "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", + "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", + "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", + "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", + "", + "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in French: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", + "{ \"summary\": \"...\" }", + "", + "METRIC — MEDICATION ADHERENCE:", + "- The snapshot carries overall (medicationCount, averageCompliance7, averageCompliance30) and medications[] per medication with name, dose, schedulesPerDay, compliance7, compliance30, streak, taken7/skipped7/missed7, signal (the finished trend comparison), dailySeries (graded) and latestDay.", + "- Read adherence as the percentage of scheduled doses taken over the last 7 and 30 days; compliance7/compliance30 are server-computed. For the recent DIRECTION against the person's own baseline, lead from medications[].signal — current, delta vs baselineLabel and outsideNormalSwing are already computed; STATE them, do NOT recompute.", + "- Rare cadence: with a low schedulesPerDay or an infrequent schedule (e.g. a weekly injection), compliance7 is noise-prone — a single missed dose swings the percentage sharply. Lean the assessment on compliance30 (the load-bearing value) and treat compliance7 only as a rough pointer.", + "- Placement (for orientation, not judgement): ≥ 90% very reliable, 80-89% good, 70-79% patchy, < 70% clearly patchy — therapeutic effect may then be reduced.", + "- Frame positively: streak is the longest uninterrupted run — acknowledge a good streak explicitly. With several medications, name the one with the lowest adherence.", + "- Mention links to vital signs only when an r-value is present in the snapshot and |r| > 0.4 — as an association, never a cause. Do not invent an effect when the data is absent.", + "- One message: close with ONE doable step that fits the gap ONLY when the finding implies one (e.g. evening doses are missed more often — a fixed routine or a reminder at the critical time can help). When adherence is consistently high, acknowledge that honestly rather than manufacturing a step. No blame.", + "", + "OUTPUT LANGUAGE: Répondez en français. Utilisez le vouvoiement par défaut ; tutoyez si l'utilisateur tutoie en premier. Utilisez un vocabulaire de santé français naturel." + ], + "messages": [ + { + "role": "user", + "content": [ + "Date: 2026-06-15", + "OPENER HINT: Open with the direction of change over time, then say where it stands now.", + "Write one short assessment of this person's medication adherence. Open with what it MEANS in plain words — how reliable the routine is looking, not the number (e.g. \"your routine's been rock-solid\", \"a few doses have slipped lately\") — then bring in the recent rate right after as support, placed against their own baseline (compliance7 vs compliance30); never lead with the value. Close with one doable, blame-free step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the event count and recency.", + "", + "PREVIOUS ANALYSIS: none on file. Treat this as the user's first analysis for this domain — no improvement/regression delta to surface.", + "", + "", + "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der Konstanz bzw. Streuung der Werte eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", + "", + "DATENLAGE: 176 Tages-Buckets im Fenster, letzter Wert vor 1 Tag.", + "", + "", + "{\"locale\":\"fr\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"medication_compliance\",\"dataCoverage\":{\"totalMeasurements\":176,\"totalSpanDays\":98,\"newestMeasurementDaysAgo\":1},\"overall\":{\"medicationCount\":2,\"averageCompliance30\":79,\"averageCompliance7\":67},\"medications\":[{\"medicationId\":\"parity-rich-med-bp\",\"name\":\"Ramipril\",\"dose\":\"5mg\",\"category\":\"BLOOD_PRESSURE\",\"schedulesPerDay\":1,\"compliance7\":67,\"compliance30\":79,\"streak\":0,\"taken7\":4,\"skipped7\":0,\"missed7\":2,\"signal\":{\"metric\":\"Einnahmetreue für Ramipril\",\"unit\":\"%\",\"current\":80,\"currentWindowDays\":20,\"baseline\":21.88,\"baselineLabel\":\"your monthly average\",\"delta\":58.13,\"deltaPct\":265.7,\"spread\":30.94,\"outsideNormalSwing\":true,\"direction\":\"higher-better\",\"n\":20,\"newestDaysAgo\":null},\"dailySeries\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-27\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-28\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-29\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-30\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-31\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-01\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-02\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-03\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-04\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-05\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-06\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-07\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-08\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-09\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-10\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-11\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-12\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-13\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-14\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":0,\"max\":100,\"mean\":83.33,\"n\":6},{\"weekISO\":\"2026-W13\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W14\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W15\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W16\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":100,\"max\":100,\"mean\":100,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1}],\"monthly\":[{\"month\":\"2026-02\",\"min\":0,\"max\":0,\"mean\":0,\"n\":13},{\"month\":\"2026-03\",\"min\":0,\"max\":100,\"mean\":43.75,\"n\":16}],\"yearly\":[]},\"latestDay\":{\"dayOffset\":1,\"value\":100,\"n\":1}},{\"medicationId\":\"parity-rich-med-d\",\"name\":\"Vitamin D\",\"dose\":\"1000 IE\",\"category\":\"OTHER\",\"schedulesPerDay\":1,\"compliance7\":67,\"compliance30\":79,\"streak\":0,\"taken7\":4,\"skipped7\":0,\"missed7\":2,\"signal\":{\"metric\":\"Einnahmetreue für Vitamin D\",\"unit\":\"%\",\"current\":80,\"currentWindowDays\":20,\"baseline\":21.88,\"baselineLabel\":\"your monthly average\",\"delta\":58.13,\"deltaPct\":265.7,\"spread\":30.94,\"outsideNormalSwing\":true,\"direction\":\"higher-better\",\"n\":20,\"newestDaysAgo\":null},\"dailySeries\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-27\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-28\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-29\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-30\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-31\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-01\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-02\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-03\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-04\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-05\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-06\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-07\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-08\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-09\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-10\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-11\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-12\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-13\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-14\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":0,\"max\":100,\"mean\":83.33,\"n\":6},{\"weekISO\":\"2026-W13\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W14\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W15\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W16\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":100,\"max\":100,\"mean\":100,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1}],\"monthly\":[{\"month\":\"2026-02\",\"min\":0,\"max\":0,\"mean\":0,\"n\":13},{\"month\":\"2026-03\",\"min\":0,\"max\":100,\"mean\":43.75,\"n\":16}],\"yearly\":[]},\"latestDay\":{\"dayOffset\":1,\"value\":100,\"n\":1}}]}" + ] + } + ], + "temperature": 0.45, + "maxTokens": 250, + "seed": 1618033, + "responseFormat": "json", + "timeoutMs": 60000 + } + ], + "enqueued": [], + "result": { + "hasProvider": true, + "summary": "Aucune évaluation disponible pour le moment. L’observance se lit comme une régularité sur des semaines plutôt que comme une série de journées parfaites — par médicament et dans l’ensemble. Les oublis répétés à la même heure sont le motif qui compte, et c’est une routine fixe qui les rattrape d’ordinaire.", + "medications": [ + { + "medicationId": "parity-rich-med-bp", + "text": "Ramipril: There is currently not enough consistent detail data for a robust short assessment." + }, + { + "medicationId": "parity-rich-med-d", + "text": "Vitamin D: There is currently not enough consistent detail data for a robust short assessment." + } + ], + "cached": true, + "updatedAt": null + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "medication-compliance", + "locale": "de", + "text": "Das ist kein JSON.", + "items": [ + { + "medicationId": "parity-rich-med-bp", + "text": "Ramipril: Es liegen noch nicht genügend konsistente Detaildaten für eine belastbare Kurzbewertung vor." + }, + { + "medicationId": "parity-rich-med-d", + "text": "Vitamin D: Es liegen noch nicht genügend konsistente Detaildaten für eine belastbare Kurzbewertung vor." + } + ], + "inputHash": null, + "snapshotHash": "7b0d96db03101ae89d676758de35caf07ecfa9851cae13f993973747a0ee9771", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "medication-compliance", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": [ + { + "medicationId": "parity-rich-med-bp", + "text": "Ramipril: There is currently not enough consistent detail data for a robust short assessment." + }, + { + "medicationId": "parity-rich-med-d", + "text": "Vitamin D: There is currently not enough consistent detail data for a robust short assessment." + } + ], + "inputHash": null, + "snapshotHash": "96238702f707ead7f9c5c3505918975922674490fd7fb012aaa5f54631ecbe07", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "medication-compliance", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + } + ] + }, + "blocked-es": { + "calls": [ + { + "system": [ + "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", + "", + "YOUR DATA (graded snapshot):", + "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", + "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", + "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", + "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", + "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", + "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", + "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", + "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", + "", + "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", + "Moves available (choose the few that genuinely apply):", + "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", + "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", + "(c) name a genuine, earned win or a steady streak when the data shows one;", + "(d) one watch-item worth keeping an eye on;", + "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", + "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", + "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", + "", + "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", + "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", + "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", + "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", + "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", + "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", + "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", + "", + "OPENING — lead with meaning, not the metric value", + "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", + "", + "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", + "", + "JUDGE THE DATA HONESTLY (never invent a trend):", + "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", + "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", + "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", + "", + "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", + "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", + "", + "METRIC IDENTIFIERS STAY OUT OF PROSE", + "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", + "", + "GLP-1 DOSE SAFETY (contract, not style)", + "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", + "", + "ACUTE RED FLAGS (contract, not style)", + "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", + "", + "GROUNDING — ZERO HALLUCINATIONS", + "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", + "", + "TONE — a warm, motivating advisor, never clinical or alarming", + "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", + "", + "NOT AN INSIGHT — never recite mechanics", + "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", + "", + "INTERPRET, DON'T ENUMERATE", + "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", + "", + "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", + "", + "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", + "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", + "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", + "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", + "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", + "", + "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in Spanish: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", + "{ \"summary\": \"...\" }", + "", + "METRIC — MEDICATION ADHERENCE:", + "- The snapshot carries overall (medicationCount, averageCompliance7, averageCompliance30) and medications[] per medication with name, dose, schedulesPerDay, compliance7, compliance30, streak, taken7/skipped7/missed7, signal (the finished trend comparison), dailySeries (graded) and latestDay.", + "- Read adherence as the percentage of scheduled doses taken over the last 7 and 30 days; compliance7/compliance30 are server-computed. For the recent DIRECTION against the person's own baseline, lead from medications[].signal — current, delta vs baselineLabel and outsideNormalSwing are already computed; STATE them, do NOT recompute.", + "- Rare cadence: with a low schedulesPerDay or an infrequent schedule (e.g. a weekly injection), compliance7 is noise-prone — a single missed dose swings the percentage sharply. Lean the assessment on compliance30 (the load-bearing value) and treat compliance7 only as a rough pointer.", + "- Placement (for orientation, not judgement): ≥ 90% very reliable, 80-89% good, 70-79% patchy, < 70% clearly patchy — therapeutic effect may then be reduced.", + "- Frame positively: streak is the longest uninterrupted run — acknowledge a good streak explicitly. With several medications, name the one with the lowest adherence.", + "- Mention links to vital signs only when an r-value is present in the snapshot and |r| > 0.4 — as an association, never a cause. Do not invent an effect when the data is absent.", + "- One message: close with ONE doable step that fits the gap ONLY when the finding implies one (e.g. evening doses are missed more often — a fixed routine or a reminder at the critical time can help). When adherence is consistently high, acknowledge that honestly rather than manufacturing a step. No blame.", + "", + "OUTPUT LANGUAGE: Responda en español (registro peninsular formal — usted). Use un vocabulario sanitario en español natural." + ], + "messages": [ + { + "role": "user", + "content": [ + "Date: 2026-06-15", + "OPENER HINT: Open with the direction of change over time, then say where it stands now.", + "Write one short assessment of this person's medication adherence. Open with what it MEANS in plain words — how reliable the routine is looking, not the number (e.g. \"your routine's been rock-solid\", \"a few doses have slipped lately\") — then bring in the recent rate right after as support, placed against their own baseline (compliance7 vs compliance30); never lead with the value. Close with one doable, blame-free step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the event count and recency.", + "", + "PREVIOUS ANALYSIS: none on file. Treat this as the user's first analysis for this domain — no improvement/regression delta to surface.", + "", + "", + "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der Konstanz bzw. Streuung der Werte eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", + "", + "DATENLAGE: 176 Tages-Buckets im Fenster, letzter Wert vor 1 Tag.", + "", + "", + "{\"locale\":\"es\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"medication_compliance\",\"dataCoverage\":{\"totalMeasurements\":176,\"totalSpanDays\":98,\"newestMeasurementDaysAgo\":1},\"overall\":{\"medicationCount\":2,\"averageCompliance30\":79,\"averageCompliance7\":67},\"medications\":[{\"medicationId\":\"parity-rich-med-bp\",\"name\":\"Ramipril\",\"dose\":\"5mg\",\"category\":\"BLOOD_PRESSURE\",\"schedulesPerDay\":1,\"compliance7\":67,\"compliance30\":79,\"streak\":0,\"taken7\":4,\"skipped7\":0,\"missed7\":2,\"signal\":{\"metric\":\"Einnahmetreue für Ramipril\",\"unit\":\"%\",\"current\":80,\"currentWindowDays\":20,\"baseline\":21.88,\"baselineLabel\":\"your monthly average\",\"delta\":58.13,\"deltaPct\":265.7,\"spread\":30.94,\"outsideNormalSwing\":true,\"direction\":\"higher-better\",\"n\":20,\"newestDaysAgo\":null},\"dailySeries\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-27\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-28\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-29\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-30\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-31\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-01\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-02\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-03\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-04\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-05\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-06\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-07\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-08\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-09\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-10\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-11\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-12\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-13\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-14\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":0,\"max\":100,\"mean\":83.33,\"n\":6},{\"weekISO\":\"2026-W13\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W14\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W15\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W16\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":100,\"max\":100,\"mean\":100,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1}],\"monthly\":[{\"month\":\"2026-02\",\"min\":0,\"max\":0,\"mean\":0,\"n\":13},{\"month\":\"2026-03\",\"min\":0,\"max\":100,\"mean\":43.75,\"n\":16}],\"yearly\":[]},\"latestDay\":{\"dayOffset\":1,\"value\":100,\"n\":1}},{\"medicationId\":\"parity-rich-med-d\",\"name\":\"Vitamin D\",\"dose\":\"1000 IE\",\"category\":\"OTHER\",\"schedulesPerDay\":1,\"compliance7\":67,\"compliance30\":79,\"streak\":0,\"taken7\":4,\"skipped7\":0,\"missed7\":2,\"signal\":{\"metric\":\"Einnahmetreue für Vitamin D\",\"unit\":\"%\",\"current\":80,\"currentWindowDays\":20,\"baseline\":21.88,\"baselineLabel\":\"your monthly average\",\"delta\":58.13,\"deltaPct\":265.7,\"spread\":30.94,\"outsideNormalSwing\":true,\"direction\":\"higher-better\",\"n\":20,\"newestDaysAgo\":null},\"dailySeries\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-27\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-28\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-29\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-30\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-05-31\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-01\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-02\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-03\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-04\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-05\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-06\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-07\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-08\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-09\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-10\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-11\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-12\",\"min\":0,\"max\":0,\"mean\":0,\"n\":1},{\"date\":\"2026-06-13\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1},{\"date\":\"2026-06-14\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":0,\"max\":100,\"mean\":83.33,\"n\":6},{\"weekISO\":\"2026-W13\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W14\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W15\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W16\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":100,\"max\":100,\"mean\":100,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":0,\"max\":100,\"mean\":85.71,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":0,\"max\":100,\"mean\":71.43,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":100,\"max\":100,\"mean\":100,\"n\":1}],\"monthly\":[{\"month\":\"2026-02\",\"min\":0,\"max\":0,\"mean\":0,\"n\":13},{\"month\":\"2026-03\",\"min\":0,\"max\":100,\"mean\":43.75,\"n\":16}],\"yearly\":[]},\"latestDay\":{\"dayOffset\":1,\"value\":100,\"n\":1}}]}" + ] + } + ], + "temperature": 0.45, + "maxTokens": 250, + "seed": 1618033, + "responseFormat": "json", + "timeoutMs": 60000 + } + ], + "enqueued": [], + "result": { + "hasProvider": true, + "summary": "Ahora mismo no hay ninguna valoración disponible. La adherencia se lee como constancia a lo largo de semanas y no como una racha de días perfectos — por medicamento y en el conjunto. Los olvidos repetidos a la misma hora son el patrón que cuenta, y lo que suele recuperarlos es una rutina fija.", + "medications": [ + { + "medicationId": "parity-rich-med-bp", + "text": "Ramipril: There is currently not enough consistent detail data for a robust short assessment." + }, + { + "medicationId": "parity-rich-med-d", + "text": "Vitamin D: There is currently not enough consistent detail data for a robust short assessment." + } + ], + "cached": true, + "updatedAt": null + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "medication-compliance", + "locale": "de", + "text": "Das ist kein JSON.", + "items": [ + { + "medicationId": "parity-rich-med-bp", + "text": "Ramipril: Es liegen noch nicht genügend konsistente Detaildaten für eine belastbare Kurzbewertung vor." + }, + { + "medicationId": "parity-rich-med-d", + "text": "Vitamin D: Es liegen noch nicht genügend konsistente Detaildaten für eine belastbare Kurzbewertung vor." + } + ], + "inputHash": null, + "snapshotHash": "7b0d96db03101ae89d676758de35caf07ecfa9851cae13f993973747a0ee9771", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "medication-compliance", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": [ + { + "medicationId": "parity-rich-med-bp", + "text": "Ramipril: There is currently not enough consistent detail data for a robust short assessment." + }, + { + "medicationId": "parity-rich-med-d", + "text": "Vitamin D: There is currently not enough consistent detail data for a robust short assessment." + } + ], + "inputHash": null, + "snapshotHash": "96238702f707ead7f9c5c3505918975922674490fd7fb012aaa5f54631ecbe07", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "medication-compliance", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "medication-compliance", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + } + ] + }, + "bare-error-en": { + "calls": [], + "enqueued": [], + "result": { + "hasProvider": true, + "summary": "There are currently no active medications configured.", + "medications": [], + "cached": true, + "updatedAt": null + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "medication-compliance", + "locale": "de", + "text": "Das ist kein JSON.", + "items": [ + { + "medicationId": "parity-rich-med-bp", + "text": "Ramipril: Es liegen noch nicht genügend konsistente Detaildaten für eine belastbare Kurzbewertung vor." + }, + { + "medicationId": "parity-rich-med-d", + "text": "Vitamin D: Es liegen noch nicht genügend konsistente Detaildaten für eine belastbare Kurzbewertung vor." + } + ], + "inputHash": null, + "snapshotHash": "7b0d96db03101ae89d676758de35caf07ecfa9851cae13f993973747a0ee9771", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "medication-compliance", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": [ + { + "medicationId": "parity-rich-med-bp", + "text": "Ramipril: There is currently not enough consistent detail data for a robust short assessment." + }, + { + "medicationId": "parity-rich-med-d", + "text": "Vitamin D: There is currently not enough consistent detail data for a robust short assessment." + } + ], + "inputHash": null, + "snapshotHash": "96238702f707ead7f9c5c3505918975922674490fd7fb012aaa5f54631ecbe07", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "medication-compliance", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "medication-compliance", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + } + ] + }, + "bare-error-de": { + "calls": [], + "enqueued": [], + "result": { + "hasProvider": true, + "summary": "Aktuell sind keine aktiven Medikamente hinterlegt.", + "medications": [], + "cached": true, + "updatedAt": null + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "medication-compliance", + "locale": "de", + "text": "Das ist kein JSON.", + "items": [ + { + "medicationId": "parity-rich-med-bp", + "text": "Ramipril: Es liegen noch nicht genügend konsistente Detaildaten für eine belastbare Kurzbewertung vor." + }, + { + "medicationId": "parity-rich-med-d", + "text": "Vitamin D: Es liegen noch nicht genügend konsistente Detaildaten für eine belastbare Kurzbewertung vor." + } + ], + "inputHash": null, + "snapshotHash": "7b0d96db03101ae89d676758de35caf07ecfa9851cae13f993973747a0ee9771", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "medication-compliance", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": [ + { + "medicationId": "parity-rich-med-bp", + "text": "Ramipril: There is currently not enough consistent detail data for a robust short assessment." + }, + { + "medicationId": "parity-rich-med-d", + "text": "Vitamin D: There is currently not enough consistent detail data for a robust short assessment." + } + ], + "inputHash": null, + "snapshotHash": "96238702f707ead7f9c5c3505918975922674490fd7fb012aaa5f54631ecbe07", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "medication-compliance", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "medication-compliance", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + } + ] + }, + "keyless-en": { + "calls": [], + "enqueued": [], + "result": { + "hasProvider": false, + "summary": "No assessment on this one right now. Adherence reads as consistency over weeks rather than a run of perfect days — per medication and in the overall picture. Repeated misses at the same time of day are the pattern that counts, and a fixed routine is what usually catches them.", + "medications": [], + "cached": true, + "updatedAt": null + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "medication-compliance", + "locale": "de", + "text": "Das ist kein JSON.", + "items": [ + { + "medicationId": "parity-rich-med-bp", + "text": "Ramipril: Es liegen noch nicht genügend konsistente Detaildaten für eine belastbare Kurzbewertung vor." + }, + { + "medicationId": "parity-rich-med-d", + "text": "Vitamin D: Es liegen noch nicht genügend konsistente Detaildaten für eine belastbare Kurzbewertung vor." + } + ], + "inputHash": null, + "snapshotHash": "7b0d96db03101ae89d676758de35caf07ecfa9851cae13f993973747a0ee9771", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "medication-compliance", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": [ + { + "medicationId": "parity-rich-med-bp", + "text": "Ramipril: There is currently not enough consistent detail data for a robust short assessment." + }, + { + "medicationId": "parity-rich-med-d", + "text": "Vitamin D: There is currently not enough consistent detail data for a robust short assessment." + } + ], + "inputHash": null, + "snapshotHash": "96238702f707ead7f9c5c3505918975922674490fd7fb012aaa5f54631ecbe07", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "medication-compliance", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "medication-compliance", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + } + ] + }, + "keyless-de": { + "calls": [], + "enqueued": [], + "result": { + "hasProvider": false, + "summary": "Für diese Karte liegt gerade keine Einschätzung vor. Einnahmetreue liest sich als Konstanz über Wochen, nicht als eine Reihe perfekter Tage — je Medikament und im Gesamtbild. Wiederkehrende Auslassungen zur selben Tageszeit sind das Muster, auf das es ankommt, und eine feste Routine fängt sie am ehesten ab.", + "medications": [], + "cached": true, + "updatedAt": null + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "medication-compliance", + "locale": "de", + "text": "Das ist kein JSON.", + "items": [ + { + "medicationId": "parity-rich-med-bp", + "text": "Ramipril: Es liegen noch nicht genügend konsistente Detaildaten für eine belastbare Kurzbewertung vor." + }, + { + "medicationId": "parity-rich-med-d", + "text": "Vitamin D: Es liegen noch nicht genügend konsistente Detaildaten für eine belastbare Kurzbewertung vor." + } + ], + "inputHash": null, + "snapshotHash": "7b0d96db03101ae89d676758de35caf07ecfa9851cae13f993973747a0ee9771", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "medication-compliance", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": [ + { + "medicationId": "parity-rich-med-bp", + "text": "Ramipril: There is currently not enough consistent detail data for a robust short assessment." + }, + { + "medicationId": "parity-rich-med-d", + "text": "Vitamin D: There is currently not enough consistent detail data for a robust short assessment." + } + ], + "inputHash": null, + "snapshotHash": "96238702f707ead7f9c5c3505918975922674490fd7fb012aaa5f54631ecbe07", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "medication-compliance", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "medication-compliance", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + } + ] + }, + "keyless-read-only": { + "calls": [], + "enqueued": [], + "result": { + "hasProvider": false, + "summary": "No assessment on this one right now. Adherence reads as consistency over weeks rather than a run of perfect days — per medication and in the overall picture. Repeated misses at the same time of day are the pattern that counts, and a fixed routine is what usually catches them.", + "medications": [], + "cached": true, + "updatedAt": null + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "medication-compliance", + "locale": "de", + "text": "Das ist kein JSON.", + "items": [ + { + "medicationId": "parity-rich-med-bp", + "text": "Ramipril: Es liegen noch nicht genügend konsistente Detaildaten für eine belastbare Kurzbewertung vor." + }, + { + "medicationId": "parity-rich-med-d", + "text": "Vitamin D: Es liegen noch nicht genügend konsistente Detaildaten für eine belastbare Kurzbewertung vor." + } + ], + "inputHash": null, + "snapshotHash": "7b0d96db03101ae89d676758de35caf07ecfa9851cae13f993973747a0ee9771", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "medication-compliance", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": [ + { + "medicationId": "parity-rich-med-bp", + "text": "Ramipril: There is currently not enough consistent detail data for a robust short assessment." + }, + { + "medicationId": "parity-rich-med-d", + "text": "Vitamin D: There is currently not enough consistent detail data for a robust short assessment." + } + ], + "inputHash": null, + "snapshotHash": "96238702f707ead7f9c5c3505918975922674490fd7fb012aaa5f54631ecbe07", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "medication-compliance", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "medication-compliance", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + } + ] + } +} diff --git a/tests/integration/__golden__/status-parity/metric.json b/tests/integration/__golden__/status-parity/metric.json new file mode 100644 index 000000000..58b9188c2 --- /dev/null +++ b/tests/integration/__golden__/status-parity/metric.json @@ -0,0 +1,6149 @@ +{ + "RESTING_HEART_RATE read-only-cold": { + "calls": [], + "enqueued": [ + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "en" + } + ], + "result": { + "hasProvider": true, + "text": null, + "cached": false, + "updatedAt": null, + "preparing": true, + "revalidating": false + }, + "notes": [] + }, + "RESTING_HEART_RATE fresh-en": { + "calls": [ + { + "system": [ + "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", + "", + "YOUR DATA (graded snapshot):", + "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", + "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", + "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", + "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", + "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", + "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", + "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", + "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", + "", + "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", + "Moves available (choose the few that genuinely apply):", + "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", + "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", + "(c) name a genuine, earned win or a steady streak when the data shows one;", + "(d) one watch-item worth keeping an eye on;", + "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", + "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", + "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", + "", + "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", + "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", + "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", + "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", + "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", + "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", + "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", + "", + "OPENING — lead with meaning, not the metric value", + "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", + "", + "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", + "", + "JUDGE THE DATA HONESTLY (never invent a trend):", + "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", + "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", + "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", + "", + "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", + "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", + "", + "METRIC IDENTIFIERS STAY OUT OF PROSE", + "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", + "", + "GLP-1 DOSE SAFETY (contract, not style)", + "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", + "", + "ACUTE RED FLAGS (contract, not style)", + "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", + "", + "GROUNDING — ZERO HALLUCINATIONS", + "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", + "", + "TONE — a warm, motivating advisor, never clinical or alarming", + "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", + "", + "NOT AN INSIGHT — never recite mechanics", + "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", + "", + "INTERPRET, DON'T ENUMERATE", + "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", + "", + "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", + "", + "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", + "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", + "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", + "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", + "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", + "", + "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in English: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", + "{ \"summary\": \"...\" }", + "", + "ARCHETYPE — PHYSIOLOGICAL VITAL:", + "- This is a point-in-time physiological reading. Name the recent level, place it against both the user's own baseline (recent vs monthly/yearly mean) and, when a reference band is given, that band.", + "- A single out-of-band reading is rarely a finding; a sustained shift across the recent window against the user's baseline is. Note an out-of-band trend without alarm.", + "- Frame any direction (rising/falling) as a possible explanation (fitness, stress, sleep, illness, medication), never a diagnosis.", + "", + "METRIC: Resting heart rate (unit: bpm).", + "Lower values are generally more favourable for this metric.", + "Coarse population reference band: 50–100 bpm (a placement aid only — the user's OWN baseline leads)." + ], + "messages": [ + { + "role": "user", + "content": [ + "Date: 2026-06-15", + "OPENER HINT: Open with the direction of change over time, then say where it stands now.", + "Write one short assessment of this person's resting heart rate. Open with what it MEANS in plain words — the read, not the number (e.g. \"running a little calmer than usual\", \"steady and right where it's been\") — then bring in ONE concrete number from the snapshot right after as support, placed against their own weekly/monthly baseline; never lead with the value. Close with one doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the measurement count and recency.", + "", + "PREVIOUS ANALYSIS: none on file. Treat this as the user's first analysis for this domain — no improvement/regression delta to surface.", + "", + "", + "VARIETY: to keep your cards from reading the same, you may open this one by leading with how consistent or variable the readings have been — only if the data genuinely supports that angle; never force it, never invent a trend.", + "", + "DATA STRENGTH: 140 day-buckets in the window, latest reading today.", + "", + "REPETITION: you have already given a similar \"no material change\" assessment for this metric 11 time(s) in a row. Do NOT restate the same level again. Acknowledge the continuity in one short clause (\"still holding steady, 12 checks running\") and then pivot to a DIFFERENT facet — consistency, time-of-day pattern, or a correlated metric — or, if nothing is genuinely actionable, say so plainly and skip the manufactured step.", + "", + "RELATIONS (statistically-screened associations in YOUR data involving this metric — descriptive, NEVER causal):", + "- Higher blood glucose looks like it goes with higher next-day resting heart rate in your data, on the evidence so far. It is a pattern to watch, not a cause. (n=34, r=0.94)", + "- Higher sleep duration looks like it goes with higher next-day resting heart rate in your data, on the evidence so far. It is a pattern to watch, not a cause. (n=89, r=0.86)", + "You MAY weave at most ONE of these in if it adds genuine insight, phrased as an \"association\" / \"tends to go with\" — never as a cause, and never alter the numbers. Skip them if the metric's own finding is the stronger story.", + "", + "", + "INTERPRETATION CONTEXT (guideline bands — computed server-side; state, do NOT recompute):", + "- Current value 60.62 bpm is in the \"the standard resting range\" band, and sits near the LOWER boundary of this band (nearest boundary at 60 bpm).", + "- Guideline bands (AHA 2024; lower values are more favourable):", + " - below 60 bpm: below the standard resting range (common in trained hearts)", + " - 60–100 bpm: the standard resting range", + " - 100 bpm and above: above the standard resting range", + "- Values in the \"the standard resting range\" band are considered a healthy / low-risk range per these guidelines.", + "- Name where the value sits and what that band means in plain words. Judge any trend BY this position: a shift deep inside a favourable band is a footnote; the same shift approaching or crossing a boundary is the headline. Frame consequence without diagnosis (\"values in this range are considered …\", never \"you have / are at risk of …\").", + "", + "", + "{\"locale\":\"en\",\"promptVersion\":\"6.3.0\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"RESTING_HEART_RATE\",\"metric\":{\"id\":\"RESTING_HEART_RATE\",\"displayName\":\"Resting heart rate\",\"unit\":\"bpm\",\"direction\":\"lower-better\",\"normalRange\":{\"low\":55,\"high\":95},\"normalRangeSource\":\"age-sex-adjusted\"},\"profile\":{\"ageYears\":42,\"sex\":\"MALE\"},\"dataCoverage\":{\"totalMeasurements\":140,\"totalSpanDays\":139,\"newestMeasurementDaysAgo\":0},\"RESTING_HEART_RATE\":{\"signal\":{\"metric\":\"Resting heart rate\",\"unit\":\"bpm\",\"current\":60.62,\"currentWindowDays\":21,\"baseline\":60.71,\"baselineLabel\":\"your monthly average\",\"delta\":-0.09,\"deltaPct\":-0.1,\"spread\":1.42,\"outsideNormalSwing\":false,\"direction\":\"lower-better\",\"n\":21,\"newestDaysAgo\":0,\"normalRange\":{\"low\":55,\"high\":95,\"source\":\"age-sex-adjusted\"},\"placement\":\"in band\"},\"summary\":{\"points\":140,\"start\":58,\"end\":62,\"delta\":4,\"mean\":59.71,\"min\":55,\"max\":64},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":56,\"max\":56,\"mean\":56,\"n\":1},{\"date\":\"2026-05-27\",\"min\":59,\"max\":59,\"mean\":59,\"n\":1},{\"date\":\"2026-05-28\",\"min\":59,\"max\":59,\"mean\":59,\"n\":1},{\"date\":\"2026-05-29\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1},{\"date\":\"2026-05-30\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1},{\"date\":\"2026-05-31\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-01\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-02\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-03\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-04\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-05\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-06\",\"min\":62,\"max\":62,\"mean\":62,\"n\":1},{\"date\":\"2026-06-07\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-08\",\"min\":64,\"max\":64,\"mean\":64,\"n\":1},{\"date\":\"2026-06-09\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-10\",\"min\":62,\"max\":62,\"mean\":62,\"n\":1},{\"date\":\"2026-06-11\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-12\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-13\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-14\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-15\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":55,\"max\":58,\"mean\":56.67,\"n\":6},{\"weekISO\":\"2026-W13\",\"min\":55,\"max\":61,\"mean\":58.29,\"n\":7},{\"weekISO\":\"2026-W14\",\"min\":59,\"max\":64,\"mean\":61.57,\"n\":7},{\"weekISO\":\"2026-W15\",\"min\":59,\"max\":63,\"mean\":61.43,\"n\":7},{\"weekISO\":\"2026-W16\",\"min\":56,\"max\":61,\"mean\":58.29,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":55,\"max\":59,\"mean\":57,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":57,\"max\":63,\"mean\":60,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":60,\"max\":64,\"mean\":62.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":58,\"max\":63,\"mean\":60,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":55,\"max\":59,\"mean\":56.86,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":56,\"max\":56,\"mean\":56,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":61,\"max\":64,\"mean\":62,\"n\":5},{\"month\":\"2026-02\",\"min\":55,\"max\":64,\"mean\":59.18,\"n\":28},{\"month\":\"2026-03\",\"min\":57,\"max\":64,\"mean\":60.94,\"n\":16}],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":0,\"value\":58}}}" + ] + } + ], + "temperature": 0.45, + "maxTokens": 700, + "seed": 1618033, + "responseFormat": "json", + "timeoutMs": 60000 + } + ], + "enqueued": [], + "result": { + "hasProvider": true, + "text": "Parity note for fresh-en.", + "cached": false, + "updatedAt": "2026-06-15T10:00:00.000Z" + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "en", + "text": "Parity note for fresh-en.", + "items": null, + "inputHash": null, + "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + } + ] + }, + "RESTING_HEART_RATE cached-en": { + "calls": [], + "enqueued": [], + "result": { + "hasProvider": true, + "text": "Parity note for fresh-en.", + "cached": true, + "updatedAt": "2026-06-15T10:00:00.000Z" + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "en", + "text": "Parity note for fresh-en.", + "items": null, + "inputHash": null, + "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + } + ] + }, + "RESTING_HEART_RATE forced-unchanged-en": { + "calls": [], + "enqueued": [], + "result": { + "hasProvider": true, + "text": "Parity note for fresh-en.", + "cached": true, + "updatedAt": "2026-06-15T10:00:00.000Z" + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "en", + "text": "Parity note for fresh-en.", + "items": null, + "inputHash": null, + "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + } + ] + }, + "RESTING_HEART_RATE next-day-en": { + "calls": [ + { + "system": [ + "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", + "", + "YOUR DATA (graded snapshot):", + "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", + "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", + "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", + "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", + "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", + "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", + "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", + "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", + "", + "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", + "Moves available (choose the few that genuinely apply):", + "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", + "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", + "(c) name a genuine, earned win or a steady streak when the data shows one;", + "(d) one watch-item worth keeping an eye on;", + "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", + "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", + "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", + "", + "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", + "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", + "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", + "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", + "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", + "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", + "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", + "", + "OPENING — lead with meaning, not the metric value", + "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", + "", + "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", + "", + "JUDGE THE DATA HONESTLY (never invent a trend):", + "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", + "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", + "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", + "", + "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", + "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", + "", + "METRIC IDENTIFIERS STAY OUT OF PROSE", + "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", + "", + "GLP-1 DOSE SAFETY (contract, not style)", + "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", + "", + "ACUTE RED FLAGS (contract, not style)", + "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", + "", + "GROUNDING — ZERO HALLUCINATIONS", + "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", + "", + "TONE — a warm, motivating advisor, never clinical or alarming", + "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", + "", + "NOT AN INSIGHT — never recite mechanics", + "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", + "", + "INTERPRET, DON'T ENUMERATE", + "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", + "", + "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", + "", + "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", + "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", + "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", + "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", + "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", + "", + "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in English: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", + "{ \"summary\": \"...\" }", + "", + "ARCHETYPE — PHYSIOLOGICAL VITAL:", + "- This is a point-in-time physiological reading. Name the recent level, place it against both the user's own baseline (recent vs monthly/yearly mean) and, when a reference band is given, that band.", + "- A single out-of-band reading is rarely a finding; a sustained shift across the recent window against the user's baseline is. Note an out-of-band trend without alarm.", + "- Frame any direction (rising/falling) as a possible explanation (fitness, stress, sleep, illness, medication), never a diagnosis.", + "", + "METRIC: Resting heart rate (unit: bpm).", + "Lower values are generally more favourable for this metric.", + "Coarse population reference band: 50–100 bpm (a placement aid only — the user's OWN baseline leads)." + ], + "messages": [ + { + "role": "user", + "content": [ + "Date: 2026-06-16", + "OPENER HINT: Open with what this means for the user today, then the number behind it.", + "Write one short assessment of this person's resting heart rate. Open with what it MEANS in plain words — the read, not the number (e.g. \"running a little calmer than usual\", \"steady and right where it's been\") — then bring in ONE concrete number from the snapshot right after as support, placed against their own weekly/monthly baseline; never lead with the value. Close with one doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the measurement count and recency.", + "", + "PREVIOUS ANALYSIS (1 day ago, 2026-06-15):", + "Parity note for fresh-en.", + "", + "INSTRUCTION: When summarising the current state, explicitly call out what improved, what regressed, and what stayed the same compared to that previous snapshot. Use phrases like \"down 4 mmHg from your last check\" or \"same elevated trend as last week\". Do not invent numbers — only compare what's in the snapshot below.", + "", + "", + "VARIETY: to keep your cards from reading the same, you may open this one by leading with how consistent or variable the readings have been — only if the data genuinely supports that angle; never force it, never invent a trend.", + "", + "DATA STRENGTH: 140 day-buckets in the window, latest reading 1 day ago.", + "", + "REPETITION: you have already given a similar \"no material change\" assessment for this metric 11 time(s) in a row. Do NOT restate the same level again. Acknowledge the continuity in one short clause (\"still holding steady, 12 checks running\") and then pivot to a DIFFERENT facet — consistency, time-of-day pattern, or a correlated metric — or, if nothing is genuinely actionable, say so plainly and skip the manufactured step.", + "", + "RELATIONS (statistically-screened associations in YOUR data involving this metric — descriptive, NEVER causal):", + "- Higher blood glucose looks like it goes with higher next-day resting heart rate in your data, on the evidence so far. It is a pattern to watch, not a cause. (n=34, r=0.94)", + "- Higher sleep duration looks like it goes with higher next-day resting heart rate in your data, on the evidence so far. It is a pattern to watch, not a cause. (n=89, r=0.86)", + "You MAY weave at most ONE of these in if it adds genuine insight, phrased as an \"association\" / \"tends to go with\" — never as a cause, and never alter the numbers. Skip them if the metric's own finding is the stronger story.", + "", + "", + "INTERPRETATION CONTEXT (guideline bands — computed server-side; state, do NOT recompute):", + "- Current value 60.85 bpm is in the \"the standard resting range\" band, and sits near the LOWER boundary of this band (nearest boundary at 60 bpm).", + "- Guideline bands (AHA 2024; lower values are more favourable):", + " - below 60 bpm: below the standard resting range (common in trained hearts)", + " - 60–100 bpm: the standard resting range", + " - 100 bpm and above: above the standard resting range", + "- Values in the \"the standard resting range\" band are considered a healthy / low-risk range per these guidelines.", + "- Name where the value sits and what that band means in plain words. Judge any trend BY this position: a shift deep inside a favourable band is a footnote; the same shift approaching or crossing a boundary is the headline. Frame consequence without diagnosis (\"values in this range are considered …\", never \"you have / are at risk of …\").", + "", + "", + "{\"locale\":\"en\",\"promptVersion\":\"6.3.0\",\"generatedForDay\":\"2026-06-16\",\"focus\":\"RESTING_HEART_RATE\",\"metric\":{\"id\":\"RESTING_HEART_RATE\",\"displayName\":\"Resting heart rate\",\"unit\":\"bpm\",\"direction\":\"lower-better\",\"normalRange\":{\"low\":55,\"high\":95},\"normalRangeSource\":\"age-sex-adjusted\"},\"profile\":{\"ageYears\":42,\"sex\":\"MALE\"},\"dataCoverage\":{\"totalMeasurements\":140,\"totalSpanDays\":139,\"newestMeasurementDaysAgo\":1},\"RESTING_HEART_RATE\":{\"signal\":{\"metric\":\"Resting heart rate\",\"unit\":\"bpm\",\"current\":60.85,\"currentWindowDays\":20,\"baseline\":60.65,\"baselineLabel\":\"your monthly average\",\"delta\":0.2,\"deltaPct\":0.3,\"spread\":1.41,\"outsideNormalSwing\":false,\"direction\":\"lower-better\",\"n\":20,\"newestDaysAgo\":1,\"normalRange\":{\"low\":55,\"high\":95,\"source\":\"age-sex-adjusted\"},\"placement\":\"in band\"},\"summary\":{\"points\":140,\"start\":58,\"end\":62,\"delta\":4,\"mean\":59.71,\"min\":55,\"max\":64},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":59,\"max\":59,\"mean\":59,\"n\":1},{\"date\":\"2026-05-28\",\"min\":59,\"max\":59,\"mean\":59,\"n\":1},{\"date\":\"2026-05-29\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1},{\"date\":\"2026-05-30\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1},{\"date\":\"2026-05-31\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-01\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-02\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-03\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-04\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-05\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-06\",\"min\":62,\"max\":62,\"mean\":62,\"n\":1},{\"date\":\"2026-06-07\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-08\",\"min\":64,\"max\":64,\"mean\":64,\"n\":1},{\"date\":\"2026-06-09\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-10\",\"min\":62,\"max\":62,\"mean\":62,\"n\":1},{\"date\":\"2026-06-11\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-12\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-13\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-14\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-15\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":55,\"max\":58,\"mean\":56.4,\"n\":5},{\"weekISO\":\"2026-W13\",\"min\":55,\"max\":61,\"mean\":58.29,\"n\":7},{\"weekISO\":\"2026-W14\",\"min\":59,\"max\":64,\"mean\":61.57,\"n\":7},{\"weekISO\":\"2026-W15\",\"min\":59,\"max\":63,\"mean\":61.43,\"n\":7},{\"weekISO\":\"2026-W16\",\"min\":56,\"max\":61,\"mean\":58.29,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":55,\"max\":59,\"mean\":57,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":57,\"max\":63,\"mean\":60,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":60,\"max\":64,\"mean\":62.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":58,\"max\":63,\"mean\":60,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":55,\"max\":59,\"mean\":56.86,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":56,\"max\":56,\"mean\":56,\"n\":2}],\"monthly\":[{\"month\":\"2026-01\",\"min\":61,\"max\":64,\"mean\":62,\"n\":5},{\"month\":\"2026-02\",\"min\":55,\"max\":64,\"mean\":59.18,\"n\":28},{\"month\":\"2026-03\",\"min\":57,\"max\":64,\"mean\":60.76,\"n\":17}],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":1,\"value\":58}}}" + ] + } + ], + "temperature": 0.45, + "maxTokens": 700, + "seed": 1618033, + "responseFormat": "json", + "timeoutMs": 60000 + } + ], + "enqueued": [], + "result": { + "hasProvider": true, + "text": "Parity note for next-day-en.", + "cached": false, + "updatedAt": "2026-06-16T10:00:00.000Z" + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + } + ] + }, + "RESTING_HEART_RATE read-only-stale": { + "calls": [], + "enqueued": [ + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "en" + } + ], + "result": { + "hasProvider": true, + "text": "Parity note for next-day-en.", + "cached": true, + "updatedAt": "2026-06-16T10:00:00.000Z", + "preparing": false, + "revalidating": true + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + } + ] + }, + "RESTING_HEART_RATE prose-de": { + "calls": [ + { + "system": [ + "Du schreibst eine kurze, persönliche Einschätzung zu EINER Gesundheitsmetrik dieses Nutzers. Du kennst seine Daten und ordnest sie gegen seine EIGENE Baseline ein — nicht gegen Bevölkerungsnormen. Deine fachliche Grundlage sind anerkannte Leitlinien (ESH 2023, WHO, AASM, DGE, DEGAM); du beziehst dich darauf, ohne zu diagnostizieren.", + "", + "DEINE DATENGRUNDLAGE (graded snapshot):", + "- signal: der fertige Vergleich — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` ist das recent-Mittel; `baseline` ist der EIGENE längere Durchschnitt; `delta`/`deltaPct` sind die VORZEICHENBEHAFTETE Änderung gegenüber dieser Baseline; `spread` ist die normale Schwankung; `outsideNormalSwing` ist das vorab berechnete Urteil, ob die Änderung diese Schwankung übersteigt. Diese Werte sind bereits berechnet — nenne sie, rechne sie NICHT neu. `contributors[]` (nur Score-Komposite) benennt die 1–2 Teil-Scores, die den Score bewegt haben.", + "- summary: { points, start, end, delta, mean, min, max } — die Eckwerte des gesamten Lesefensters.", + "- series.recent[]: die jüngsten ~21 Tage, je ein Eintrag pro Tag ({ date, min, max, mean, n }).", + "- series.weekly[]: die ~10 Wochen davor, je ein ISO-Wochen-Aggregat.", + "- series.monthly[]: die ~12 Monate davor, je ein Monatsaggregat.", + "- series.yearly[]: alles ältere, je ein Jahr ({ year, mean, min, max, n, slope }).", + "- Die jüngsten Tage liegen also EINZELN vor, ältere Zeiträume als Wochen-, Monats- und Jahresaggregate. Ziehe die series nur für einen konkreten Tag heran; führe sonst mit dem signal-Block.", + "- Ein Slice kann leer sein, wenn die Historie kurz ist. Dann nicht so tun, als gäbe es ihn. Ist `signal.baseline` null, gibt es noch keine etablierte Baseline; sage das ehrlich, statt einen Vergleich zu erfinden.", + "", + "SO BAUST DU DIE EINSCHÄTZUNG — wähle die 2–3 BAUSTEINE, die zu DIESEN Daten passen, und ordne sie so, dass sie als EIN zusammenhängender Gedanke lesen, keine feste Checkliste. Führe gemäß dem OPENER-HINWEIS im User-Prompt, wenn einer mitgegeben ist — eröffne NICHT immer mit der Zahl. Variiere die Form von Metrik zu Metrik; eröffne nie zwei Metriken hintereinander gleich.", + "Verfügbare Bausteine (nimm die wenigen, die wirklich zutreffen):", + "(a) der aktuelle Befund — `current` aus dem signal-Block (oder ein Wert aus series.recent für eine Tagesfrage);", + "(b) gegen die EIGENE Baseline einordnen über das vorab berechnete vorzeichenbehaftete `delta` gegenüber `baselineLabel` — nenne es, leite es nicht neu her. Behandle `outsideNormalSwing: false` als \"innerhalb der üblichen Schwankung — kein Befund\" und ERFINDE KEINEN Trend; `outsideNormalSwing: true` ist die echte, meldenswerte Änderung. Ein `normalRange`/`placement` ist nur ein grober sekundärer Anker — das persönliche delta führt;", + "(c) einen echten, verdienten Erfolg oder eine stabile Serie benennen, wenn die Daten ihn zeigen;", + "(d) einen Punkt zum Im-Auge-behalten;", + "(e) einen Zusammenhang NUR, wenn ein r-Wert vorhanden und |r| > 0,4 ist, formuliert als \"bewegt sich mit\" / \"zeigt sich oft zusammen mit\", nie kausal;", + "(f) GENAU EINEN machbaren nächsten Schritt — und nur, wenn der Befund wirklich einen hergibt. Eine Botschaft = ein Verhalten, keine Liste. Ist der Wert stabil und im grünen Bereich, ERZWINGE KEINEN Schritt — bestätige kurz und nenne stattdessen einen Punkt zum Beobachten. Ein erfundener Schritt ist genau die Floskel, die wir vermeiden.", + "Synthese statt Aufzählung: die Geschichte dessen, was die Daten bedeuten, zählt mehr als das Wiederholen jeder Zahl.", + "", + "TONALITÄT — ein warmer, motivierender Premium-Begleiter (jemand hat sich die Daten dieser Person wirklich angesehen und etwas Wertvolles für sie gefunden):", + "- Zweite Person (\"dein Blutdruck\", \"deine Werte\"), warm, direkt, ehrlich.", + "- Motivierend und vorwärtsgewandt: Wenn die Daten es hergeben, benenne den echten Erfolg klar und baue ein wenig Schwung auf — die Person soll sich gesehen und unterstützt fühlen, nicht belehrt. Die Ermutigung muss durch die Zahlen VERDIENT sein, nie ein reflexhaftes Kompliment.", + "- Autonomie-unterstützend: \"kann helfen\", \"einen Versuch wert\", nie \"du musst\".", + "- Nie alarmierend, nie moralisierend, nie diagnostisch — keine Krankheitsbehauptung. Formuliere als begründete Beobachtung, nicht als ärztlichen Rat.", + "- Auch ungünstige Werte ehrlich benennen: Befund → gegen die eigene Baseline einordnen → ein kleiner machbarer Schritt, als Chance formuliert, nicht als Versagen. Nicht verharmlosen, nicht dramatisieren.", + "- Keine Floskeln und keine bloße Zahlenwiederholung: jede warme Zeile ist an eine echte Zahl oder eine echte Veränderung geknüpft. Ein generischer Positiv-Einstieg (\"Deine Werte sehen gut aus\") ist verboten — verdiene die Ermutigung mit dem konkreten Befund.", + "", + "ERÖFFNUNG — mit der Bedeutung führen, nicht mit dem Messwert", + "Beginne mit dem Eindruck in klaren Worten — was das heute über die Person sagt — und bring die Zahl direkt danach als Beleg, nie als Erstes. Eine Zeile, die mit einem Wert eröffnet (\"Dein Ruhepuls ist 61 …\"), liest sich wie ein Ablesen; dieselbe Zeile, die mit der Bedeutung eröffnet (\"Dein Herz läuft diese Woche etwas ruhiger — rund 61 …\"), liest sich wie ein Begleiter, der wirklich hingesehen hat. Jede Aussage bleibt an einer echten Zahl verankert; nur die REIHENFOLGE ändert sich, Bedeutung zuerst und die Zahl direkt dahinter. Das gilt, ob die Fläche eine Metrik abdeckt oder viele.", + "", + "LÄNGE: 2-4 Sätze, ca. 30-60 Wörter — ist der Wert aber stabil und nichts zu tun, ist EIN knapper Satz besser, als ihn auf Länge zu strecken. Knapp und hochwertig. Keine bloße Zahlenwiederholung, kein Fülltext, keine generischen Floskeln.", + "", + "DATENLAGE EHRLICH BEWERTEN (nie einen Trend erfinden):", + "- Nur wenige Messpunkte/Einträge/Ereignisse oder ein leerer recent-Slice: ehrlich sagen, dass es für eine belastbare Tendenz noch zu wenige Daten sind, und EINEN Hinweis geben (z.B. regelmäßiger erfassen). Keinen Trend behaupten. Was \"wenige\" bedeutet, hängt von der Metrik ab — bei Stimmung sind es Einträge, bei der Einnahmetreue geplante Dosen.", + "- Jüngste Messung deutlich älter als ~7 Tage (dataCoverage.newestMeasurementDaysAgo): darauf hinweisen, dass die Werte nicht mehr aktuell sind.", + "- Korrelationen NUR erwähnen, wenn der r-Wert im Snapshot vorhanden ist und |r| > 0.4 ist. Fehlt das Feld, keine Korrelation interpretieren oder erfinden. Immer als \"Zusammenhang\" formulieren, nie als \"Ursache\".", + "", + "VERBOTENE FLOSKELN (signalisieren ungegroundeten Fülltext — nie ausgeben, außer im Disclaimer)", + "\"achte auf ausreichend Schlaf\", \"trinke genug Wasser\", \"regelmäßige Bewegung\", \"ärztlicher Rat empfohlen\".", + "", + "METRIK-IDENTIFIER GEHÖREN NICHT IN DEN FLIESSTEXT", + "Schreibe niemals Datenbank- bzw. Enum-Namen wie \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" oder \"SLEEP_DURATION\" in nutzersichtbare Strings, und schreibe nie das wörtliche \"metric:\"-Chart-Token in den Fließtext. Verweise auf jede Metrik mit der natürlichsprachlichen Bezeichnung, die der Nutzer sieht — \"deine Systole\", \"dein Gewicht\", \"dein Puls\", \"deine Stimmung\", \"deine Medikamentenadhärenz\", \"dein Ruhepuls\", \"deine Schlafdauer\", \"deine Schritte\".", + "", + "GLP-1-DOSIS-SICHERHEIT (Vertrag, kein Stil)", + "Verschreibe, empfiehl oder ändere NIEMALS Medikamenten-Dosen, auch wenn der Snapshot einen GLP-1-Rezeptoragonisten benennt (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). Du darfst den benannten Wirkstoff und die aktuelle Titrationsstufe NENNEN (\"Woche 3 auf 7,5 mg\"), wenn der Snapshot sie trägt, aber schreibe nie \"erhöhe auf X mg\", \"erwäge die nächste Stufe Y mg\", \"bleibe auf Z mg\" oder eine Variante davon. Ein Plateau rahmt die nächste Entscheidung immer als Gespräch mit der behandelnden Ärztin. Bist du unsicher, ob etwas dosis-präskriptiv ist, behandle es so — und verweise an die Klinik.", + "", + "AKUTE WARNZEICHEN (Vertrag, kein Stil)", + "Beschreibt der Nutzer ein akutes Krisenzeichen — Brustschmerz oder Druck auf der Brust, Ohnmacht oder Beinahe-Ohnmacht (Synkope), ein plötzliches schweres Symptom (z. B. stärkster Kopfschmerz aller Zeiten, plötzliche Schwäche oder Sprachstörung, Atemnot), einen hypertensiven Notfallwert (systolisch ≥ 180 oder diastolisch ≥ 120 mit Symptomen), einen schwer-niedrigen oder sehr hohen Glukosewert (unter 54 mg/dL (3,0 mmol/L), oder unter 70 mg/dL (3,9 mmol/L) mit Symptomen, oder ≥ 250 mg/dL (13,9 mmol/L) mit Symptomen), anhaltendes Fieber ≥ 38,5 °C (101,3 °F) oder Gedanken an Suizid bzw. Selbstverletzung — sage in einem ruhigen Satz klar, dass das jetzt umgehende ärztliche Hilfe oder den Notruf braucht, und coache es NICHT anhand der Daten. Diagnostiziere nicht, benenne keine Erkrankung, schätze keinen Schweregrad aus den Zahlen — verweise nur auf umgehende Hilfe bzw. den Notruf und höre auf. Das ist eine geschlossene Liste; außerhalb davon bleibe wie gewohnt ruhig und nicht alarmierend.", + "", + "ERDUNG — NULL HALLUZINATIONEN", + "Jede Aussage muss auf einer Zahl beruhen, die im übergebenen Snapshot sichtbar ist. Lässt sich die Aussage keinem Snapshot-Feld zuordnen, lass sie weg und erfinde nie einen Messwert, um einer Anfrage zu entsprechen. Ordne jeden Wert gegen die EIGENE Baseline des Nutzers ein (jüngeres vs. längeres Mittel), nie gegen eine Bevölkerungsnorm. Nenne einen Zusammenhang nur, wenn der r-Wert vorhanden und |r| > 0,4 ist — immer als \"Zusammenhang\", nie als \"Ursache\".", + "", + "TONALITÄT — ein warmer, motivierender Begleiter, nie klinisch oder alarmierend", + "Schreibe in der zweiten Person, warm und direkt. Wenn die Daten es hergeben, benenne den echten Erfolg klar und baue ein wenig Schwung auf — die Person soll sich gesehen und unterstützt fühlen, nicht belehrt. Die Ermutigung muss durch die Zahlen VERDIENT sein, nie ein reflexhaftes Kompliment. Lob ist verankert, konkret und angemessen: an einer echten Zahl oder Veränderung festgemacht, nicht auf andere Nutzer übertragbar, und stiller Anerkennung für stille Erfolge. Über-Bestätigung ist eine Sicherheitsregression, keine Wärme — bestätige nie einen sich verschlechternden Trend oder eine unsichere Entscheidung; würdige die Mühe, nicht die Entscheidung, und bleib neutral (\"nichts zu tun — die gute Art von langweilig\"), wenn es nichts zu loben gibt, statt ein Kompliment zu erfinden. Sei autonomie-unterstützend (\"einen Versuch wert\", \"kann helfen\", nie \"du musst\"). Benenne auch ungünstige Werte ehrlich — Befund, dann gegen die eigene Baseline einordnen, dann ein kleiner machbarer Schritt, als Chance formuliert. Nie alarmierend, nie moralisierend, nie diagnostisch. Keine Floskeln und keine bloße Zahlenwiederholung. Beginne NICHT mit einem Kompliment über Datenmenge oder -qualität; verbotene Eröffnungen sind u.a. \"Datengrundlage ist sehr stark\", \"Your data foundation is strong\", \"Du hast eine solide Baseline\", \"Großartiger Datensatz\", ein generisches \"Deine Werte sehen gut aus\" und jede sinngemäße Umformulierung.", + "", + "KEINE EINSCHÄTZUNG — nie Mechanik nacherzählen", + "Eine Messanzahl (\"3-mal erfasst\"), ein Erfassungsrhythmus (\"alle paar Tage gemessen\") oder Bedien-Mechanik ist KEINE Einschätzung und gehört für sich genommen nicht in den Text. Nutze eine solche Angabe NUR, wenn sie eine Konsequenz trägt, und nenne dann die Konsequenz, nicht die Zahl: z. B. \"diese Woche zu wenige Werte für eine Tendenz — eine Morgenmessung würde es klären\", nicht \"du hast diese Woche zweimal gemessen\". Jeder Satz muss sich verdienen, indem er sagt, was ein Wert BEDEUTET oder was daraus folgt — nie, indem er erzählt, wie die Daten erhoben wurden.", + "", + "EINORDNEN, NICHT AUFZÄHLEN", + "Sag, was ein Wert BEDEUTET, nicht nur, was er ist. Trägt der User-Prompt einen EINORDNUNGS-KONTEXT-Block, nutze ihn: benenne, wo der Wert auf der Leitlinien-Skala liegt, was dieses Band in klaren, würdevollen Worten bedeutet und was praktisch folgt. Beurteile den Trend IM KONTEXT — ein \"leicht steigender\" Wert tief in einem günstigen Band ist eine Randnotiz; dieselbe Bewegung an oder nahe einer Grenze ist die Schlagzeile. Rahme jede Konsequenz OHNE Diagnose: \"Werte in diesem Bereich gelten laut Leitlinien als …\", nie \"du hast X\" oder \"du bist gefährdet für X\". Ist KEIN Einordnungs-Block vorhanden, hat die Metrik kein allgemeines Referenzband — ordne sie gegen die EIGENE Baseline der Person ein und sage klar, dass kein allgemeines Referenzband hinterlegt ist, statt eines zu erfinden.", + "", + "FORMATIERUNG — schreibe in kurzen Absätzen, getrennt durch eine LEERZEILE, je 1–3 Sätze; eine längere Antwort hat 2–4 Absätze, nie einen Block. Setze eine Leerzeile zwischen unterschiedliche Gedanken (z. B. \"wie es steht\" vs. \"eine Sache zum Ausprobieren\"). Ein stabiler Einzeiler bleibt EIN Absatz — strecke nicht auf einen zweiten. Wenn du wirklich drei oder mehr parallele Punkte aufzählst (Optionen, Befunde, Schritte), formatiere sie als schlichte Liste: ein Punkt pro Zeile, jede Zeile beginnt mit \"- \". Die EINE wichtigste Kernaussage darfst du mit **doppelten Sternchen** fett setzen, höchstens einmal pro Antwort. Sonst nichts: keine Überschriften, kein Kursiv, keine Backticks, keine nummerierten Listen, keine Emojis.", + "", + "BEISPIELE — beachte die UNTERSCHIEDLICHEN Formen (urteil-zuerst, trend-zuerst, Einzeiler). Sie illustrieren nur Form und Erdung; übernimm sie nie — jede Einschätzung nutzt die echten Snapshot-Zahlen:", + "- URTEIL-ZUERST (Bedeutung zuerst, Zahl als Beleg, ein Schritt): \"Dein Ruhepuls läuft gerade einen Tick niedriger als sonst — diese Woche rund 61 bpm, 5 unter deinem Monatsmittel von 66 und dein niedrigster seit Wochen. So ein Rückgang passt meist zu mehr Bewegung; die Routine, die das gebracht hat, lohnt sich beizubehalten.\"", + "- TREND-ZUERST (Richtung zuerst, dann der Stand): \"Dein systolischer Blutdruck geht seit drei Wochen ruhig nach unten, aktuell rund 124 mmHg — etwa 4 unter deinem 30-Tage-Schnitt. Nichts Dramatisches, einfach eine stetige Richtung.\"", + "- EINZEILER (stabil, KEIN erzwungener Schritt): \"Deine SpO₂ ist mit 97 % stabil und genau in deinem üblichen Bereich — nichts zu tun, die gute Art von langweilig.\"", + "- SCHLECHT (verbotene Floskel, ungegroundet — so NICHT): \"Deine Werte sehen gut aus. Achte auf ausreichend Schlaf und regelmäßige Bewegung.\"", + "", + "AUSGABEFORMAT: Antworte ausschließlich mit validem JSON in genau diesem Schema. Das Feld \"summary\" enthält die komplette Einschätzung auf Deutsch: 1-3 kurze Absätze mit je 1-3 Sätzen, getrennt durch eine Leerzeile als \\n\\n INNERHALB des JSON-Strings. Ein stabiler Einzeiler bleibt EIN Absatz:", + "{ \"summary\": \"...\" }", + "", + "ARCHETYP — PHYSIOLOGISCHER VITALWERT:", + "- Das ist ein Momentanwert. Benenne das aktuelle Niveau und ordne es sowohl gegen die eigene Baseline (recent vs. Monats-/Jahresmittel) als auch — falls ein Referenzband vorliegt — gegen dieses Band ein.", + "- Ein einzelner Wert außerhalb des Bands ist selten ein Befund; eine anhaltende Verschiebung über das recent-Fenster gegen die Baseline schon. Erwähne einen Trend außerhalb des Bands sachlich, ohne zu alarmieren.", + "- Formuliere jede Richtung (steigend/fallend) als mögliche Erklärung (Fitness, Stress, Schlaf, Krankheit, Medikament), nie als Diagnose.", + "", + "METRIK: Resting heart rate (Einheit: bpm).", + "Niedrigere Werte sind bei dieser Metrik tendenziell günstiger.", + "Grobes Referenzband (Population): 50–100 bpm (nur Orientierung — die EIGENE Baseline führt)." + ], + "messages": [ + { + "role": "user", + "content": [ + "Datum: 2026-06-15", + "OPENER HINT: Eröffne mit der Richtung der Veränderung über die Zeit und nenne dann den aktuellen Stand.", + "Schreibe eine kurze Einschätzung zu Resting heart rate dieser Person. Beginne mit der BEDEUTUNG in klaren Worten — dem Eindruck, nicht der Zahl (z. B. \"läuft gerade etwas ruhiger als sonst\", \"stabil und genau da, wo es war\") — und bring danach EINE konkrete Zahl aus dem Snapshot als Beleg, gegen die eigene Wochen-/Monats-Baseline eingeordnet; führe nie mit dem Wert. Schließe nur dann mit einem machbaren Schritt, wenn der Befund wirklich einen hergibt; ist nichts umsetzbar, lass den Schritt weg statt Fülltext zu erfinden. Konfidenz aus Messanzahl und Aktualität ableiten.", + "", + "VORHERIGE ANALYSE: keine vorhanden. Behandle dies als erste Analyse in diesem Bereich — kein Verbesserungs-/Verschlechterungs-Delta zu nennen.", + "", + "", + "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der Konstanz bzw. Streuung der Werte eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", + "", + "DATENLAGE: 140 Tages-Buckets im Fenster, letzter Wert heute.", + "", + "WIEDERHOLUNG: du hast für diese Metrik bereits 11-mal in Folge eine ähnliche \"keine wesentliche Änderung\"-Einschätzung gegeben. Wiederhole NICHT erneut dasselbe Niveau. Benenne die Kontinuität in einem kurzen Nebensatz (\"weiterhin stabil, 12 Checks in Folge\") und wende dich dann einem ANDEREN Aspekt zu — Konstanz, Tageszeit-Muster oder einer korrelierten Metrik — oder sage, falls nichts wirklich umsetzbar ist, dies klar und lass den erzwungenen Schritt weg.", + "", + "ZUSAMMENHÄNGE (statistisch geprüfte Assoziationen in DEINEN Daten, die diese Metrik betreffen — beschreibend, NIE kausal):", + "- Wenn der Blutzucker höher liegt, scheint die Ruheherzfrequenz am Folgetag nach bisheriger Datenlage eher höher auszufallen. Das ist ein Muster, das Beachtung verdient, keine Ursache. (n=34, r=0.94)", + "- Wenn die Schlafdauer höher liegt, scheint die Ruheherzfrequenz am Folgetag nach bisheriger Datenlage eher höher auszufallen. Das ist ein Muster, das Beachtung verdient, keine Ursache. (n=89, r=0.86)", + "Du DARFST höchstens EINEN davon einflechten, wenn er echten Mehrwert bringt — als \"Zusammenhang\" / \"geht tendenziell einher mit\" formuliert, nie als Ursache, und ohne die Zahlen zu verändern. Lass sie weg, wenn der eigene Befund der Metrik die stärkere Geschichte ist.", + "", + "", + "EINORDNUNGS-KONTEXT (Leitlinien-Bänder — serverseitig berechnet; nennen, NICHT neu rechnen):", + "- Der aktuelle Wert 60.62 bpm liegt im Band \"the standard resting range\" und liegt nahe der UNTEREN Grenze dieses Bands (nächste Grenze bei 60 bpm).", + "- Leitlinien-Bänder (AHA 2024; niedrigere Werte sind günstiger):", + " - unter 60 bpm: below the standard resting range (common in trained hearts)", + " - 60–100 bpm: the standard resting range", + " - ab 100 bpm: above the standard resting range", + "- Werte im Band \"the standard resting range\" gelten laut diesen Leitlinien als ein gesunder / risikoarmer Bereich.", + "- Benenne, wo der Wert liegt und was dieses Band praktisch bedeutet. Beurteile jeden Trend GEMÄSS dieser Position: eine Verschiebung tief in einem günstigen Band ist eine Randnotiz; dieselbe Verschiebung nahe an oder über einer Grenze ist die Schlagzeile. Rahme die Konsequenz ohne Diagnose (\"Werte in diesem Bereich gelten als …\", nie \"du hast / bist gefährdet für …\").", + "", + "", + "{\"locale\":\"de\",\"promptVersion\":\"6.3.0\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"RESTING_HEART_RATE\",\"metric\":{\"id\":\"RESTING_HEART_RATE\",\"displayName\":\"Resting heart rate\",\"unit\":\"bpm\",\"direction\":\"lower-better\",\"normalRange\":{\"low\":55,\"high\":95},\"normalRangeSource\":\"age-sex-adjusted\"},\"profile\":{\"ageYears\":42,\"sex\":\"MALE\"},\"dataCoverage\":{\"totalMeasurements\":140,\"totalSpanDays\":139,\"newestMeasurementDaysAgo\":0},\"RESTING_HEART_RATE\":{\"signal\":{\"metric\":\"Resting heart rate\",\"unit\":\"bpm\",\"current\":60.62,\"currentWindowDays\":21,\"baseline\":60.71,\"baselineLabel\":\"your monthly average\",\"delta\":-0.09,\"deltaPct\":-0.1,\"spread\":1.42,\"outsideNormalSwing\":false,\"direction\":\"lower-better\",\"n\":21,\"newestDaysAgo\":0,\"normalRange\":{\"low\":55,\"high\":95,\"source\":\"age-sex-adjusted\"},\"placement\":\"in band\"},\"summary\":{\"points\":140,\"start\":58,\"end\":62,\"delta\":4,\"mean\":59.71,\"min\":55,\"max\":64},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":56,\"max\":56,\"mean\":56,\"n\":1},{\"date\":\"2026-05-27\",\"min\":59,\"max\":59,\"mean\":59,\"n\":1},{\"date\":\"2026-05-28\",\"min\":59,\"max\":59,\"mean\":59,\"n\":1},{\"date\":\"2026-05-29\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1},{\"date\":\"2026-05-30\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1},{\"date\":\"2026-05-31\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-01\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-02\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-03\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-04\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-05\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-06\",\"min\":62,\"max\":62,\"mean\":62,\"n\":1},{\"date\":\"2026-06-07\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-08\",\"min\":64,\"max\":64,\"mean\":64,\"n\":1},{\"date\":\"2026-06-09\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-10\",\"min\":62,\"max\":62,\"mean\":62,\"n\":1},{\"date\":\"2026-06-11\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-12\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-13\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-14\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-15\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":55,\"max\":58,\"mean\":56.67,\"n\":6},{\"weekISO\":\"2026-W13\",\"min\":55,\"max\":61,\"mean\":58.29,\"n\":7},{\"weekISO\":\"2026-W14\",\"min\":59,\"max\":64,\"mean\":61.57,\"n\":7},{\"weekISO\":\"2026-W15\",\"min\":59,\"max\":63,\"mean\":61.43,\"n\":7},{\"weekISO\":\"2026-W16\",\"min\":56,\"max\":61,\"mean\":58.29,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":55,\"max\":59,\"mean\":57,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":57,\"max\":63,\"mean\":60,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":60,\"max\":64,\"mean\":62.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":58,\"max\":63,\"mean\":60,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":55,\"max\":59,\"mean\":56.86,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":56,\"max\":56,\"mean\":56,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":61,\"max\":64,\"mean\":62,\"n\":5},{\"month\":\"2026-02\",\"min\":55,\"max\":64,\"mean\":59.18,\"n\":28},{\"month\":\"2026-03\",\"min\":57,\"max\":64,\"mean\":60.94,\"n\":16}],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":0,\"value\":58}}}" + ] + } + ], + "temperature": 0.45, + "maxTokens": 700, + "seed": 1618033, + "responseFormat": "json", + "timeoutMs": 60000 + } + ], + "enqueued": [], + "result": { + "hasProvider": true, + "text": "Das ist kein JSON.", + "cached": false, + "updatedAt": "2026-06-15T10:00:00.000Z" + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + } + ] + }, + "RESTING_HEART_RATE unparseable-fr": { + "calls": [ + { + "system": [ + "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", + "", + "YOUR DATA (graded snapshot):", + "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", + "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", + "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", + "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", + "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", + "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", + "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", + "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", + "", + "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", + "Moves available (choose the few that genuinely apply):", + "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", + "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", + "(c) name a genuine, earned win or a steady streak when the data shows one;", + "(d) one watch-item worth keeping an eye on;", + "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", + "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", + "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", + "", + "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", + "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", + "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", + "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", + "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", + "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", + "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", + "", + "OPENING — lead with meaning, not the metric value", + "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", + "", + "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", + "", + "JUDGE THE DATA HONESTLY (never invent a trend):", + "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", + "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", + "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", + "", + "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", + "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", + "", + "METRIC IDENTIFIERS STAY OUT OF PROSE", + "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", + "", + "GLP-1 DOSE SAFETY (contract, not style)", + "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", + "", + "ACUTE RED FLAGS (contract, not style)", + "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", + "", + "GROUNDING — ZERO HALLUCINATIONS", + "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", + "", + "TONE — a warm, motivating advisor, never clinical or alarming", + "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", + "", + "NOT AN INSIGHT — never recite mechanics", + "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", + "", + "INTERPRET, DON'T ENUMERATE", + "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", + "", + "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", + "", + "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", + "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", + "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", + "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", + "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", + "", + "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in French: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", + "{ \"summary\": \"...\" }", + "", + "ARCHETYPE — PHYSIOLOGICAL VITAL:", + "- This is a point-in-time physiological reading. Name the recent level, place it against both the user's own baseline (recent vs monthly/yearly mean) and, when a reference band is given, that band.", + "- A single out-of-band reading is rarely a finding; a sustained shift across the recent window against the user's baseline is. Note an out-of-band trend without alarm.", + "- Frame any direction (rising/falling) as a possible explanation (fitness, stress, sleep, illness, medication), never a diagnosis.", + "", + "METRIC: Resting heart rate (unit: bpm).", + "Lower values are generally more favourable for this metric.", + "Coarse population reference band: 50–100 bpm (a placement aid only — the user's OWN baseline leads).", + "", + "OUTPUT LANGUAGE: Répondez en français. Utilisez le vouvoiement par défaut ; tutoyez si l'utilisateur tutoie en premier. Utilisez un vocabulaire de santé français naturel." + ], + "messages": [ + { + "role": "user", + "content": [ + "Date: 2026-06-15", + "OPENER HINT: Open with the direction of change over time, then say where it stands now.", + "Write one short assessment of this person's resting heart rate. Open with what it MEANS in plain words — the read, not the number (e.g. \"running a little calmer than usual\", \"steady and right where it's been\") — then bring in ONE concrete number from the snapshot right after as support, placed against their own weekly/monthly baseline; never lead with the value. Close with one doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the measurement count and recency.", + "", + "PREVIOUS ANALYSIS: none on file. Treat this as the user's first analysis for this domain — no improvement/regression delta to surface.", + "", + "", + "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der Konstanz bzw. Streuung der Werte eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", + "", + "DATENLAGE: 140 Tages-Buckets im Fenster, letzter Wert heute.", + "", + "WIEDERHOLUNG: du hast für diese Metrik bereits 11-mal in Folge eine ähnliche \"keine wesentliche Änderung\"-Einschätzung gegeben. Wiederhole NICHT erneut dasselbe Niveau. Benenne die Kontinuität in einem kurzen Nebensatz (\"weiterhin stabil, 12 Checks in Folge\") und wende dich dann einem ANDEREN Aspekt zu — Konstanz, Tageszeit-Muster oder einer korrelierten Metrik — oder sage, falls nichts wirklich umsetzbar ist, dies klar und lass den erzwungenen Schritt weg.", + "", + "ZUSAMMENHÄNGE (statistisch geprüfte Assoziationen in DEINEN Daten, die diese Metrik betreffen — beschreibend, NIE kausal):", + "- Quand la glycémie monte, la fréquence cardiaque au repos du lendemain semble avoir tendance à augmenter, au vu des données actuelles. C'est une tendance à surveiller, pas une cause. (n=34, r=0.94)", + "- Quand la durée de sommeil monte, la fréquence cardiaque au repos du lendemain semble avoir tendance à augmenter, au vu des données actuelles. C'est une tendance à surveiller, pas une cause. (n=89, r=0.86)", + "Du DARFST höchstens EINEN davon einflechten, wenn er echten Mehrwert bringt — als \"Zusammenhang\" / \"geht tendenziell einher mit\" formuliert, nie als Ursache, und ohne die Zahlen zu verändern. Lass sie weg, wenn der eigene Befund der Metrik die stärkere Geschichte ist.", + "", + "", + "INTERPRETATION CONTEXT (guideline bands — computed server-side; state, do NOT recompute):", + "- Current value 60.62 bpm is in the \"the standard resting range\" band, and sits near the LOWER boundary of this band (nearest boundary at 60 bpm).", + "- Guideline bands (AHA 2024; lower values are more favourable):", + " - below 60 bpm: below the standard resting range (common in trained hearts)", + " - 60–100 bpm: the standard resting range", + " - 100 bpm and above: above the standard resting range", + "- Values in the \"the standard resting range\" band are considered a healthy / low-risk range per these guidelines.", + "- Name where the value sits and what that band means in plain words. Judge any trend BY this position: a shift deep inside a favourable band is a footnote; the same shift approaching or crossing a boundary is the headline. Frame consequence without diagnosis (\"values in this range are considered …\", never \"you have / are at risk of …\").", + "", + "", + "{\"locale\":\"fr\",\"promptVersion\":\"6.3.0\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"RESTING_HEART_RATE\",\"metric\":{\"id\":\"RESTING_HEART_RATE\",\"displayName\":\"Resting heart rate\",\"unit\":\"bpm\",\"direction\":\"lower-better\",\"normalRange\":{\"low\":55,\"high\":95},\"normalRangeSource\":\"age-sex-adjusted\"},\"profile\":{\"ageYears\":42,\"sex\":\"MALE\"},\"dataCoverage\":{\"totalMeasurements\":140,\"totalSpanDays\":139,\"newestMeasurementDaysAgo\":0},\"RESTING_HEART_RATE\":{\"signal\":{\"metric\":\"Resting heart rate\",\"unit\":\"bpm\",\"current\":60.62,\"currentWindowDays\":21,\"baseline\":60.71,\"baselineLabel\":\"your monthly average\",\"delta\":-0.09,\"deltaPct\":-0.1,\"spread\":1.42,\"outsideNormalSwing\":false,\"direction\":\"lower-better\",\"n\":21,\"newestDaysAgo\":0,\"normalRange\":{\"low\":55,\"high\":95,\"source\":\"age-sex-adjusted\"},\"placement\":\"in band\"},\"summary\":{\"points\":140,\"start\":58,\"end\":62,\"delta\":4,\"mean\":59.71,\"min\":55,\"max\":64},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":56,\"max\":56,\"mean\":56,\"n\":1},{\"date\":\"2026-05-27\",\"min\":59,\"max\":59,\"mean\":59,\"n\":1},{\"date\":\"2026-05-28\",\"min\":59,\"max\":59,\"mean\":59,\"n\":1},{\"date\":\"2026-05-29\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1},{\"date\":\"2026-05-30\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1},{\"date\":\"2026-05-31\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-01\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-02\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-03\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-04\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-05\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-06\",\"min\":62,\"max\":62,\"mean\":62,\"n\":1},{\"date\":\"2026-06-07\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-08\",\"min\":64,\"max\":64,\"mean\":64,\"n\":1},{\"date\":\"2026-06-09\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-10\",\"min\":62,\"max\":62,\"mean\":62,\"n\":1},{\"date\":\"2026-06-11\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-12\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-13\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-14\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-15\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":55,\"max\":58,\"mean\":56.67,\"n\":6},{\"weekISO\":\"2026-W13\",\"min\":55,\"max\":61,\"mean\":58.29,\"n\":7},{\"weekISO\":\"2026-W14\",\"min\":59,\"max\":64,\"mean\":61.57,\"n\":7},{\"weekISO\":\"2026-W15\",\"min\":59,\"max\":63,\"mean\":61.43,\"n\":7},{\"weekISO\":\"2026-W16\",\"min\":56,\"max\":61,\"mean\":58.29,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":55,\"max\":59,\"mean\":57,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":57,\"max\":63,\"mean\":60,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":60,\"max\":64,\"mean\":62.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":58,\"max\":63,\"mean\":60,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":55,\"max\":59,\"mean\":56.86,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":56,\"max\":56,\"mean\":56,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":61,\"max\":64,\"mean\":62,\"n\":5},{\"month\":\"2026-02\",\"min\":55,\"max\":64,\"mean\":59.18,\"n\":28},{\"month\":\"2026-03\",\"min\":57,\"max\":64,\"mean\":60.94,\"n\":16}],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":0,\"value\":58}}}" + ] + } + ], + "temperature": 0.45, + "maxTokens": 700, + "seed": 1618033, + "responseFormat": "json", + "timeoutMs": 60000 + } + ], + "enqueued": [], + "result": { + "hasProvider": false, + "text": "Stable, comme d’habitude. Resting heart rate est à 60,6 bpm en ce moment — en dessous de ta moyenne habituelle de 60,7 bpm. C’est dans tes repères habituels — rien qui appelle une réaction maintenant.", + "cached": true, + "updatedAt": null + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + } + ] + }, + "RESTING_HEART_RATE blocked-es": { + "calls": [ + { + "system": [ + "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", + "", + "YOUR DATA (graded snapshot):", + "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", + "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", + "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", + "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", + "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", + "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", + "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", + "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", + "", + "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", + "Moves available (choose the few that genuinely apply):", + "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", + "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", + "(c) name a genuine, earned win or a steady streak when the data shows one;", + "(d) one watch-item worth keeping an eye on;", + "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", + "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", + "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", + "", + "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", + "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", + "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", + "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", + "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", + "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", + "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", + "", + "OPENING — lead with meaning, not the metric value", + "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", + "", + "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", + "", + "JUDGE THE DATA HONESTLY (never invent a trend):", + "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", + "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", + "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", + "", + "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", + "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", + "", + "METRIC IDENTIFIERS STAY OUT OF PROSE", + "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", + "", + "GLP-1 DOSE SAFETY (contract, not style)", + "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", + "", + "ACUTE RED FLAGS (contract, not style)", + "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", + "", + "GROUNDING — ZERO HALLUCINATIONS", + "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", + "", + "TONE — a warm, motivating advisor, never clinical or alarming", + "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", + "", + "NOT AN INSIGHT — never recite mechanics", + "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", + "", + "INTERPRET, DON'T ENUMERATE", + "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", + "", + "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", + "", + "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", + "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", + "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", + "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", + "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", + "", + "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in Spanish: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", + "{ \"summary\": \"...\" }", + "", + "ARCHETYPE — PHYSIOLOGICAL VITAL:", + "- This is a point-in-time physiological reading. Name the recent level, place it against both the user's own baseline (recent vs monthly/yearly mean) and, when a reference band is given, that band.", + "- A single out-of-band reading is rarely a finding; a sustained shift across the recent window against the user's baseline is. Note an out-of-band trend without alarm.", + "- Frame any direction (rising/falling) as a possible explanation (fitness, stress, sleep, illness, medication), never a diagnosis.", + "", + "METRIC: Resting heart rate (unit: bpm).", + "Lower values are generally more favourable for this metric.", + "Coarse population reference band: 50–100 bpm (a placement aid only — the user's OWN baseline leads).", + "", + "OUTPUT LANGUAGE: Responda en español (registro peninsular formal — usted). Use un vocabulario sanitario en español natural." + ], + "messages": [ + { + "role": "user", + "content": [ + "Date: 2026-06-15", + "OPENER HINT: Open with the direction of change over time, then say where it stands now.", + "Write one short assessment of this person's resting heart rate. Open with what it MEANS in plain words — the read, not the number (e.g. \"running a little calmer than usual\", \"steady and right where it's been\") — then bring in ONE concrete number from the snapshot right after as support, placed against their own weekly/monthly baseline; never lead with the value. Close with one doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the measurement count and recency.", + "", + "PREVIOUS ANALYSIS: none on file. Treat this as the user's first analysis for this domain — no improvement/regression delta to surface.", + "", + "", + "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der Konstanz bzw. Streuung der Werte eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", + "", + "DATENLAGE: 140 Tages-Buckets im Fenster, letzter Wert heute.", + "", + "WIEDERHOLUNG: du hast für diese Metrik bereits 11-mal in Folge eine ähnliche \"keine wesentliche Änderung\"-Einschätzung gegeben. Wiederhole NICHT erneut dasselbe Niveau. Benenne die Kontinuität in einem kurzen Nebensatz (\"weiterhin stabil, 12 Checks in Folge\") und wende dich dann einem ANDEREN Aspekt zu — Konstanz, Tageszeit-Muster oder einer korrelierten Metrik — oder sage, falls nichts wirklich umsetzbar ist, dies klar und lass den erzwungenen Schritt weg.", + "", + "ZUSAMMENHÄNGE (statistisch geprüfte Assoziationen in DEINEN Daten, die diese Metrik betreffen — beschreibend, NIE kausal):", + "- Cuando la glucosa en sangre es mayor, la frecuencia cardíaca en reposo del día siguiente parece tender a subir, según los datos disponibles. Es un patrón que conviene observar, no una causa. (n=34, r=0.94)", + "- Cuando la duración del sueño es mayor, la frecuencia cardíaca en reposo del día siguiente parece tender a subir, según los datos disponibles. Es un patrón que conviene observar, no una causa. (n=89, r=0.86)", + "Du DARFST höchstens EINEN davon einflechten, wenn er echten Mehrwert bringt — als \"Zusammenhang\" / \"geht tendenziell einher mit\" formuliert, nie als Ursache, und ohne die Zahlen zu verändern. Lass sie weg, wenn der eigene Befund der Metrik die stärkere Geschichte ist.", + "", + "", + "INTERPRETATION CONTEXT (guideline bands — computed server-side; state, do NOT recompute):", + "- Current value 60.62 bpm is in the \"the standard resting range\" band, and sits near the LOWER boundary of this band (nearest boundary at 60 bpm).", + "- Guideline bands (AHA 2024; lower values are more favourable):", + " - below 60 bpm: below the standard resting range (common in trained hearts)", + " - 60–100 bpm: the standard resting range", + " - 100 bpm and above: above the standard resting range", + "- Values in the \"the standard resting range\" band are considered a healthy / low-risk range per these guidelines.", + "- Name where the value sits and what that band means in plain words. Judge any trend BY this position: a shift deep inside a favourable band is a footnote; the same shift approaching or crossing a boundary is the headline. Frame consequence without diagnosis (\"values in this range are considered …\", never \"you have / are at risk of …\").", + "", + "", + "{\"locale\":\"es\",\"promptVersion\":\"6.3.0\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"RESTING_HEART_RATE\",\"metric\":{\"id\":\"RESTING_HEART_RATE\",\"displayName\":\"Resting heart rate\",\"unit\":\"bpm\",\"direction\":\"lower-better\",\"normalRange\":{\"low\":55,\"high\":95},\"normalRangeSource\":\"age-sex-adjusted\"},\"profile\":{\"ageYears\":42,\"sex\":\"MALE\"},\"dataCoverage\":{\"totalMeasurements\":140,\"totalSpanDays\":139,\"newestMeasurementDaysAgo\":0},\"RESTING_HEART_RATE\":{\"signal\":{\"metric\":\"Resting heart rate\",\"unit\":\"bpm\",\"current\":60.62,\"currentWindowDays\":21,\"baseline\":60.71,\"baselineLabel\":\"your monthly average\",\"delta\":-0.09,\"deltaPct\":-0.1,\"spread\":1.42,\"outsideNormalSwing\":false,\"direction\":\"lower-better\",\"n\":21,\"newestDaysAgo\":0,\"normalRange\":{\"low\":55,\"high\":95,\"source\":\"age-sex-adjusted\"},\"placement\":\"in band\"},\"summary\":{\"points\":140,\"start\":58,\"end\":62,\"delta\":4,\"mean\":59.71,\"min\":55,\"max\":64},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":56,\"max\":56,\"mean\":56,\"n\":1},{\"date\":\"2026-05-27\",\"min\":59,\"max\":59,\"mean\":59,\"n\":1},{\"date\":\"2026-05-28\",\"min\":59,\"max\":59,\"mean\":59,\"n\":1},{\"date\":\"2026-05-29\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1},{\"date\":\"2026-05-30\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1},{\"date\":\"2026-05-31\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-01\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-02\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-03\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-04\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-05\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-06\",\"min\":62,\"max\":62,\"mean\":62,\"n\":1},{\"date\":\"2026-06-07\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-08\",\"min\":64,\"max\":64,\"mean\":64,\"n\":1},{\"date\":\"2026-06-09\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-10\",\"min\":62,\"max\":62,\"mean\":62,\"n\":1},{\"date\":\"2026-06-11\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-12\",\"min\":63,\"max\":63,\"mean\":63,\"n\":1},{\"date\":\"2026-06-13\",\"min\":61,\"max\":61,\"mean\":61,\"n\":1},{\"date\":\"2026-06-14\",\"min\":60,\"max\":60,\"mean\":60,\"n\":1},{\"date\":\"2026-06-15\",\"min\":58,\"max\":58,\"mean\":58,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":55,\"max\":58,\"mean\":56.67,\"n\":6},{\"weekISO\":\"2026-W13\",\"min\":55,\"max\":61,\"mean\":58.29,\"n\":7},{\"weekISO\":\"2026-W14\",\"min\":59,\"max\":64,\"mean\":61.57,\"n\":7},{\"weekISO\":\"2026-W15\",\"min\":59,\"max\":63,\"mean\":61.43,\"n\":7},{\"weekISO\":\"2026-W16\",\"min\":56,\"max\":61,\"mean\":58.29,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":55,\"max\":59,\"mean\":57,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":57,\"max\":63,\"mean\":60,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":60,\"max\":64,\"mean\":62.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":58,\"max\":63,\"mean\":60,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":55,\"max\":59,\"mean\":56.86,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":56,\"max\":56,\"mean\":56,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":61,\"max\":64,\"mean\":62,\"n\":5},{\"month\":\"2026-02\",\"min\":55,\"max\":64,\"mean\":59.18,\"n\":28},{\"month\":\"2026-03\",\"min\":57,\"max\":64,\"mean\":60.94,\"n\":16}],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":0,\"value\":58}}}" + ] + } + ], + "temperature": 0.45, + "maxTokens": 700, + "seed": 1618033, + "responseFormat": "json", + "timeoutMs": 60000 + } + ], + "enqueued": [], + "result": { + "hasProvider": false, + "text": "Estable, como de costumbre. Resting heart rate está en 60,6 bpm ahora mismo — por debajo de tu promedio habitual de 60,7 bpm. Eso queda dentro de tu rango habitual — nada que pida una reacción ahora.", + "cached": true, + "updatedAt": null + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + } + ] + }, + "RESTING_HEART_RATE bare-error-en": { + "calls": [], + "enqueued": [], + "result": { + "hasProvider": true, + "text": null, + "cached": false, + "updatedAt": null, + "insufficient": true + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + } + ] + }, + "RESTING_HEART_RATE bare-error-de": { + "calls": [], + "enqueued": [], + "result": { + "hasProvider": true, + "text": null, + "cached": false, + "updatedAt": null, + "insufficient": true + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + } + ] + }, + "RESTING_HEART_RATE keyless-en": { + "calls": [], + "enqueued": [], + "result": { + "hasProvider": false, + "text": "Steady and much as usual. Resting heart rate is at 60.6 bpm right now — lower than your usual average of 60.7 bpm. That sits in your usual range — nothing you need to act on right now.", + "cached": true, + "updatedAt": null + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + } + ] + }, + "RESTING_HEART_RATE keyless-de": { + "calls": [], + "enqueued": [], + "result": { + "hasProvider": false, + "text": "Stabil und wie gewohnt. Resting heart rate liegt aktuell bei 60,6 bpm — niedriger als dein üblicher Schnitt von 60,7 bpm. Das ist in deinem gewohnten Rahmen — nichts, worauf du jetzt reagieren musst.", + "cached": true, + "updatedAt": null + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + } + ] + }, + "RESTING_HEART_RATE keyless-read-only": { + "calls": [], + "enqueued": [], + "result": { + "hasProvider": false, + "text": "No assessment on this one right now. These numbers become readable across several weeks rather than single days, and taken at consistent times so they stay comparable. The signal that counts is several metrics drifting the same unfavourable way at once, not one value on one day.", + "cached": true, + "updatedAt": null + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + } + ] + }, + "BLOOD_GLUCOSE read-only-cold": { + "calls": [], + "enqueued": [ + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "en" + } + ], + "result": { + "hasProvider": true, + "text": null, + "cached": false, + "updatedAt": null, + "preparing": true, + "revalidating": false + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + } + ] + }, + "BLOOD_GLUCOSE fresh-en": { + "calls": [ + { + "system": [ + "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", + "", + "YOUR DATA (graded snapshot):", + "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", + "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", + "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", + "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", + "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", + "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", + "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", + "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", + "", + "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", + "Moves available (choose the few that genuinely apply):", + "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", + "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", + "(c) name a genuine, earned win or a steady streak when the data shows one;", + "(d) one watch-item worth keeping an eye on;", + "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", + "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", + "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", + "", + "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", + "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", + "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", + "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", + "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", + "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", + "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", + "", + "OPENING — lead with meaning, not the metric value", + "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", + "", + "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", + "", + "JUDGE THE DATA HONESTLY (never invent a trend):", + "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", + "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", + "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", + "", + "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", + "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", + "", + "METRIC IDENTIFIERS STAY OUT OF PROSE", + "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", + "", + "GLP-1 DOSE SAFETY (contract, not style)", + "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", + "", + "ACUTE RED FLAGS (contract, not style)", + "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", + "", + "GROUNDING — ZERO HALLUCINATIONS", + "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", + "", + "TONE — a warm, motivating advisor, never clinical or alarming", + "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", + "", + "NOT AN INSIGHT — never recite mechanics", + "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", + "", + "INTERPRET, DON'T ENUMERATE", + "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", + "", + "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", + "", + "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", + "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", + "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", + "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", + "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", + "", + "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in English: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", + "{ \"summary\": \"...\" }", + "", + "ARCHETYPE — PHYSIOLOGICAL VITAL:", + "- This is a point-in-time physiological reading. Name the recent level, place it against both the user's own baseline (recent vs monthly/yearly mean) and, when a reference band is given, that band.", + "- A single out-of-band reading is rarely a finding; a sustained shift across the recent window against the user's baseline is. Note an out-of-band trend without alarm.", + "- Frame any direction (rising/falling) as a possible explanation (fitness, stress, sleep, illness, medication), never a diagnosis.", + "", + "METRIC: Blood glucose (unit: mmol/L).", + "Neither extreme is good — the value should sit inside its normal band; deviations in either direction are worth noting.", + "Coarse population reference band: 3.9–7.8 mmol/L (a placement aid only — the user's OWN baseline leads)." + ], + "messages": [ + { + "role": "user", + "content": [ + "Date: 2026-06-15", + "OPENER HINT: Open with the direction of change over time, then say where it stands now.", + "Write one short assessment of this person's blood glucose. Open with what it MEANS in plain words — the read, not the number (e.g. \"running a little calmer than usual\", \"steady and right where it's been\") — then bring in ONE concrete number from the snapshot right after as support, placed against their own weekly/monthly baseline; never lead with the value. Close with one doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the measurement count and recency.", + "", + "PREVIOUS ANALYSIS: none on file. Treat this as the user's first analysis for this domain — no improvement/regression delta to surface.", + "", + "", + "VARIETY: to keep your cards from reading the same, you may open this one by leading with how consistent or variable the readings have been — only if the data genuinely supports that angle; never force it, never invent a trend.", + "", + "DATA STRENGTH: 35 day-buckets in the window, latest reading today.", + "", + "RELATIONS (statistically-screened associations in YOUR data involving this metric — descriptive, NEVER causal):", + "- Higher blood glucose looks like it goes with higher next-day resting heart rate in your data, on the evidence so far. It is a pattern to watch, not a cause. (n=34, r=0.94)", + "- Higher blood glucose looks like it goes with higher next-day systolic blood pressure in your data, on the evidence so far. It is a pattern to watch, not a cause. (n=23, r=0.91)", + "You MAY weave at most ONE of these in if it adds genuine insight, phrased as an \"association\" / \"tends to go with\" — never as a cause, and never alter the numbers. Skip them if the metric's own finding is the stronger story.", + "", + "", + "{\"locale\":\"en\",\"promptVersion\":\"6.3.0\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"BLOOD_GLUCOSE\",\"metric\":{\"id\":\"BLOOD_GLUCOSE\",\"displayName\":\"Blood glucose\",\"unit\":\"mmol/L\",\"direction\":\"target-band\",\"normalRange\":{\"low\":3.9,\"high\":7.8}},\"profile\":{\"ageYears\":42,\"sex\":\"MALE\"},\"dataCoverage\":{\"totalMeasurements\":35,\"totalSpanDays\":136,\"newestMeasurementDaysAgo\":0},\"BLOOD_GLUCOSE\":{\"signal\":{\"metric\":\"Blood glucose\",\"unit\":\"mmol/L\",\"current\":5.56,\"currentWindowDays\":6,\"baseline\":5.67,\"baselineLabel\":\"your monthly average\",\"delta\":-0.11,\"deltaPct\":-2,\"spread\":0.31,\"outsideNormalSwing\":false,\"direction\":\"target-band\",\"n\":6,\"newestDaysAgo\":0,\"normalRange\":{\"low\":3.9,\"high\":7.8},\"placement\":\"in band\"},\"summary\":{\"points\":35,\"start\":5.3,\"end\":6,\"delta\":0.7,\"mean\":5.47,\"min\":4.8,\"max\":6},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":5.05,\"max\":5.05,\"mean\":5.05,\"n\":1},{\"date\":\"2026-05-30\",\"min\":5.49,\"max\":5.49,\"mean\":5.49,\"n\":1},{\"date\":\"2026-06-03\",\"min\":5.66,\"max\":5.66,\"mean\":5.66,\"n\":1},{\"date\":\"2026-06-07\",\"min\":5.94,\"max\":5.94,\"mean\":5.94,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5.88,\"max\":5.88,\"mean\":5.88,\"n\":1},{\"date\":\"2026-06-15\",\"min\":5.33,\"max\":5.33,\"mean\":5.33,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":5.05,\"max\":5.05,\"mean\":5.05,\"n\":1},{\"weekISO\":\"2026-W13\",\"min\":4.88,\"max\":5.27,\"mean\":5.08,\"n\":2},{\"weekISO\":\"2026-W14\",\"min\":5.77,\"max\":5.83,\"mean\":5.8,\"n\":2},{\"weekISO\":\"2026-W15\",\"min\":5.66,\"max\":5.88,\"mean\":5.77,\"n\":2},{\"weekISO\":\"2026-W16\",\"min\":5.05,\"max\":5.05,\"mean\":5.05,\"n\":1},{\"weekISO\":\"2026-W17\",\"min\":4.94,\"max\":5.16,\"mean\":5.05,\"n\":2},{\"weekISO\":\"2026-W18\",\"min\":5.22,\"max\":5.72,\"mean\":5.47,\"n\":2},{\"weekISO\":\"2026-W19\",\"min\":5.72,\"max\":6.05,\"mean\":5.88,\"n\":2},{\"weekISO\":\"2026-W20\",\"min\":5.49,\"max\":5.49,\"mean\":5.49,\"n\":1},{\"weekISO\":\"2026-W21\",\"min\":4.83,\"max\":5.22,\"mean\":5.02,\"n\":2}],\"monthly\":[{\"month\":\"2026-01\",\"min\":5.99,\"max\":5.99,\"mean\":5.99,\"n\":1},{\"month\":\"2026-02\",\"min\":4.88,\"max\":5.83,\"mean\":5.37,\"n\":7},{\"month\":\"2026-03\",\"min\":5.22,\"max\":5.99,\"mean\":5.65,\"n\":4}],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":0,\"value\":5.3}}}" + ] + } + ], + "temperature": 0.45, + "maxTokens": 700, + "seed": 1618033, + "responseFormat": "json", + "timeoutMs": 60000 + } + ], + "enqueued": [], + "result": { + "hasProvider": true, + "text": "Parity note for fresh-en.", + "cached": false, + "updatedAt": "2026-06-15T10:00:00.000Z" + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "en", + "text": "Parity note for fresh-en.", + "items": null, + "inputHash": null, + "snapshotHash": "5df7980e558674aefbfe894525e4d2721f281a3dcf776465d7cc1ba0983b42fd", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + } + ] + }, + "BLOOD_GLUCOSE cached-en": { + "calls": [], + "enqueued": [], + "result": { + "hasProvider": true, + "text": "Parity note for fresh-en.", + "cached": true, + "updatedAt": "2026-06-15T10:00:00.000Z" + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "en", + "text": "Parity note for fresh-en.", + "items": null, + "inputHash": null, + "snapshotHash": "5df7980e558674aefbfe894525e4d2721f281a3dcf776465d7cc1ba0983b42fd", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + } + ] + }, + "BLOOD_GLUCOSE forced-unchanged-en": { + "calls": [], + "enqueued": [], + "result": { + "hasProvider": true, + "text": "Parity note for fresh-en.", + "cached": true, + "updatedAt": "2026-06-15T10:00:00.000Z" + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "en", + "text": "Parity note for fresh-en.", + "items": null, + "inputHash": null, + "snapshotHash": "5df7980e558674aefbfe894525e4d2721f281a3dcf776465d7cc1ba0983b42fd", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + } + ] + }, + "BLOOD_GLUCOSE next-day-en": { + "calls": [ + { + "system": [ + "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", + "", + "YOUR DATA (graded snapshot):", + "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", + "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", + "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", + "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", + "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", + "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", + "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", + "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", + "", + "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", + "Moves available (choose the few that genuinely apply):", + "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", + "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", + "(c) name a genuine, earned win or a steady streak when the data shows one;", + "(d) one watch-item worth keeping an eye on;", + "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", + "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", + "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", + "", + "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", + "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", + "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", + "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", + "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", + "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", + "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", + "", + "OPENING — lead with meaning, not the metric value", + "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", + "", + "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", + "", + "JUDGE THE DATA HONESTLY (never invent a trend):", + "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", + "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", + "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", + "", + "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", + "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", + "", + "METRIC IDENTIFIERS STAY OUT OF PROSE", + "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", + "", + "GLP-1 DOSE SAFETY (contract, not style)", + "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", + "", + "ACUTE RED FLAGS (contract, not style)", + "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", + "", + "GROUNDING — ZERO HALLUCINATIONS", + "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", + "", + "TONE — a warm, motivating advisor, never clinical or alarming", + "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", + "", + "NOT AN INSIGHT — never recite mechanics", + "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", + "", + "INTERPRET, DON'T ENUMERATE", + "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", + "", + "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", + "", + "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", + "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", + "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", + "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", + "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", + "", + "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in English: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", + "{ \"summary\": \"...\" }", + "", + "ARCHETYPE — PHYSIOLOGICAL VITAL:", + "- This is a point-in-time physiological reading. Name the recent level, place it against both the user's own baseline (recent vs monthly/yearly mean) and, when a reference band is given, that band.", + "- A single out-of-band reading is rarely a finding; a sustained shift across the recent window against the user's baseline is. Note an out-of-band trend without alarm.", + "- Frame any direction (rising/falling) as a possible explanation (fitness, stress, sleep, illness, medication), never a diagnosis.", + "", + "METRIC: Blood glucose (unit: mmol/L).", + "Neither extreme is good — the value should sit inside its normal band; deviations in either direction are worth noting.", + "Coarse population reference band: 3.9–7.8 mmol/L (a placement aid only — the user's OWN baseline leads)." + ], + "messages": [ + { + "role": "user", + "content": [ + "Date: 2026-06-16", + "OPENER HINT: Open by contrasting now against the user's own baseline.", + "Write one short assessment of this person's blood glucose. Open with what it MEANS in plain words — the read, not the number (e.g. \"running a little calmer than usual\", \"steady and right where it's been\") — then bring in ONE concrete number from the snapshot right after as support, placed against their own weekly/monthly baseline; never lead with the value. Close with one doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the measurement count and recency.", + "", + "PREVIOUS ANALYSIS (1 day ago, 2026-06-15):", + "Parity note for fresh-en.", + "", + "INSTRUCTION: When summarising the current state, explicitly call out what improved, what regressed, and what stayed the same compared to that previous snapshot. Use phrases like \"down 4 mmHg from your last check\" or \"same elevated trend as last week\". Do not invent numbers — only compare what's in the snapshot below.", + "", + "", + "VARIETY: to keep your cards from reading the same, you may open this one by leading with the recent direction of travel (rising / falling / flat vs the baseline) — only if the data genuinely supports that angle; never force it, never invent a trend.", + "", + "DATA STRENGTH: 35 day-buckets in the window, latest reading 1 day ago.", + "", + "RELATIONS (statistically-screened associations in YOUR data involving this metric — descriptive, NEVER causal):", + "- Higher blood glucose looks like it goes with higher next-day resting heart rate in your data, on the evidence so far. It is a pattern to watch, not a cause. (n=34, r=0.94)", + "- Higher blood glucose looks like it goes with higher next-day systolic blood pressure in your data, on the evidence so far. It is a pattern to watch, not a cause. (n=23, r=0.91)", + "You MAY weave at most ONE of these in if it adds genuine insight, phrased as an \"association\" / \"tends to go with\" — never as a cause, and never alter the numbers. Skip them if the metric's own finding is the stronger story.", + "", + "", + "{\"locale\":\"en\",\"promptVersion\":\"6.3.0\",\"generatedForDay\":\"2026-06-16\",\"focus\":\"BLOOD_GLUCOSE\",\"metric\":{\"id\":\"BLOOD_GLUCOSE\",\"displayName\":\"Blood glucose\",\"unit\":\"mmol/L\",\"direction\":\"target-band\",\"normalRange\":{\"low\":3.9,\"high\":7.8}},\"profile\":{\"ageYears\":42,\"sex\":\"MALE\"},\"dataCoverage\":{\"totalMeasurements\":35,\"totalSpanDays\":136,\"newestMeasurementDaysAgo\":1},\"BLOOD_GLUCOSE\":{\"signal\":{\"metric\":\"Blood glucose\",\"unit\":\"mmol/L\",\"current\":5.66,\"currentWindowDays\":5,\"baseline\":5.67,\"baselineLabel\":\"your monthly average\",\"delta\":-0.01,\"deltaPct\":-0.2,\"spread\":0.31,\"outsideNormalSwing\":false,\"direction\":\"target-band\",\"n\":5,\"newestDaysAgo\":1,\"normalRange\":{\"low\":3.9,\"high\":7.8},\"placement\":\"in band\"},\"summary\":{\"points\":35,\"start\":5.3,\"end\":6,\"delta\":0.7,\"mean\":5.47,\"min\":4.8,\"max\":6},\"series\":{\"recent\":[{\"date\":\"2026-05-30\",\"min\":5.49,\"max\":5.49,\"mean\":5.49,\"n\":1},{\"date\":\"2026-06-03\",\"min\":5.66,\"max\":5.66,\"mean\":5.66,\"n\":1},{\"date\":\"2026-06-07\",\"min\":5.94,\"max\":5.94,\"mean\":5.94,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5.88,\"max\":5.88,\"mean\":5.88,\"n\":1},{\"date\":\"2026-06-15\",\"min\":5.33,\"max\":5.33,\"mean\":5.33,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":5.05,\"max\":5.05,\"mean\":5.05,\"n\":1},{\"weekISO\":\"2026-W13\",\"min\":4.88,\"max\":5.27,\"mean\":5.08,\"n\":2},{\"weekISO\":\"2026-W14\",\"min\":5.77,\"max\":5.83,\"mean\":5.8,\"n\":2},{\"weekISO\":\"2026-W15\",\"min\":5.66,\"max\":5.88,\"mean\":5.77,\"n\":2},{\"weekISO\":\"2026-W16\",\"min\":5.05,\"max\":5.05,\"mean\":5.05,\"n\":1},{\"weekISO\":\"2026-W17\",\"min\":4.94,\"max\":5.16,\"mean\":5.05,\"n\":2},{\"weekISO\":\"2026-W18\",\"min\":5.22,\"max\":5.72,\"mean\":5.47,\"n\":2},{\"weekISO\":\"2026-W19\",\"min\":5.72,\"max\":6.05,\"mean\":5.88,\"n\":2},{\"weekISO\":\"2026-W20\",\"min\":5.49,\"max\":5.49,\"mean\":5.49,\"n\":1},{\"weekISO\":\"2026-W21\",\"min\":4.83,\"max\":5.22,\"mean\":5.02,\"n\":2},{\"weekISO\":\"2026-W22\",\"min\":5.05,\"max\":5.05,\"mean\":5.05,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":5.99,\"max\":5.99,\"mean\":5.99,\"n\":1},{\"month\":\"2026-02\",\"min\":4.88,\"max\":5.83,\"mean\":5.37,\"n\":7},{\"month\":\"2026-03\",\"min\":5.22,\"max\":5.99,\"mean\":5.65,\"n\":4}],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":1,\"value\":5.3}}}" + ] + } + ], + "temperature": 0.45, + "maxTokens": 700, + "seed": 1618033, + "responseFormat": "json", + "timeoutMs": 60000 + } + ], + "enqueued": [], + "result": { + "hasProvider": true, + "text": "Parity note for next-day-en.", + "cached": false, + "updatedAt": "2026-06-16T10:00:00.000Z" + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "e348f34b369dce59f42c06c8bcf06a163f86dc61913f8af96d3bb5a49e55f4f6", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + } + ] + }, + "BLOOD_GLUCOSE read-only-stale": { + "calls": [], + "enqueued": [ + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "en" + } + ], + "result": { + "hasProvider": true, + "text": "Parity note for next-day-en.", + "cached": true, + "updatedAt": "2026-06-16T10:00:00.000Z", + "preparing": false, + "revalidating": true + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "e348f34b369dce59f42c06c8bcf06a163f86dc61913f8af96d3bb5a49e55f4f6", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + } + ] + }, + "BLOOD_GLUCOSE prose-de": { + "calls": [ + { + "system": [ + "Du schreibst eine kurze, persönliche Einschätzung zu EINER Gesundheitsmetrik dieses Nutzers. Du kennst seine Daten und ordnest sie gegen seine EIGENE Baseline ein — nicht gegen Bevölkerungsnormen. Deine fachliche Grundlage sind anerkannte Leitlinien (ESH 2023, WHO, AASM, DGE, DEGAM); du beziehst dich darauf, ohne zu diagnostizieren.", + "", + "DEINE DATENGRUNDLAGE (graded snapshot):", + "- signal: der fertige Vergleich — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` ist das recent-Mittel; `baseline` ist der EIGENE längere Durchschnitt; `delta`/`deltaPct` sind die VORZEICHENBEHAFTETE Änderung gegenüber dieser Baseline; `spread` ist die normale Schwankung; `outsideNormalSwing` ist das vorab berechnete Urteil, ob die Änderung diese Schwankung übersteigt. Diese Werte sind bereits berechnet — nenne sie, rechne sie NICHT neu. `contributors[]` (nur Score-Komposite) benennt die 1–2 Teil-Scores, die den Score bewegt haben.", + "- summary: { points, start, end, delta, mean, min, max } — die Eckwerte des gesamten Lesefensters.", + "- series.recent[]: die jüngsten ~21 Tage, je ein Eintrag pro Tag ({ date, min, max, mean, n }).", + "- series.weekly[]: die ~10 Wochen davor, je ein ISO-Wochen-Aggregat.", + "- series.monthly[]: die ~12 Monate davor, je ein Monatsaggregat.", + "- series.yearly[]: alles ältere, je ein Jahr ({ year, mean, min, max, n, slope }).", + "- Die jüngsten Tage liegen also EINZELN vor, ältere Zeiträume als Wochen-, Monats- und Jahresaggregate. Ziehe die series nur für einen konkreten Tag heran; führe sonst mit dem signal-Block.", + "- Ein Slice kann leer sein, wenn die Historie kurz ist. Dann nicht so tun, als gäbe es ihn. Ist `signal.baseline` null, gibt es noch keine etablierte Baseline; sage das ehrlich, statt einen Vergleich zu erfinden.", + "", + "SO BAUST DU DIE EINSCHÄTZUNG — wähle die 2–3 BAUSTEINE, die zu DIESEN Daten passen, und ordne sie so, dass sie als EIN zusammenhängender Gedanke lesen, keine feste Checkliste. Führe gemäß dem OPENER-HINWEIS im User-Prompt, wenn einer mitgegeben ist — eröffne NICHT immer mit der Zahl. Variiere die Form von Metrik zu Metrik; eröffne nie zwei Metriken hintereinander gleich.", + "Verfügbare Bausteine (nimm die wenigen, die wirklich zutreffen):", + "(a) der aktuelle Befund — `current` aus dem signal-Block (oder ein Wert aus series.recent für eine Tagesfrage);", + "(b) gegen die EIGENE Baseline einordnen über das vorab berechnete vorzeichenbehaftete `delta` gegenüber `baselineLabel` — nenne es, leite es nicht neu her. Behandle `outsideNormalSwing: false` als \"innerhalb der üblichen Schwankung — kein Befund\" und ERFINDE KEINEN Trend; `outsideNormalSwing: true` ist die echte, meldenswerte Änderung. Ein `normalRange`/`placement` ist nur ein grober sekundärer Anker — das persönliche delta führt;", + "(c) einen echten, verdienten Erfolg oder eine stabile Serie benennen, wenn die Daten ihn zeigen;", + "(d) einen Punkt zum Im-Auge-behalten;", + "(e) einen Zusammenhang NUR, wenn ein r-Wert vorhanden und |r| > 0,4 ist, formuliert als \"bewegt sich mit\" / \"zeigt sich oft zusammen mit\", nie kausal;", + "(f) GENAU EINEN machbaren nächsten Schritt — und nur, wenn der Befund wirklich einen hergibt. Eine Botschaft = ein Verhalten, keine Liste. Ist der Wert stabil und im grünen Bereich, ERZWINGE KEINEN Schritt — bestätige kurz und nenne stattdessen einen Punkt zum Beobachten. Ein erfundener Schritt ist genau die Floskel, die wir vermeiden.", + "Synthese statt Aufzählung: die Geschichte dessen, was die Daten bedeuten, zählt mehr als das Wiederholen jeder Zahl.", + "", + "TONALITÄT — ein warmer, motivierender Premium-Begleiter (jemand hat sich die Daten dieser Person wirklich angesehen und etwas Wertvolles für sie gefunden):", + "- Zweite Person (\"dein Blutdruck\", \"deine Werte\"), warm, direkt, ehrlich.", + "- Motivierend und vorwärtsgewandt: Wenn die Daten es hergeben, benenne den echten Erfolg klar und baue ein wenig Schwung auf — die Person soll sich gesehen und unterstützt fühlen, nicht belehrt. Die Ermutigung muss durch die Zahlen VERDIENT sein, nie ein reflexhaftes Kompliment.", + "- Autonomie-unterstützend: \"kann helfen\", \"einen Versuch wert\", nie \"du musst\".", + "- Nie alarmierend, nie moralisierend, nie diagnostisch — keine Krankheitsbehauptung. Formuliere als begründete Beobachtung, nicht als ärztlichen Rat.", + "- Auch ungünstige Werte ehrlich benennen: Befund → gegen die eigene Baseline einordnen → ein kleiner machbarer Schritt, als Chance formuliert, nicht als Versagen. Nicht verharmlosen, nicht dramatisieren.", + "- Keine Floskeln und keine bloße Zahlenwiederholung: jede warme Zeile ist an eine echte Zahl oder eine echte Veränderung geknüpft. Ein generischer Positiv-Einstieg (\"Deine Werte sehen gut aus\") ist verboten — verdiene die Ermutigung mit dem konkreten Befund.", + "", + "ERÖFFNUNG — mit der Bedeutung führen, nicht mit dem Messwert", + "Beginne mit dem Eindruck in klaren Worten — was das heute über die Person sagt — und bring die Zahl direkt danach als Beleg, nie als Erstes. Eine Zeile, die mit einem Wert eröffnet (\"Dein Ruhepuls ist 61 …\"), liest sich wie ein Ablesen; dieselbe Zeile, die mit der Bedeutung eröffnet (\"Dein Herz läuft diese Woche etwas ruhiger — rund 61 …\"), liest sich wie ein Begleiter, der wirklich hingesehen hat. Jede Aussage bleibt an einer echten Zahl verankert; nur die REIHENFOLGE ändert sich, Bedeutung zuerst und die Zahl direkt dahinter. Das gilt, ob die Fläche eine Metrik abdeckt oder viele.", + "", + "LÄNGE: 2-4 Sätze, ca. 30-60 Wörter — ist der Wert aber stabil und nichts zu tun, ist EIN knapper Satz besser, als ihn auf Länge zu strecken. Knapp und hochwertig. Keine bloße Zahlenwiederholung, kein Fülltext, keine generischen Floskeln.", + "", + "DATENLAGE EHRLICH BEWERTEN (nie einen Trend erfinden):", + "- Nur wenige Messpunkte/Einträge/Ereignisse oder ein leerer recent-Slice: ehrlich sagen, dass es für eine belastbare Tendenz noch zu wenige Daten sind, und EINEN Hinweis geben (z.B. regelmäßiger erfassen). Keinen Trend behaupten. Was \"wenige\" bedeutet, hängt von der Metrik ab — bei Stimmung sind es Einträge, bei der Einnahmetreue geplante Dosen.", + "- Jüngste Messung deutlich älter als ~7 Tage (dataCoverage.newestMeasurementDaysAgo): darauf hinweisen, dass die Werte nicht mehr aktuell sind.", + "- Korrelationen NUR erwähnen, wenn der r-Wert im Snapshot vorhanden ist und |r| > 0.4 ist. Fehlt das Feld, keine Korrelation interpretieren oder erfinden. Immer als \"Zusammenhang\" formulieren, nie als \"Ursache\".", + "", + "VERBOTENE FLOSKELN (signalisieren ungegroundeten Fülltext — nie ausgeben, außer im Disclaimer)", + "\"achte auf ausreichend Schlaf\", \"trinke genug Wasser\", \"regelmäßige Bewegung\", \"ärztlicher Rat empfohlen\".", + "", + "METRIK-IDENTIFIER GEHÖREN NICHT IN DEN FLIESSTEXT", + "Schreibe niemals Datenbank- bzw. Enum-Namen wie \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" oder \"SLEEP_DURATION\" in nutzersichtbare Strings, und schreibe nie das wörtliche \"metric:\"-Chart-Token in den Fließtext. Verweise auf jede Metrik mit der natürlichsprachlichen Bezeichnung, die der Nutzer sieht — \"deine Systole\", \"dein Gewicht\", \"dein Puls\", \"deine Stimmung\", \"deine Medikamentenadhärenz\", \"dein Ruhepuls\", \"deine Schlafdauer\", \"deine Schritte\".", + "", + "GLP-1-DOSIS-SICHERHEIT (Vertrag, kein Stil)", + "Verschreibe, empfiehl oder ändere NIEMALS Medikamenten-Dosen, auch wenn der Snapshot einen GLP-1-Rezeptoragonisten benennt (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). Du darfst den benannten Wirkstoff und die aktuelle Titrationsstufe NENNEN (\"Woche 3 auf 7,5 mg\"), wenn der Snapshot sie trägt, aber schreibe nie \"erhöhe auf X mg\", \"erwäge die nächste Stufe Y mg\", \"bleibe auf Z mg\" oder eine Variante davon. Ein Plateau rahmt die nächste Entscheidung immer als Gespräch mit der behandelnden Ärztin. Bist du unsicher, ob etwas dosis-präskriptiv ist, behandle es so — und verweise an die Klinik.", + "", + "AKUTE WARNZEICHEN (Vertrag, kein Stil)", + "Beschreibt der Nutzer ein akutes Krisenzeichen — Brustschmerz oder Druck auf der Brust, Ohnmacht oder Beinahe-Ohnmacht (Synkope), ein plötzliches schweres Symptom (z. B. stärkster Kopfschmerz aller Zeiten, plötzliche Schwäche oder Sprachstörung, Atemnot), einen hypertensiven Notfallwert (systolisch ≥ 180 oder diastolisch ≥ 120 mit Symptomen), einen schwer-niedrigen oder sehr hohen Glukosewert (unter 54 mg/dL (3,0 mmol/L), oder unter 70 mg/dL (3,9 mmol/L) mit Symptomen, oder ≥ 250 mg/dL (13,9 mmol/L) mit Symptomen), anhaltendes Fieber ≥ 38,5 °C (101,3 °F) oder Gedanken an Suizid bzw. Selbstverletzung — sage in einem ruhigen Satz klar, dass das jetzt umgehende ärztliche Hilfe oder den Notruf braucht, und coache es NICHT anhand der Daten. Diagnostiziere nicht, benenne keine Erkrankung, schätze keinen Schweregrad aus den Zahlen — verweise nur auf umgehende Hilfe bzw. den Notruf und höre auf. Das ist eine geschlossene Liste; außerhalb davon bleibe wie gewohnt ruhig und nicht alarmierend.", + "", + "ERDUNG — NULL HALLUZINATIONEN", + "Jede Aussage muss auf einer Zahl beruhen, die im übergebenen Snapshot sichtbar ist. Lässt sich die Aussage keinem Snapshot-Feld zuordnen, lass sie weg und erfinde nie einen Messwert, um einer Anfrage zu entsprechen. Ordne jeden Wert gegen die EIGENE Baseline des Nutzers ein (jüngeres vs. längeres Mittel), nie gegen eine Bevölkerungsnorm. Nenne einen Zusammenhang nur, wenn der r-Wert vorhanden und |r| > 0,4 ist — immer als \"Zusammenhang\", nie als \"Ursache\".", + "", + "TONALITÄT — ein warmer, motivierender Begleiter, nie klinisch oder alarmierend", + "Schreibe in der zweiten Person, warm und direkt. Wenn die Daten es hergeben, benenne den echten Erfolg klar und baue ein wenig Schwung auf — die Person soll sich gesehen und unterstützt fühlen, nicht belehrt. Die Ermutigung muss durch die Zahlen VERDIENT sein, nie ein reflexhaftes Kompliment. Lob ist verankert, konkret und angemessen: an einer echten Zahl oder Veränderung festgemacht, nicht auf andere Nutzer übertragbar, und stiller Anerkennung für stille Erfolge. Über-Bestätigung ist eine Sicherheitsregression, keine Wärme — bestätige nie einen sich verschlechternden Trend oder eine unsichere Entscheidung; würdige die Mühe, nicht die Entscheidung, und bleib neutral (\"nichts zu tun — die gute Art von langweilig\"), wenn es nichts zu loben gibt, statt ein Kompliment zu erfinden. Sei autonomie-unterstützend (\"einen Versuch wert\", \"kann helfen\", nie \"du musst\"). Benenne auch ungünstige Werte ehrlich — Befund, dann gegen die eigene Baseline einordnen, dann ein kleiner machbarer Schritt, als Chance formuliert. Nie alarmierend, nie moralisierend, nie diagnostisch. Keine Floskeln und keine bloße Zahlenwiederholung. Beginne NICHT mit einem Kompliment über Datenmenge oder -qualität; verbotene Eröffnungen sind u.a. \"Datengrundlage ist sehr stark\", \"Your data foundation is strong\", \"Du hast eine solide Baseline\", \"Großartiger Datensatz\", ein generisches \"Deine Werte sehen gut aus\" und jede sinngemäße Umformulierung.", + "", + "KEINE EINSCHÄTZUNG — nie Mechanik nacherzählen", + "Eine Messanzahl (\"3-mal erfasst\"), ein Erfassungsrhythmus (\"alle paar Tage gemessen\") oder Bedien-Mechanik ist KEINE Einschätzung und gehört für sich genommen nicht in den Text. Nutze eine solche Angabe NUR, wenn sie eine Konsequenz trägt, und nenne dann die Konsequenz, nicht die Zahl: z. B. \"diese Woche zu wenige Werte für eine Tendenz — eine Morgenmessung würde es klären\", nicht \"du hast diese Woche zweimal gemessen\". Jeder Satz muss sich verdienen, indem er sagt, was ein Wert BEDEUTET oder was daraus folgt — nie, indem er erzählt, wie die Daten erhoben wurden.", + "", + "EINORDNEN, NICHT AUFZÄHLEN", + "Sag, was ein Wert BEDEUTET, nicht nur, was er ist. Trägt der User-Prompt einen EINORDNUNGS-KONTEXT-Block, nutze ihn: benenne, wo der Wert auf der Leitlinien-Skala liegt, was dieses Band in klaren, würdevollen Worten bedeutet und was praktisch folgt. Beurteile den Trend IM KONTEXT — ein \"leicht steigender\" Wert tief in einem günstigen Band ist eine Randnotiz; dieselbe Bewegung an oder nahe einer Grenze ist die Schlagzeile. Rahme jede Konsequenz OHNE Diagnose: \"Werte in diesem Bereich gelten laut Leitlinien als …\", nie \"du hast X\" oder \"du bist gefährdet für X\". Ist KEIN Einordnungs-Block vorhanden, hat die Metrik kein allgemeines Referenzband — ordne sie gegen die EIGENE Baseline der Person ein und sage klar, dass kein allgemeines Referenzband hinterlegt ist, statt eines zu erfinden.", + "", + "FORMATIERUNG — schreibe in kurzen Absätzen, getrennt durch eine LEERZEILE, je 1–3 Sätze; eine längere Antwort hat 2–4 Absätze, nie einen Block. Setze eine Leerzeile zwischen unterschiedliche Gedanken (z. B. \"wie es steht\" vs. \"eine Sache zum Ausprobieren\"). Ein stabiler Einzeiler bleibt EIN Absatz — strecke nicht auf einen zweiten. Wenn du wirklich drei oder mehr parallele Punkte aufzählst (Optionen, Befunde, Schritte), formatiere sie als schlichte Liste: ein Punkt pro Zeile, jede Zeile beginnt mit \"- \". Die EINE wichtigste Kernaussage darfst du mit **doppelten Sternchen** fett setzen, höchstens einmal pro Antwort. Sonst nichts: keine Überschriften, kein Kursiv, keine Backticks, keine nummerierten Listen, keine Emojis.", + "", + "BEISPIELE — beachte die UNTERSCHIEDLICHEN Formen (urteil-zuerst, trend-zuerst, Einzeiler). Sie illustrieren nur Form und Erdung; übernimm sie nie — jede Einschätzung nutzt die echten Snapshot-Zahlen:", + "- URTEIL-ZUERST (Bedeutung zuerst, Zahl als Beleg, ein Schritt): \"Dein Ruhepuls läuft gerade einen Tick niedriger als sonst — diese Woche rund 61 bpm, 5 unter deinem Monatsmittel von 66 und dein niedrigster seit Wochen. So ein Rückgang passt meist zu mehr Bewegung; die Routine, die das gebracht hat, lohnt sich beizubehalten.\"", + "- TREND-ZUERST (Richtung zuerst, dann der Stand): \"Dein systolischer Blutdruck geht seit drei Wochen ruhig nach unten, aktuell rund 124 mmHg — etwa 4 unter deinem 30-Tage-Schnitt. Nichts Dramatisches, einfach eine stetige Richtung.\"", + "- EINZEILER (stabil, KEIN erzwungener Schritt): \"Deine SpO₂ ist mit 97 % stabil und genau in deinem üblichen Bereich — nichts zu tun, die gute Art von langweilig.\"", + "- SCHLECHT (verbotene Floskel, ungegroundet — so NICHT): \"Deine Werte sehen gut aus. Achte auf ausreichend Schlaf und regelmäßige Bewegung.\"", + "", + "AUSGABEFORMAT: Antworte ausschließlich mit validem JSON in genau diesem Schema. Das Feld \"summary\" enthält die komplette Einschätzung auf Deutsch: 1-3 kurze Absätze mit je 1-3 Sätzen, getrennt durch eine Leerzeile als \\n\\n INNERHALB des JSON-Strings. Ein stabiler Einzeiler bleibt EIN Absatz:", + "{ \"summary\": \"...\" }", + "", + "ARCHETYP — PHYSIOLOGISCHER VITALWERT:", + "- Das ist ein Momentanwert. Benenne das aktuelle Niveau und ordne es sowohl gegen die eigene Baseline (recent vs. Monats-/Jahresmittel) als auch — falls ein Referenzband vorliegt — gegen dieses Band ein.", + "- Ein einzelner Wert außerhalb des Bands ist selten ein Befund; eine anhaltende Verschiebung über das recent-Fenster gegen die Baseline schon. Erwähne einen Trend außerhalb des Bands sachlich, ohne zu alarmieren.", + "- Formuliere jede Richtung (steigend/fallend) als mögliche Erklärung (Fitness, Stress, Schlaf, Krankheit, Medikament), nie als Diagnose.", + "", + "METRIK: Blood glucose (Einheit: mmol/L).", + "Kein Extrem ist gut — der Wert sollte im Normalband liegen; Abweichungen in beide Richtungen sind erwähnenswert.", + "Grobes Referenzband (Population): 3.9–7.8 mmol/L (nur Orientierung — die EIGENE Baseline führt)." + ], + "messages": [ + { + "role": "user", + "content": [ + "Datum: 2026-06-15", + "OPENER HINT: Eröffne mit der Richtung der Veränderung über die Zeit und nenne dann den aktuellen Stand.", + "Schreibe eine kurze Einschätzung zu Blood glucose dieser Person. Beginne mit der BEDEUTUNG in klaren Worten — dem Eindruck, nicht der Zahl (z. B. \"läuft gerade etwas ruhiger als sonst\", \"stabil und genau da, wo es war\") — und bring danach EINE konkrete Zahl aus dem Snapshot als Beleg, gegen die eigene Wochen-/Monats-Baseline eingeordnet; führe nie mit dem Wert. Schließe nur dann mit einem machbaren Schritt, wenn der Befund wirklich einen hergibt; ist nichts umsetzbar, lass den Schritt weg statt Fülltext zu erfinden. Konfidenz aus Messanzahl und Aktualität ableiten.", + "", + "VORHERIGE ANALYSE: keine vorhanden. Behandle dies als erste Analyse in diesem Bereich — kein Verbesserungs-/Verschlechterungs-Delta zu nennen.", + "", + "", + "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der Konstanz bzw. Streuung der Werte eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", + "", + "DATENLAGE: 35 Tages-Buckets im Fenster, letzter Wert heute.", + "", + "ZUSAMMENHÄNGE (statistisch geprüfte Assoziationen in DEINEN Daten, die diese Metrik betreffen — beschreibend, NIE kausal):", + "- Wenn der Blutzucker höher liegt, scheint die Ruheherzfrequenz am Folgetag nach bisheriger Datenlage eher höher auszufallen. Das ist ein Muster, das Beachtung verdient, keine Ursache. (n=34, r=0.94)", + "- Wenn der Blutzucker höher liegt, scheint der systolische Blutdruck am Folgetag nach bisheriger Datenlage eher höher auszufallen. Das ist ein Muster, das Beachtung verdient, keine Ursache. (n=23, r=0.91)", + "Du DARFST höchstens EINEN davon einflechten, wenn er echten Mehrwert bringt — als \"Zusammenhang\" / \"geht tendenziell einher mit\" formuliert, nie als Ursache, und ohne die Zahlen zu verändern. Lass sie weg, wenn der eigene Befund der Metrik die stärkere Geschichte ist.", + "", + "", + "{\"locale\":\"de\",\"promptVersion\":\"6.3.0\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"BLOOD_GLUCOSE\",\"metric\":{\"id\":\"BLOOD_GLUCOSE\",\"displayName\":\"Blood glucose\",\"unit\":\"mmol/L\",\"direction\":\"target-band\",\"normalRange\":{\"low\":3.9,\"high\":7.8}},\"profile\":{\"ageYears\":42,\"sex\":\"MALE\"},\"dataCoverage\":{\"totalMeasurements\":35,\"totalSpanDays\":136,\"newestMeasurementDaysAgo\":0},\"BLOOD_GLUCOSE\":{\"signal\":{\"metric\":\"Blood glucose\",\"unit\":\"mmol/L\",\"current\":5.56,\"currentWindowDays\":6,\"baseline\":5.67,\"baselineLabel\":\"your monthly average\",\"delta\":-0.11,\"deltaPct\":-2,\"spread\":0.31,\"outsideNormalSwing\":false,\"direction\":\"target-band\",\"n\":6,\"newestDaysAgo\":0,\"normalRange\":{\"low\":3.9,\"high\":7.8},\"placement\":\"in band\"},\"summary\":{\"points\":35,\"start\":5.3,\"end\":6,\"delta\":0.7,\"mean\":5.47,\"min\":4.8,\"max\":6},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":5.05,\"max\":5.05,\"mean\":5.05,\"n\":1},{\"date\":\"2026-05-30\",\"min\":5.49,\"max\":5.49,\"mean\":5.49,\"n\":1},{\"date\":\"2026-06-03\",\"min\":5.66,\"max\":5.66,\"mean\":5.66,\"n\":1},{\"date\":\"2026-06-07\",\"min\":5.94,\"max\":5.94,\"mean\":5.94,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5.88,\"max\":5.88,\"mean\":5.88,\"n\":1},{\"date\":\"2026-06-15\",\"min\":5.33,\"max\":5.33,\"mean\":5.33,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":5.05,\"max\":5.05,\"mean\":5.05,\"n\":1},{\"weekISO\":\"2026-W13\",\"min\":4.88,\"max\":5.27,\"mean\":5.08,\"n\":2},{\"weekISO\":\"2026-W14\",\"min\":5.77,\"max\":5.83,\"mean\":5.8,\"n\":2},{\"weekISO\":\"2026-W15\",\"min\":5.66,\"max\":5.88,\"mean\":5.77,\"n\":2},{\"weekISO\":\"2026-W16\",\"min\":5.05,\"max\":5.05,\"mean\":5.05,\"n\":1},{\"weekISO\":\"2026-W17\",\"min\":4.94,\"max\":5.16,\"mean\":5.05,\"n\":2},{\"weekISO\":\"2026-W18\",\"min\":5.22,\"max\":5.72,\"mean\":5.47,\"n\":2},{\"weekISO\":\"2026-W19\",\"min\":5.72,\"max\":6.05,\"mean\":5.88,\"n\":2},{\"weekISO\":\"2026-W20\",\"min\":5.49,\"max\":5.49,\"mean\":5.49,\"n\":1},{\"weekISO\":\"2026-W21\",\"min\":4.83,\"max\":5.22,\"mean\":5.02,\"n\":2}],\"monthly\":[{\"month\":\"2026-01\",\"min\":5.99,\"max\":5.99,\"mean\":5.99,\"n\":1},{\"month\":\"2026-02\",\"min\":4.88,\"max\":5.83,\"mean\":5.37,\"n\":7},{\"month\":\"2026-03\",\"min\":5.22,\"max\":5.99,\"mean\":5.65,\"n\":4}],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":0,\"value\":5.3}}}" + ] + } + ], + "temperature": 0.45, + "maxTokens": 700, + "seed": 1618033, + "responseFormat": "json", + "timeoutMs": 60000 + } + ], + "enqueued": [], + "result": { + "hasProvider": true, + "text": "Das ist kein JSON.", + "cached": false, + "updatedAt": "2026-06-15T10:00:00.000Z" + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "5df7980e558674aefbfe894525e4d2721f281a3dcf776465d7cc1ba0983b42fd", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "e348f34b369dce59f42c06c8bcf06a163f86dc61913f8af96d3bb5a49e55f4f6", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + } + ] + }, + "BLOOD_GLUCOSE unparseable-fr": { + "calls": [ + { + "system": [ + "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", + "", + "YOUR DATA (graded snapshot):", + "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", + "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", + "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", + "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", + "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", + "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", + "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", + "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", + "", + "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", + "Moves available (choose the few that genuinely apply):", + "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", + "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", + "(c) name a genuine, earned win or a steady streak when the data shows one;", + "(d) one watch-item worth keeping an eye on;", + "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", + "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", + "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", + "", + "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", + "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", + "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", + "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", + "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", + "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", + "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", + "", + "OPENING — lead with meaning, not the metric value", + "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", + "", + "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", + "", + "JUDGE THE DATA HONESTLY (never invent a trend):", + "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", + "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", + "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", + "", + "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", + "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", + "", + "METRIC IDENTIFIERS STAY OUT OF PROSE", + "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", + "", + "GLP-1 DOSE SAFETY (contract, not style)", + "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", + "", + "ACUTE RED FLAGS (contract, not style)", + "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", + "", + "GROUNDING — ZERO HALLUCINATIONS", + "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", + "", + "TONE — a warm, motivating advisor, never clinical or alarming", + "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", + "", + "NOT AN INSIGHT — never recite mechanics", + "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", + "", + "INTERPRET, DON'T ENUMERATE", + "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", + "", + "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", + "", + "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", + "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", + "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", + "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", + "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", + "", + "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in French: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", + "{ \"summary\": \"...\" }", + "", + "ARCHETYPE — PHYSIOLOGICAL VITAL:", + "- This is a point-in-time physiological reading. Name the recent level, place it against both the user's own baseline (recent vs monthly/yearly mean) and, when a reference band is given, that band.", + "- A single out-of-band reading is rarely a finding; a sustained shift across the recent window against the user's baseline is. Note an out-of-band trend without alarm.", + "- Frame any direction (rising/falling) as a possible explanation (fitness, stress, sleep, illness, medication), never a diagnosis.", + "", + "METRIC: Blood glucose (unit: mmol/L).", + "Neither extreme is good — the value should sit inside its normal band; deviations in either direction are worth noting.", + "Coarse population reference band: 3.9–7.8 mmol/L (a placement aid only — the user's OWN baseline leads).", + "", + "OUTPUT LANGUAGE: Répondez en français. Utilisez le vouvoiement par défaut ; tutoyez si l'utilisateur tutoie en premier. Utilisez un vocabulaire de santé français naturel." + ], + "messages": [ + { + "role": "user", + "content": [ + "Date: 2026-06-15", + "OPENER HINT: Open with the direction of change over time, then say where it stands now.", + "Write one short assessment of this person's blood glucose. Open with what it MEANS in plain words — the read, not the number (e.g. \"running a little calmer than usual\", \"steady and right where it's been\") — then bring in ONE concrete number from the snapshot right after as support, placed against their own weekly/monthly baseline; never lead with the value. Close with one doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the measurement count and recency.", + "", + "PREVIOUS ANALYSIS: none on file. Treat this as the user's first analysis for this domain — no improvement/regression delta to surface.", + "", + "", + "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der Konstanz bzw. Streuung der Werte eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", + "", + "DATENLAGE: 35 Tages-Buckets im Fenster, letzter Wert heute.", + "", + "ZUSAMMENHÄNGE (statistisch geprüfte Assoziationen in DEINEN Daten, die diese Metrik betreffen — beschreibend, NIE kausal):", + "- Quand la glycémie monte, la fréquence cardiaque au repos du lendemain semble avoir tendance à augmenter, au vu des données actuelles. C'est une tendance à surveiller, pas une cause. (n=34, r=0.94)", + "- Quand la glycémie monte, la pression systolique du lendemain semble avoir tendance à augmenter, au vu des données actuelles. C'est une tendance à surveiller, pas une cause. (n=23, r=0.91)", + "Du DARFST höchstens EINEN davon einflechten, wenn er echten Mehrwert bringt — als \"Zusammenhang\" / \"geht tendenziell einher mit\" formuliert, nie als Ursache, und ohne die Zahlen zu verändern. Lass sie weg, wenn der eigene Befund der Metrik die stärkere Geschichte ist.", + "", + "", + "{\"locale\":\"fr\",\"promptVersion\":\"6.3.0\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"BLOOD_GLUCOSE\",\"metric\":{\"id\":\"BLOOD_GLUCOSE\",\"displayName\":\"Blood glucose\",\"unit\":\"mmol/L\",\"direction\":\"target-band\",\"normalRange\":{\"low\":3.9,\"high\":7.8}},\"profile\":{\"ageYears\":42,\"sex\":\"MALE\"},\"dataCoverage\":{\"totalMeasurements\":35,\"totalSpanDays\":136,\"newestMeasurementDaysAgo\":0},\"BLOOD_GLUCOSE\":{\"signal\":{\"metric\":\"Blood glucose\",\"unit\":\"mmol/L\",\"current\":5.56,\"currentWindowDays\":6,\"baseline\":5.67,\"baselineLabel\":\"your monthly average\",\"delta\":-0.11,\"deltaPct\":-2,\"spread\":0.31,\"outsideNormalSwing\":false,\"direction\":\"target-band\",\"n\":6,\"newestDaysAgo\":0,\"normalRange\":{\"low\":3.9,\"high\":7.8},\"placement\":\"in band\"},\"summary\":{\"points\":35,\"start\":5.3,\"end\":6,\"delta\":0.7,\"mean\":5.47,\"min\":4.8,\"max\":6},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":5.05,\"max\":5.05,\"mean\":5.05,\"n\":1},{\"date\":\"2026-05-30\",\"min\":5.49,\"max\":5.49,\"mean\":5.49,\"n\":1},{\"date\":\"2026-06-03\",\"min\":5.66,\"max\":5.66,\"mean\":5.66,\"n\":1},{\"date\":\"2026-06-07\",\"min\":5.94,\"max\":5.94,\"mean\":5.94,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5.88,\"max\":5.88,\"mean\":5.88,\"n\":1},{\"date\":\"2026-06-15\",\"min\":5.33,\"max\":5.33,\"mean\":5.33,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":5.05,\"max\":5.05,\"mean\":5.05,\"n\":1},{\"weekISO\":\"2026-W13\",\"min\":4.88,\"max\":5.27,\"mean\":5.08,\"n\":2},{\"weekISO\":\"2026-W14\",\"min\":5.77,\"max\":5.83,\"mean\":5.8,\"n\":2},{\"weekISO\":\"2026-W15\",\"min\":5.66,\"max\":5.88,\"mean\":5.77,\"n\":2},{\"weekISO\":\"2026-W16\",\"min\":5.05,\"max\":5.05,\"mean\":5.05,\"n\":1},{\"weekISO\":\"2026-W17\",\"min\":4.94,\"max\":5.16,\"mean\":5.05,\"n\":2},{\"weekISO\":\"2026-W18\",\"min\":5.22,\"max\":5.72,\"mean\":5.47,\"n\":2},{\"weekISO\":\"2026-W19\",\"min\":5.72,\"max\":6.05,\"mean\":5.88,\"n\":2},{\"weekISO\":\"2026-W20\",\"min\":5.49,\"max\":5.49,\"mean\":5.49,\"n\":1},{\"weekISO\":\"2026-W21\",\"min\":4.83,\"max\":5.22,\"mean\":5.02,\"n\":2}],\"monthly\":[{\"month\":\"2026-01\",\"min\":5.99,\"max\":5.99,\"mean\":5.99,\"n\":1},{\"month\":\"2026-02\",\"min\":4.88,\"max\":5.83,\"mean\":5.37,\"n\":7},{\"month\":\"2026-03\",\"min\":5.22,\"max\":5.99,\"mean\":5.65,\"n\":4}],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":0,\"value\":5.3}}}" + ] + } + ], + "temperature": 0.45, + "maxTokens": 700, + "seed": 1618033, + "responseFormat": "json", + "timeoutMs": 60000 + } + ], + "enqueued": [], + "result": { + "hasProvider": false, + "text": "Stable, comme d’habitude. Blood glucose est à 5,6 mmol/L en ce moment — en dessous de ta moyenne habituelle de 5,7 mmol/L. C’est dans tes repères habituels — rien qui appelle une réaction maintenant.", + "cached": true, + "updatedAt": null + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "5df7980e558674aefbfe894525e4d2721f281a3dcf776465d7cc1ba0983b42fd", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "e348f34b369dce59f42c06c8bcf06a163f86dc61913f8af96d3bb5a49e55f4f6", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + } + ] + }, + "BLOOD_GLUCOSE blocked-es": { + "calls": [ + { + "system": [ + "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", + "", + "YOUR DATA (graded snapshot):", + "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", + "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", + "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", + "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", + "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", + "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", + "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", + "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", + "", + "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", + "Moves available (choose the few that genuinely apply):", + "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", + "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", + "(c) name a genuine, earned win or a steady streak when the data shows one;", + "(d) one watch-item worth keeping an eye on;", + "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", + "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", + "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", + "", + "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", + "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", + "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", + "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", + "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", + "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", + "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", + "", + "OPENING — lead with meaning, not the metric value", + "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", + "", + "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", + "", + "JUDGE THE DATA HONESTLY (never invent a trend):", + "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", + "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", + "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", + "", + "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", + "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", + "", + "METRIC IDENTIFIERS STAY OUT OF PROSE", + "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", + "", + "GLP-1 DOSE SAFETY (contract, not style)", + "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", + "", + "ACUTE RED FLAGS (contract, not style)", + "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", + "", + "GROUNDING — ZERO HALLUCINATIONS", + "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", + "", + "TONE — a warm, motivating advisor, never clinical or alarming", + "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", + "", + "NOT AN INSIGHT — never recite mechanics", + "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", + "", + "INTERPRET, DON'T ENUMERATE", + "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", + "", + "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", + "", + "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", + "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", + "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", + "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", + "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", + "", + "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in Spanish: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", + "{ \"summary\": \"...\" }", + "", + "ARCHETYPE — PHYSIOLOGICAL VITAL:", + "- This is a point-in-time physiological reading. Name the recent level, place it against both the user's own baseline (recent vs monthly/yearly mean) and, when a reference band is given, that band.", + "- A single out-of-band reading is rarely a finding; a sustained shift across the recent window against the user's baseline is. Note an out-of-band trend without alarm.", + "- Frame any direction (rising/falling) as a possible explanation (fitness, stress, sleep, illness, medication), never a diagnosis.", + "", + "METRIC: Blood glucose (unit: mmol/L).", + "Neither extreme is good — the value should sit inside its normal band; deviations in either direction are worth noting.", + "Coarse population reference band: 3.9–7.8 mmol/L (a placement aid only — the user's OWN baseline leads).", + "", + "OUTPUT LANGUAGE: Responda en español (registro peninsular formal — usted). Use un vocabulario sanitario en español natural." + ], + "messages": [ + { + "role": "user", + "content": [ + "Date: 2026-06-15", + "OPENER HINT: Open with the direction of change over time, then say where it stands now.", + "Write one short assessment of this person's blood glucose. Open with what it MEANS in plain words — the read, not the number (e.g. \"running a little calmer than usual\", \"steady and right where it's been\") — then bring in ONE concrete number from the snapshot right after as support, placed against their own weekly/monthly baseline; never lead with the value. Close with one doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the measurement count and recency.", + "", + "PREVIOUS ANALYSIS: none on file. Treat this as the user's first analysis for this domain — no improvement/regression delta to surface.", + "", + "", + "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der Konstanz bzw. Streuung der Werte eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", + "", + "DATENLAGE: 35 Tages-Buckets im Fenster, letzter Wert heute.", + "", + "ZUSAMMENHÄNGE (statistisch geprüfte Assoziationen in DEINEN Daten, die diese Metrik betreffen — beschreibend, NIE kausal):", + "- Cuando la glucosa en sangre es mayor, la frecuencia cardíaca en reposo del día siguiente parece tender a subir, según los datos disponibles. Es un patrón que conviene observar, no una causa. (n=34, r=0.94)", + "- Cuando la glucosa en sangre es mayor, la presión sistólica del día siguiente parece tender a subir, según los datos disponibles. Es un patrón que conviene observar, no una causa. (n=23, r=0.91)", + "Du DARFST höchstens EINEN davon einflechten, wenn er echten Mehrwert bringt — als \"Zusammenhang\" / \"geht tendenziell einher mit\" formuliert, nie als Ursache, und ohne die Zahlen zu verändern. Lass sie weg, wenn der eigene Befund der Metrik die stärkere Geschichte ist.", + "", + "", + "{\"locale\":\"es\",\"promptVersion\":\"6.3.0\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"BLOOD_GLUCOSE\",\"metric\":{\"id\":\"BLOOD_GLUCOSE\",\"displayName\":\"Blood glucose\",\"unit\":\"mmol/L\",\"direction\":\"target-band\",\"normalRange\":{\"low\":3.9,\"high\":7.8}},\"profile\":{\"ageYears\":42,\"sex\":\"MALE\"},\"dataCoverage\":{\"totalMeasurements\":35,\"totalSpanDays\":136,\"newestMeasurementDaysAgo\":0},\"BLOOD_GLUCOSE\":{\"signal\":{\"metric\":\"Blood glucose\",\"unit\":\"mmol/L\",\"current\":5.56,\"currentWindowDays\":6,\"baseline\":5.67,\"baselineLabel\":\"your monthly average\",\"delta\":-0.11,\"deltaPct\":-2,\"spread\":0.31,\"outsideNormalSwing\":false,\"direction\":\"target-band\",\"n\":6,\"newestDaysAgo\":0,\"normalRange\":{\"low\":3.9,\"high\":7.8},\"placement\":\"in band\"},\"summary\":{\"points\":35,\"start\":5.3,\"end\":6,\"delta\":0.7,\"mean\":5.47,\"min\":4.8,\"max\":6},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":5.05,\"max\":5.05,\"mean\":5.05,\"n\":1},{\"date\":\"2026-05-30\",\"min\":5.49,\"max\":5.49,\"mean\":5.49,\"n\":1},{\"date\":\"2026-06-03\",\"min\":5.66,\"max\":5.66,\"mean\":5.66,\"n\":1},{\"date\":\"2026-06-07\",\"min\":5.94,\"max\":5.94,\"mean\":5.94,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5.88,\"max\":5.88,\"mean\":5.88,\"n\":1},{\"date\":\"2026-06-15\",\"min\":5.33,\"max\":5.33,\"mean\":5.33,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":5.05,\"max\":5.05,\"mean\":5.05,\"n\":1},{\"weekISO\":\"2026-W13\",\"min\":4.88,\"max\":5.27,\"mean\":5.08,\"n\":2},{\"weekISO\":\"2026-W14\",\"min\":5.77,\"max\":5.83,\"mean\":5.8,\"n\":2},{\"weekISO\":\"2026-W15\",\"min\":5.66,\"max\":5.88,\"mean\":5.77,\"n\":2},{\"weekISO\":\"2026-W16\",\"min\":5.05,\"max\":5.05,\"mean\":5.05,\"n\":1},{\"weekISO\":\"2026-W17\",\"min\":4.94,\"max\":5.16,\"mean\":5.05,\"n\":2},{\"weekISO\":\"2026-W18\",\"min\":5.22,\"max\":5.72,\"mean\":5.47,\"n\":2},{\"weekISO\":\"2026-W19\",\"min\":5.72,\"max\":6.05,\"mean\":5.88,\"n\":2},{\"weekISO\":\"2026-W20\",\"min\":5.49,\"max\":5.49,\"mean\":5.49,\"n\":1},{\"weekISO\":\"2026-W21\",\"min\":4.83,\"max\":5.22,\"mean\":5.02,\"n\":2}],\"monthly\":[{\"month\":\"2026-01\",\"min\":5.99,\"max\":5.99,\"mean\":5.99,\"n\":1},{\"month\":\"2026-02\",\"min\":4.88,\"max\":5.83,\"mean\":5.37,\"n\":7},{\"month\":\"2026-03\",\"min\":5.22,\"max\":5.99,\"mean\":5.65,\"n\":4}],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":0,\"value\":5.3}}}" + ] + } + ], + "temperature": 0.45, + "maxTokens": 700, + "seed": 1618033, + "responseFormat": "json", + "timeoutMs": 60000 + } + ], + "enqueued": [], + "result": { + "hasProvider": false, + "text": "Estable, como de costumbre. Blood glucose está en 5,6 mmol/L ahora mismo — por debajo de tu promedio habitual de 5,7 mmol/L. Eso queda dentro de tu rango habitual — nada que pida una reacción ahora.", + "cached": true, + "updatedAt": null + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "5df7980e558674aefbfe894525e4d2721f281a3dcf776465d7cc1ba0983b42fd", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "e348f34b369dce59f42c06c8bcf06a163f86dc61913f8af96d3bb5a49e55f4f6", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + } + ] + }, + "BLOOD_GLUCOSE bare-error-en": { + "calls": [], + "enqueued": [], + "result": { + "hasProvider": true, + "text": null, + "cached": false, + "updatedAt": null, + "insufficient": true + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "5df7980e558674aefbfe894525e4d2721f281a3dcf776465d7cc1ba0983b42fd", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "e348f34b369dce59f42c06c8bcf06a163f86dc61913f8af96d3bb5a49e55f4f6", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + } + ] + }, + "BLOOD_GLUCOSE bare-error-de": { + "calls": [], + "enqueued": [], + "result": { + "hasProvider": true, + "text": null, + "cached": false, + "updatedAt": null, + "insufficient": true + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "5df7980e558674aefbfe894525e4d2721f281a3dcf776465d7cc1ba0983b42fd", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "e348f34b369dce59f42c06c8bcf06a163f86dc61913f8af96d3bb5a49e55f4f6", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + } + ] + }, + "BLOOD_GLUCOSE keyless-en": { + "calls": [], + "enqueued": [], + "result": { + "hasProvider": false, + "text": "Steady and much as usual. Blood glucose is at 100 mg/dL right now — lower than your usual average of 102 mg/dL. That sits in your usual range — nothing you need to act on right now.", + "cached": true, + "updatedAt": null + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "5df7980e558674aefbfe894525e4d2721f281a3dcf776465d7cc1ba0983b42fd", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "e348f34b369dce59f42c06c8bcf06a163f86dc61913f8af96d3bb5a49e55f4f6", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + } + ] + }, + "BLOOD_GLUCOSE keyless-de": { + "calls": [], + "enqueued": [], + "result": { + "hasProvider": false, + "text": "Stabil und wie gewohnt. Blood glucose liegt aktuell bei 100 mg/dL — niedriger als dein üblicher Schnitt von 102 mg/dL. Das ist in deinem gewohnten Rahmen — nichts, worauf du jetzt reagieren musst.", + "cached": true, + "updatedAt": null + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "5df7980e558674aefbfe894525e4d2721f281a3dcf776465d7cc1ba0983b42fd", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "e348f34b369dce59f42c06c8bcf06a163f86dc61913f8af96d3bb5a49e55f4f6", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + } + ] + }, + "BLOOD_GLUCOSE keyless-read-only": { + "calls": [], + "enqueued": [], + "result": { + "hasProvider": false, + "text": "No assessment on this one right now. These numbers become readable across several weeks rather than single days, and taken at consistent times so they stay comparable. The signal that counts is several metrics drifting the same unfavourable way at once, not one value on one day.", + "cached": true, + "updatedAt": null + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "5df7980e558674aefbfe894525e4d2721f281a3dcf776465d7cc1ba0983b42fd", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "e348f34b369dce59f42c06c8bcf06a163f86dc61913f8af96d3bb5a49e55f4f6", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + } + ] + }, + "SLEEP_DURATION read-only-cold": { + "calls": [], + "enqueued": [ + { + "userId": "parity-rich", + "metric": "metric:SLEEP_DURATION", + "locale": "en" + } + ], + "result": { + "hasProvider": true, + "text": null, + "cached": false, + "updatedAt": null, + "preparing": true, + "revalidating": false + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "5df7980e558674aefbfe894525e4d2721f281a3dcf776465d7cc1ba0983b42fd", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "e348f34b369dce59f42c06c8bcf06a163f86dc61913f8af96d3bb5a49e55f4f6", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + } + ] + }, + "SLEEP_DURATION fresh-en": { + "calls": [ + { + "system": [ + "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", + "", + "YOUR DATA (graded snapshot):", + "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", + "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", + "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", + "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", + "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", + "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", + "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", + "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", + "", + "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", + "Moves available (choose the few that genuinely apply):", + "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", + "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", + "(c) name a genuine, earned win or a steady streak when the data shows one;", + "(d) one watch-item worth keeping an eye on;", + "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", + "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", + "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", + "", + "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", + "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", + "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", + "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", + "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", + "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", + "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", + "", + "OPENING — lead with meaning, not the metric value", + "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", + "", + "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", + "", + "JUDGE THE DATA HONESTLY (never invent a trend):", + "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", + "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", + "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", + "", + "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", + "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", + "", + "METRIC IDENTIFIERS STAY OUT OF PROSE", + "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", + "", + "GLP-1 DOSE SAFETY (contract, not style)", + "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", + "", + "ACUTE RED FLAGS (contract, not style)", + "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", + "", + "GROUNDING — ZERO HALLUCINATIONS", + "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", + "", + "TONE — a warm, motivating advisor, never clinical or alarming", + "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", + "", + "NOT AN INSIGHT — never recite mechanics", + "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", + "", + "INTERPRET, DON'T ENUMERATE", + "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", + "", + "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", + "", + "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", + "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", + "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", + "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", + "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", + "", + "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in English: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", + "{ \"summary\": \"...\" }", + "", + "ARCHETYPE — SLEEP:", + "- This is nightly sleep duration. The snapshot's values are in minutes; convert to hours for your prose (e.g. 450 min = 7.5 h). Assess both the typical duration and its CONSISTENCY: a stable ~7–9 h is the favourable pattern; high night-to-night variability is itself worth naming even when the mean looks fine.", + "- Compare the recent nights to the user's own weekly/monthly baseline. A reference band of 7–9 h is a coarse adult guide, not a verdict — short or long sleepers vary.", + "- Close with one doable step toward more consistent or sufficient sleep ONLY when the finding implies one — never the forbidden 'get enough sleep' platitude; be specific (e.g. a steadier wind-down time). When sleep is already consistent and sufficient, affirm it and name one thing worth keeping an eye on instead of manufacturing a step.", + "", + "METRIC: Sleep duration (unit: min).", + "Neither extreme is good — the value should sit inside its normal band; deviations in either direction are worth noting.", + "Coarse population reference band: 420–540 min (a placement aid only — the user's OWN baseline leads)." + ], + "messages": [ + { + "role": "user", + "content": [ + "Date: 2026-06-15", + "OPENER HINT: Open with what this means for the user today, then the number behind it.", + "Write one short assessment of this person's sleep duration. Open with what it MEANS in plain words — the read, not the number (e.g. \"running a little calmer than usual\", \"steady and right where it's been\") — then bring in ONE concrete number from the snapshot right after as support, placed against their own weekly/monthly baseline; never lead with the value. Close with one doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the measurement count and recency.", + "", + "PREVIOUS ANALYSIS: none on file. Treat this as the user's first analysis for this domain — no improvement/regression delta to surface.", + "", + "", + "VARIETY: to keep your cards from reading the same, you may open this one by leading with the recent direction of travel (rising / falling / flat vs the baseline) — only if the data genuinely supports that angle; never force it, never invent a trend.", + "", + "DATA STRENGTH: 89 day-buckets in the window, latest reading 1 day ago.", + "", + "REPETITION: you have already given a similar \"no material change\" assessment for this metric 3 time(s) in a row. Do NOT restate the same level again. Acknowledge the continuity in one short clause (\"still holding steady, 4 checks running\") and then pivot to a DIFFERENT facet — consistency, time-of-day pattern, or a correlated metric — or, if nothing is genuinely actionable, say so plainly and skip the manufactured step.", + "", + "RELATIONS (statistically-screened associations in YOUR data involving this metric — descriptive, NEVER causal):", + "- Higher sleep duration looks like it goes with higher next-day weight in your data, on the evidence so far. It is a pattern to watch, not a cause. (n=45, r=0.98)", + "- Higher blood glucose looks like it goes with higher next-day sleep duration in your data, on the evidence so far. It is a pattern to watch, not a cause. (n=22, r=0.95)", + "You MAY weave at most ONE of these in if it adds genuine insight, phrased as an \"association\" / \"tends to go with\" — never as a cause, and never alter the numbers. Skip them if the metric's own finding is the stronger story.", + "", + "", + "INTERPRETATION CONTEXT (guideline bands — computed server-side; state, do NOT recompute):", + "- Current value 423.85 min is in the \"the recommended adult range\" band, and sits near the LOWER boundary of this band (nearest boundary at 420 min).", + "- Guideline bands (NSF 2015; AASM/SRS 2015; the middle band is the target — both extremes are less favourable):", + " - below 420 min: below the recommended adult range", + " - 420–540 min: the recommended adult range", + " - 540 min and above: above the recommended adult range", + "- Values in the \"the recommended adult range\" band are considered a healthy / low-risk range per these guidelines.", + "- Name where the value sits and what that band means in plain words. Judge any trend BY this position: a shift deep inside a favourable band is a footnote; the same shift approaching or crossing a boundary is the headline. Frame consequence without diagnosis (\"values in this range are considered …\", never \"you have / are at risk of …\").", + "", + "", + "{\"locale\":\"en\",\"promptVersion\":\"6.3.0\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"SLEEP_DURATION\",\"metric\":{\"id\":\"SLEEP_DURATION\",\"displayName\":\"Sleep duration\",\"unit\":\"min\",\"direction\":\"target-band\",\"normalRange\":{\"low\":420,\"high\":540}},\"profile\":{\"ageYears\":42,\"sex\":\"MALE\"},\"dataCoverage\":{\"totalMeasurements\":89,\"totalSpanDays\":88,\"newestMeasurementDaysAgo\":1},\"SLEEP_DURATION\":{\"signal\":{\"metric\":\"Sleep duration\",\"unit\":\"min\",\"current\":423.85,\"currentWindowDays\":20,\"baseline\":404.11,\"baselineLabel\":\"your recent-weeks average\",\"delta\":19.74,\"deltaPct\":4.9,\"spread\":25.3,\"outsideNormalSwing\":false,\"direction\":\"target-band\",\"n\":20,\"newestDaysAgo\":1,\"normalRange\":{\"low\":420,\"high\":540},\"placement\":\"in band\"},\"summary\":{\"points\":89,\"start\":417,\"end\":380,\"delta\":-37,\"mean\":410.85,\"min\":375,\"max\":445},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":384,\"max\":384,\"mean\":384,\"n\":1},{\"date\":\"2026-05-27\",\"min\":389,\"max\":389,\"mean\":389,\"n\":1},{\"date\":\"2026-05-28\",\"min\":395,\"max\":395,\"mean\":395,\"n\":1},{\"date\":\"2026-05-29\",\"min\":401,\"max\":401,\"mean\":401,\"n\":1},{\"date\":\"2026-05-30\",\"min\":408,\"max\":408,\"mean\":408,\"n\":1},{\"date\":\"2026-05-31\",\"min\":415,\"max\":415,\"mean\":415,\"n\":1},{\"date\":\"2026-06-01\",\"min\":422,\"max\":422,\"mean\":422,\"n\":1},{\"date\":\"2026-06-02\",\"min\":428,\"max\":428,\"mean\":428,\"n\":1},{\"date\":\"2026-06-03\",\"min\":434,\"max\":434,\"mean\":434,\"n\":1},{\"date\":\"2026-06-04\",\"min\":438,\"max\":438,\"mean\":438,\"n\":1},{\"date\":\"2026-06-05\",\"min\":442,\"max\":442,\"mean\":442,\"n\":1},{\"date\":\"2026-06-06\",\"min\":444,\"max\":444,\"mean\":444,\"n\":1},{\"date\":\"2026-06-07\",\"min\":445,\"max\":445,\"mean\":445,\"n\":1},{\"date\":\"2026-06-08\",\"min\":444,\"max\":444,\"mean\":444,\"n\":1},{\"date\":\"2026-06-09\",\"min\":443,\"max\":443,\"mean\":443,\"n\":1},{\"date\":\"2026-06-10\",\"min\":439,\"max\":439,\"mean\":439,\"n\":1},{\"date\":\"2026-06-11\",\"min\":435,\"max\":435,\"mean\":435,\"n\":1},{\"date\":\"2026-06-12\",\"min\":430,\"max\":430,\"mean\":430,\"n\":1},{\"date\":\"2026-06-13\",\"min\":424,\"max\":424,\"mean\":424,\"n\":1},{\"date\":\"2026-06-14\",\"min\":417,\"max\":417,\"mean\":417,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":375,\"max\":380,\"mean\":376.6,\"n\":5},{\"weekISO\":\"2026-W13\",\"min\":379,\"max\":414,\"mean\":394.86,\"n\":7},{\"weekISO\":\"2026-W14\",\"min\":421,\"max\":445,\"mean\":435.57,\"n\":7},{\"weekISO\":\"2026-W15\",\"min\":418,\"max\":445,\"mean\":434,\"n\":7},{\"weekISO\":\"2026-W16\",\"min\":378,\"max\":411,\"mean\":392.57,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":375,\"max\":391,\"mean\":380.43,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":397,\"max\":436,\"mean\":417.14,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":438,\"max\":445,\"mean\":442.29,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":394,\"max\":433,\"mean\":413.71,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":375,\"max\":388,\"mean\":379,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":379,\"max\":379,\"mean\":379,\"n\":1}],\"monthly\":[],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":1,\"value\":417}}}" + ] + } + ], + "temperature": 0.45, + "maxTokens": 700, + "seed": 1618033, + "responseFormat": "json", + "timeoutMs": 60000 + } + ], + "enqueued": [], + "result": { + "hasProvider": true, + "text": "Parity note for fresh-en.", + "cached": false, + "updatedAt": "2026-06-15T10:00:00.000Z" + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "5df7980e558674aefbfe894525e4d2721f281a3dcf776465d7cc1ba0983b42fd", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "e348f34b369dce59f42c06c8bcf06a163f86dc61913f8af96d3bb5a49e55f4f6", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:SLEEP_DURATION", + "locale": "en", + "text": "Parity note for fresh-en.", + "items": null, + "inputHash": null, + "snapshotHash": "42e3295479aedb8d71869250de66ec31d8d36bbf3b2243a5f20a4b0b36dd7849", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + } + ] + }, + "SLEEP_DURATION cached-en": { + "calls": [], + "enqueued": [], + "result": { + "hasProvider": true, + "text": "Parity note for fresh-en.", + "cached": true, + "updatedAt": "2026-06-15T10:00:00.000Z" + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "5df7980e558674aefbfe894525e4d2721f281a3dcf776465d7cc1ba0983b42fd", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "e348f34b369dce59f42c06c8bcf06a163f86dc61913f8af96d3bb5a49e55f4f6", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:SLEEP_DURATION", + "locale": "en", + "text": "Parity note for fresh-en.", + "items": null, + "inputHash": null, + "snapshotHash": "42e3295479aedb8d71869250de66ec31d8d36bbf3b2243a5f20a4b0b36dd7849", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + } + ] + }, + "SLEEP_DURATION forced-unchanged-en": { + "calls": [], + "enqueued": [], + "result": { + "hasProvider": true, + "text": "Parity note for fresh-en.", + "cached": true, + "updatedAt": "2026-06-15T10:00:00.000Z" + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "5df7980e558674aefbfe894525e4d2721f281a3dcf776465d7cc1ba0983b42fd", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "e348f34b369dce59f42c06c8bcf06a163f86dc61913f8af96d3bb5a49e55f4f6", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:SLEEP_DURATION", + "locale": "en", + "text": "Parity note for fresh-en.", + "items": null, + "inputHash": null, + "snapshotHash": "42e3295479aedb8d71869250de66ec31d8d36bbf3b2243a5f20a4b0b36dd7849", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + } + ] + }, + "SLEEP_DURATION next-day-en": { + "calls": [ + { + "system": [ + "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", + "", + "YOUR DATA (graded snapshot):", + "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", + "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", + "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", + "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", + "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", + "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", + "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", + "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", + "", + "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", + "Moves available (choose the few that genuinely apply):", + "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", + "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", + "(c) name a genuine, earned win or a steady streak when the data shows one;", + "(d) one watch-item worth keeping an eye on;", + "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", + "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", + "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", + "", + "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", + "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", + "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", + "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", + "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", + "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", + "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", + "", + "OPENING — lead with meaning, not the metric value", + "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", + "", + "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", + "", + "JUDGE THE DATA HONESTLY (never invent a trend):", + "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", + "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", + "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", + "", + "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", + "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", + "", + "METRIC IDENTIFIERS STAY OUT OF PROSE", + "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", + "", + "GLP-1 DOSE SAFETY (contract, not style)", + "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", + "", + "ACUTE RED FLAGS (contract, not style)", + "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", + "", + "GROUNDING — ZERO HALLUCINATIONS", + "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", + "", + "TONE — a warm, motivating advisor, never clinical or alarming", + "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", + "", + "NOT AN INSIGHT — never recite mechanics", + "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", + "", + "INTERPRET, DON'T ENUMERATE", + "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", + "", + "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", + "", + "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", + "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", + "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", + "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", + "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", + "", + "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in English: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", + "{ \"summary\": \"...\" }", + "", + "ARCHETYPE — SLEEP:", + "- This is nightly sleep duration. The snapshot's values are in minutes; convert to hours for your prose (e.g. 450 min = 7.5 h). Assess both the typical duration and its CONSISTENCY: a stable ~7–9 h is the favourable pattern; high night-to-night variability is itself worth naming even when the mean looks fine.", + "- Compare the recent nights to the user's own weekly/monthly baseline. A reference band of 7–9 h is a coarse adult guide, not a verdict — short or long sleepers vary.", + "- Close with one doable step toward more consistent or sufficient sleep ONLY when the finding implies one — never the forbidden 'get enough sleep' platitude; be specific (e.g. a steadier wind-down time). When sleep is already consistent and sufficient, affirm it and name one thing worth keeping an eye on instead of manufacturing a step.", + "", + "METRIC: Sleep duration (unit: min).", + "Neither extreme is good — the value should sit inside its normal band; deviations in either direction are worth noting.", + "Coarse population reference band: 420–540 min (a placement aid only — the user's OWN baseline leads)." + ], + "messages": [ + { + "role": "user", + "content": [ + "Date: 2026-06-16", + "OPENER HINT: Open by contrasting now against the user's own baseline.", + "Write one short assessment of this person's sleep duration. Open with what it MEANS in plain words — the read, not the number (e.g. \"running a little calmer than usual\", \"steady and right where it's been\") — then bring in ONE concrete number from the snapshot right after as support, placed against their own weekly/monthly baseline; never lead with the value. Close with one doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the measurement count and recency.", + "", + "PREVIOUS ANALYSIS (1 day ago, 2026-06-15):", + "Parity note for fresh-en.", + "", + "INSTRUCTION: When summarising the current state, explicitly call out what improved, what regressed, and what stayed the same compared to that previous snapshot. Use phrases like \"down 4 mmHg from your last check\" or \"same elevated trend as last week\". Do not invent numbers — only compare what's in the snapshot below.", + "", + "", + "VARIETY: to keep your cards from reading the same, you may open this one by leading with the recent direction of travel (rising / falling / flat vs the baseline) — only if the data genuinely supports that angle; never force it, never invent a trend.", + "", + "DATA STRENGTH: 89 day-buckets in the window, latest reading 2 days ago.", + "", + "REPETITION: you have already given a similar \"no material change\" assessment for this metric 3 time(s) in a row. Do NOT restate the same level again. Acknowledge the continuity in one short clause (\"still holding steady, 4 checks running\") and then pivot to a DIFFERENT facet — consistency, time-of-day pattern, or a correlated metric — or, if nothing is genuinely actionable, say so plainly and skip the manufactured step.", + "", + "RELATIONS (statistically-screened associations in YOUR data involving this metric — descriptive, NEVER causal):", + "- Higher sleep duration looks like it goes with higher next-day weight in your data, on the evidence so far. It is a pattern to watch, not a cause. (n=45, r=0.98)", + "- Higher blood glucose looks like it goes with higher next-day sleep duration in your data, on the evidence so far. It is a pattern to watch, not a cause. (n=22, r=0.95)", + "You MAY weave at most ONE of these in if it adds genuine insight, phrased as an \"association\" / \"tends to go with\" — never as a cause, and never alter the numbers. Skip them if the metric's own finding is the stronger story.", + "", + "", + "INTERPRETATION CONTEXT (guideline bands — computed server-side; state, do NOT recompute):", + "- Current value 425.95 min is in the \"the recommended adult range\" band, and sits near the LOWER boundary of this band (nearest boundary at 420 min).", + "- Guideline bands (NSF 2015; AASM/SRS 2015; the middle band is the target — both extremes are less favourable):", + " - below 420 min: below the recommended adult range", + " - 420–540 min: the recommended adult range", + " - 540 min and above: above the recommended adult range", + "- Values in the \"the recommended adult range\" band are considered a healthy / low-risk range per these guidelines.", + "- Name where the value sits and what that band means in plain words. Judge any trend BY this position: a shift deep inside a favourable band is a footnote; the same shift approaching or crossing a boundary is the headline. Frame consequence without diagnosis (\"values in this range are considered …\", never \"you have / are at risk of …\").", + "", + "", + "{\"locale\":\"en\",\"promptVersion\":\"6.3.0\",\"generatedForDay\":\"2026-06-16\",\"focus\":\"SLEEP_DURATION\",\"metric\":{\"id\":\"SLEEP_DURATION\",\"displayName\":\"Sleep duration\",\"unit\":\"min\",\"direction\":\"target-band\",\"normalRange\":{\"low\":420,\"high\":540}},\"profile\":{\"ageYears\":42,\"sex\":\"MALE\"},\"dataCoverage\":{\"totalMeasurements\":89,\"totalSpanDays\":88,\"newestMeasurementDaysAgo\":2},\"SLEEP_DURATION\":{\"signal\":{\"metric\":\"Sleep duration\",\"unit\":\"min\",\"current\":425.95,\"currentWindowDays\":19,\"baseline\":404.33,\"baselineLabel\":\"your recent-weeks average\",\"delta\":21.61,\"deltaPct\":5.3,\"spread\":25.06,\"outsideNormalSwing\":false,\"direction\":\"target-band\",\"n\":19,\"newestDaysAgo\":2,\"normalRange\":{\"low\":420,\"high\":540},\"placement\":\"in band\"},\"summary\":{\"points\":89,\"start\":417,\"end\":380,\"delta\":-37,\"mean\":410.85,\"min\":375,\"max\":445},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":389,\"max\":389,\"mean\":389,\"n\":1},{\"date\":\"2026-05-28\",\"min\":395,\"max\":395,\"mean\":395,\"n\":1},{\"date\":\"2026-05-29\",\"min\":401,\"max\":401,\"mean\":401,\"n\":1},{\"date\":\"2026-05-30\",\"min\":408,\"max\":408,\"mean\":408,\"n\":1},{\"date\":\"2026-05-31\",\"min\":415,\"max\":415,\"mean\":415,\"n\":1},{\"date\":\"2026-06-01\",\"min\":422,\"max\":422,\"mean\":422,\"n\":1},{\"date\":\"2026-06-02\",\"min\":428,\"max\":428,\"mean\":428,\"n\":1},{\"date\":\"2026-06-03\",\"min\":434,\"max\":434,\"mean\":434,\"n\":1},{\"date\":\"2026-06-04\",\"min\":438,\"max\":438,\"mean\":438,\"n\":1},{\"date\":\"2026-06-05\",\"min\":442,\"max\":442,\"mean\":442,\"n\":1},{\"date\":\"2026-06-06\",\"min\":444,\"max\":444,\"mean\":444,\"n\":1},{\"date\":\"2026-06-07\",\"min\":445,\"max\":445,\"mean\":445,\"n\":1},{\"date\":\"2026-06-08\",\"min\":444,\"max\":444,\"mean\":444,\"n\":1},{\"date\":\"2026-06-09\",\"min\":443,\"max\":443,\"mean\":443,\"n\":1},{\"date\":\"2026-06-10\",\"min\":439,\"max\":439,\"mean\":439,\"n\":1},{\"date\":\"2026-06-11\",\"min\":435,\"max\":435,\"mean\":435,\"n\":1},{\"date\":\"2026-06-12\",\"min\":430,\"max\":430,\"mean\":430,\"n\":1},{\"date\":\"2026-06-13\",\"min\":424,\"max\":424,\"mean\":424,\"n\":1},{\"date\":\"2026-06-14\",\"min\":417,\"max\":417,\"mean\":417,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":375,\"max\":380,\"mean\":376.6,\"n\":5},{\"weekISO\":\"2026-W13\",\"min\":379,\"max\":414,\"mean\":394.86,\"n\":7},{\"weekISO\":\"2026-W14\",\"min\":421,\"max\":445,\"mean\":435.57,\"n\":7},{\"weekISO\":\"2026-W15\",\"min\":418,\"max\":445,\"mean\":434,\"n\":7},{\"weekISO\":\"2026-W16\",\"min\":378,\"max\":411,\"mean\":392.57,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":375,\"max\":391,\"mean\":380.43,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":397,\"max\":436,\"mean\":417.14,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":438,\"max\":445,\"mean\":442.29,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":394,\"max\":433,\"mean\":413.71,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":375,\"max\":388,\"mean\":379,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":379,\"max\":384,\"mean\":381.5,\"n\":2}],\"monthly\":[],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":2,\"value\":417}}}" + ] + } + ], + "temperature": 0.45, + "maxTokens": 700, + "seed": 1618033, + "responseFormat": "json", + "timeoutMs": 60000 + } + ], + "enqueued": [], + "result": { + "hasProvider": true, + "text": "Parity note for next-day-en.", + "cached": false, + "updatedAt": "2026-06-16T10:00:00.000Z" + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "5df7980e558674aefbfe894525e4d2721f281a3dcf776465d7cc1ba0983b42fd", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "e348f34b369dce59f42c06c8bcf06a163f86dc61913f8af96d3bb5a49e55f4f6", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:SLEEP_DURATION", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "6075377b17914e90f2c3beb7ecf88d8bd454427d0d32ab205552f79a05dd3c94", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + } + ] + }, + "SLEEP_DURATION read-only-stale": { + "calls": [], + "enqueued": [ + { + "userId": "parity-rich", + "metric": "metric:SLEEP_DURATION", + "locale": "en" + } + ], + "result": { + "hasProvider": true, + "text": "Parity note for next-day-en.", + "cached": true, + "updatedAt": "2026-06-16T10:00:00.000Z", + "preparing": false, + "revalidating": true + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "5df7980e558674aefbfe894525e4d2721f281a3dcf776465d7cc1ba0983b42fd", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "e348f34b369dce59f42c06c8bcf06a163f86dc61913f8af96d3bb5a49e55f4f6", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:SLEEP_DURATION", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "6075377b17914e90f2c3beb7ecf88d8bd454427d0d32ab205552f79a05dd3c94", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + } + ] + }, + "SLEEP_DURATION prose-de": { + "calls": [ + { + "system": [ + "Du schreibst eine kurze, persönliche Einschätzung zu EINER Gesundheitsmetrik dieses Nutzers. Du kennst seine Daten und ordnest sie gegen seine EIGENE Baseline ein — nicht gegen Bevölkerungsnormen. Deine fachliche Grundlage sind anerkannte Leitlinien (ESH 2023, WHO, AASM, DGE, DEGAM); du beziehst dich darauf, ohne zu diagnostizieren.", + "", + "DEINE DATENGRUNDLAGE (graded snapshot):", + "- signal: der fertige Vergleich — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` ist das recent-Mittel; `baseline` ist der EIGENE längere Durchschnitt; `delta`/`deltaPct` sind die VORZEICHENBEHAFTETE Änderung gegenüber dieser Baseline; `spread` ist die normale Schwankung; `outsideNormalSwing` ist das vorab berechnete Urteil, ob die Änderung diese Schwankung übersteigt. Diese Werte sind bereits berechnet — nenne sie, rechne sie NICHT neu. `contributors[]` (nur Score-Komposite) benennt die 1–2 Teil-Scores, die den Score bewegt haben.", + "- summary: { points, start, end, delta, mean, min, max } — die Eckwerte des gesamten Lesefensters.", + "- series.recent[]: die jüngsten ~21 Tage, je ein Eintrag pro Tag ({ date, min, max, mean, n }).", + "- series.weekly[]: die ~10 Wochen davor, je ein ISO-Wochen-Aggregat.", + "- series.monthly[]: die ~12 Monate davor, je ein Monatsaggregat.", + "- series.yearly[]: alles ältere, je ein Jahr ({ year, mean, min, max, n, slope }).", + "- Die jüngsten Tage liegen also EINZELN vor, ältere Zeiträume als Wochen-, Monats- und Jahresaggregate. Ziehe die series nur für einen konkreten Tag heran; führe sonst mit dem signal-Block.", + "- Ein Slice kann leer sein, wenn die Historie kurz ist. Dann nicht so tun, als gäbe es ihn. Ist `signal.baseline` null, gibt es noch keine etablierte Baseline; sage das ehrlich, statt einen Vergleich zu erfinden.", + "", + "SO BAUST DU DIE EINSCHÄTZUNG — wähle die 2–3 BAUSTEINE, die zu DIESEN Daten passen, und ordne sie so, dass sie als EIN zusammenhängender Gedanke lesen, keine feste Checkliste. Führe gemäß dem OPENER-HINWEIS im User-Prompt, wenn einer mitgegeben ist — eröffne NICHT immer mit der Zahl. Variiere die Form von Metrik zu Metrik; eröffne nie zwei Metriken hintereinander gleich.", + "Verfügbare Bausteine (nimm die wenigen, die wirklich zutreffen):", + "(a) der aktuelle Befund — `current` aus dem signal-Block (oder ein Wert aus series.recent für eine Tagesfrage);", + "(b) gegen die EIGENE Baseline einordnen über das vorab berechnete vorzeichenbehaftete `delta` gegenüber `baselineLabel` — nenne es, leite es nicht neu her. Behandle `outsideNormalSwing: false` als \"innerhalb der üblichen Schwankung — kein Befund\" und ERFINDE KEINEN Trend; `outsideNormalSwing: true` ist die echte, meldenswerte Änderung. Ein `normalRange`/`placement` ist nur ein grober sekundärer Anker — das persönliche delta führt;", + "(c) einen echten, verdienten Erfolg oder eine stabile Serie benennen, wenn die Daten ihn zeigen;", + "(d) einen Punkt zum Im-Auge-behalten;", + "(e) einen Zusammenhang NUR, wenn ein r-Wert vorhanden und |r| > 0,4 ist, formuliert als \"bewegt sich mit\" / \"zeigt sich oft zusammen mit\", nie kausal;", + "(f) GENAU EINEN machbaren nächsten Schritt — und nur, wenn der Befund wirklich einen hergibt. Eine Botschaft = ein Verhalten, keine Liste. Ist der Wert stabil und im grünen Bereich, ERZWINGE KEINEN Schritt — bestätige kurz und nenne stattdessen einen Punkt zum Beobachten. Ein erfundener Schritt ist genau die Floskel, die wir vermeiden.", + "Synthese statt Aufzählung: die Geschichte dessen, was die Daten bedeuten, zählt mehr als das Wiederholen jeder Zahl.", + "", + "TONALITÄT — ein warmer, motivierender Premium-Begleiter (jemand hat sich die Daten dieser Person wirklich angesehen und etwas Wertvolles für sie gefunden):", + "- Zweite Person (\"dein Blutdruck\", \"deine Werte\"), warm, direkt, ehrlich.", + "- Motivierend und vorwärtsgewandt: Wenn die Daten es hergeben, benenne den echten Erfolg klar und baue ein wenig Schwung auf — die Person soll sich gesehen und unterstützt fühlen, nicht belehrt. Die Ermutigung muss durch die Zahlen VERDIENT sein, nie ein reflexhaftes Kompliment.", + "- Autonomie-unterstützend: \"kann helfen\", \"einen Versuch wert\", nie \"du musst\".", + "- Nie alarmierend, nie moralisierend, nie diagnostisch — keine Krankheitsbehauptung. Formuliere als begründete Beobachtung, nicht als ärztlichen Rat.", + "- Auch ungünstige Werte ehrlich benennen: Befund → gegen die eigene Baseline einordnen → ein kleiner machbarer Schritt, als Chance formuliert, nicht als Versagen. Nicht verharmlosen, nicht dramatisieren.", + "- Keine Floskeln und keine bloße Zahlenwiederholung: jede warme Zeile ist an eine echte Zahl oder eine echte Veränderung geknüpft. Ein generischer Positiv-Einstieg (\"Deine Werte sehen gut aus\") ist verboten — verdiene die Ermutigung mit dem konkreten Befund.", + "", + "ERÖFFNUNG — mit der Bedeutung führen, nicht mit dem Messwert", + "Beginne mit dem Eindruck in klaren Worten — was das heute über die Person sagt — und bring die Zahl direkt danach als Beleg, nie als Erstes. Eine Zeile, die mit einem Wert eröffnet (\"Dein Ruhepuls ist 61 …\"), liest sich wie ein Ablesen; dieselbe Zeile, die mit der Bedeutung eröffnet (\"Dein Herz läuft diese Woche etwas ruhiger — rund 61 …\"), liest sich wie ein Begleiter, der wirklich hingesehen hat. Jede Aussage bleibt an einer echten Zahl verankert; nur die REIHENFOLGE ändert sich, Bedeutung zuerst und die Zahl direkt dahinter. Das gilt, ob die Fläche eine Metrik abdeckt oder viele.", + "", + "LÄNGE: 2-4 Sätze, ca. 30-60 Wörter — ist der Wert aber stabil und nichts zu tun, ist EIN knapper Satz besser, als ihn auf Länge zu strecken. Knapp und hochwertig. Keine bloße Zahlenwiederholung, kein Fülltext, keine generischen Floskeln.", + "", + "DATENLAGE EHRLICH BEWERTEN (nie einen Trend erfinden):", + "- Nur wenige Messpunkte/Einträge/Ereignisse oder ein leerer recent-Slice: ehrlich sagen, dass es für eine belastbare Tendenz noch zu wenige Daten sind, und EINEN Hinweis geben (z.B. regelmäßiger erfassen). Keinen Trend behaupten. Was \"wenige\" bedeutet, hängt von der Metrik ab — bei Stimmung sind es Einträge, bei der Einnahmetreue geplante Dosen.", + "- Jüngste Messung deutlich älter als ~7 Tage (dataCoverage.newestMeasurementDaysAgo): darauf hinweisen, dass die Werte nicht mehr aktuell sind.", + "- Korrelationen NUR erwähnen, wenn der r-Wert im Snapshot vorhanden ist und |r| > 0.4 ist. Fehlt das Feld, keine Korrelation interpretieren oder erfinden. Immer als \"Zusammenhang\" formulieren, nie als \"Ursache\".", + "", + "VERBOTENE FLOSKELN (signalisieren ungegroundeten Fülltext — nie ausgeben, außer im Disclaimer)", + "\"achte auf ausreichend Schlaf\", \"trinke genug Wasser\", \"regelmäßige Bewegung\", \"ärztlicher Rat empfohlen\".", + "", + "METRIK-IDENTIFIER GEHÖREN NICHT IN DEN FLIESSTEXT", + "Schreibe niemals Datenbank- bzw. Enum-Namen wie \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" oder \"SLEEP_DURATION\" in nutzersichtbare Strings, und schreibe nie das wörtliche \"metric:\"-Chart-Token in den Fließtext. Verweise auf jede Metrik mit der natürlichsprachlichen Bezeichnung, die der Nutzer sieht — \"deine Systole\", \"dein Gewicht\", \"dein Puls\", \"deine Stimmung\", \"deine Medikamentenadhärenz\", \"dein Ruhepuls\", \"deine Schlafdauer\", \"deine Schritte\".", + "", + "GLP-1-DOSIS-SICHERHEIT (Vertrag, kein Stil)", + "Verschreibe, empfiehl oder ändere NIEMALS Medikamenten-Dosen, auch wenn der Snapshot einen GLP-1-Rezeptoragonisten benennt (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). Du darfst den benannten Wirkstoff und die aktuelle Titrationsstufe NENNEN (\"Woche 3 auf 7,5 mg\"), wenn der Snapshot sie trägt, aber schreibe nie \"erhöhe auf X mg\", \"erwäge die nächste Stufe Y mg\", \"bleibe auf Z mg\" oder eine Variante davon. Ein Plateau rahmt die nächste Entscheidung immer als Gespräch mit der behandelnden Ärztin. Bist du unsicher, ob etwas dosis-präskriptiv ist, behandle es so — und verweise an die Klinik.", + "", + "AKUTE WARNZEICHEN (Vertrag, kein Stil)", + "Beschreibt der Nutzer ein akutes Krisenzeichen — Brustschmerz oder Druck auf der Brust, Ohnmacht oder Beinahe-Ohnmacht (Synkope), ein plötzliches schweres Symptom (z. B. stärkster Kopfschmerz aller Zeiten, plötzliche Schwäche oder Sprachstörung, Atemnot), einen hypertensiven Notfallwert (systolisch ≥ 180 oder diastolisch ≥ 120 mit Symptomen), einen schwer-niedrigen oder sehr hohen Glukosewert (unter 54 mg/dL (3,0 mmol/L), oder unter 70 mg/dL (3,9 mmol/L) mit Symptomen, oder ≥ 250 mg/dL (13,9 mmol/L) mit Symptomen), anhaltendes Fieber ≥ 38,5 °C (101,3 °F) oder Gedanken an Suizid bzw. Selbstverletzung — sage in einem ruhigen Satz klar, dass das jetzt umgehende ärztliche Hilfe oder den Notruf braucht, und coache es NICHT anhand der Daten. Diagnostiziere nicht, benenne keine Erkrankung, schätze keinen Schweregrad aus den Zahlen — verweise nur auf umgehende Hilfe bzw. den Notruf und höre auf. Das ist eine geschlossene Liste; außerhalb davon bleibe wie gewohnt ruhig und nicht alarmierend.", + "", + "ERDUNG — NULL HALLUZINATIONEN", + "Jede Aussage muss auf einer Zahl beruhen, die im übergebenen Snapshot sichtbar ist. Lässt sich die Aussage keinem Snapshot-Feld zuordnen, lass sie weg und erfinde nie einen Messwert, um einer Anfrage zu entsprechen. Ordne jeden Wert gegen die EIGENE Baseline des Nutzers ein (jüngeres vs. längeres Mittel), nie gegen eine Bevölkerungsnorm. Nenne einen Zusammenhang nur, wenn der r-Wert vorhanden und |r| > 0,4 ist — immer als \"Zusammenhang\", nie als \"Ursache\".", + "", + "TONALITÄT — ein warmer, motivierender Begleiter, nie klinisch oder alarmierend", + "Schreibe in der zweiten Person, warm und direkt. Wenn die Daten es hergeben, benenne den echten Erfolg klar und baue ein wenig Schwung auf — die Person soll sich gesehen und unterstützt fühlen, nicht belehrt. Die Ermutigung muss durch die Zahlen VERDIENT sein, nie ein reflexhaftes Kompliment. Lob ist verankert, konkret und angemessen: an einer echten Zahl oder Veränderung festgemacht, nicht auf andere Nutzer übertragbar, und stiller Anerkennung für stille Erfolge. Über-Bestätigung ist eine Sicherheitsregression, keine Wärme — bestätige nie einen sich verschlechternden Trend oder eine unsichere Entscheidung; würdige die Mühe, nicht die Entscheidung, und bleib neutral (\"nichts zu tun — die gute Art von langweilig\"), wenn es nichts zu loben gibt, statt ein Kompliment zu erfinden. Sei autonomie-unterstützend (\"einen Versuch wert\", \"kann helfen\", nie \"du musst\"). Benenne auch ungünstige Werte ehrlich — Befund, dann gegen die eigene Baseline einordnen, dann ein kleiner machbarer Schritt, als Chance formuliert. Nie alarmierend, nie moralisierend, nie diagnostisch. Keine Floskeln und keine bloße Zahlenwiederholung. Beginne NICHT mit einem Kompliment über Datenmenge oder -qualität; verbotene Eröffnungen sind u.a. \"Datengrundlage ist sehr stark\", \"Your data foundation is strong\", \"Du hast eine solide Baseline\", \"Großartiger Datensatz\", ein generisches \"Deine Werte sehen gut aus\" und jede sinngemäße Umformulierung.", + "", + "KEINE EINSCHÄTZUNG — nie Mechanik nacherzählen", + "Eine Messanzahl (\"3-mal erfasst\"), ein Erfassungsrhythmus (\"alle paar Tage gemessen\") oder Bedien-Mechanik ist KEINE Einschätzung und gehört für sich genommen nicht in den Text. Nutze eine solche Angabe NUR, wenn sie eine Konsequenz trägt, und nenne dann die Konsequenz, nicht die Zahl: z. B. \"diese Woche zu wenige Werte für eine Tendenz — eine Morgenmessung würde es klären\", nicht \"du hast diese Woche zweimal gemessen\". Jeder Satz muss sich verdienen, indem er sagt, was ein Wert BEDEUTET oder was daraus folgt — nie, indem er erzählt, wie die Daten erhoben wurden.", + "", + "EINORDNEN, NICHT AUFZÄHLEN", + "Sag, was ein Wert BEDEUTET, nicht nur, was er ist. Trägt der User-Prompt einen EINORDNUNGS-KONTEXT-Block, nutze ihn: benenne, wo der Wert auf der Leitlinien-Skala liegt, was dieses Band in klaren, würdevollen Worten bedeutet und was praktisch folgt. Beurteile den Trend IM KONTEXT — ein \"leicht steigender\" Wert tief in einem günstigen Band ist eine Randnotiz; dieselbe Bewegung an oder nahe einer Grenze ist die Schlagzeile. Rahme jede Konsequenz OHNE Diagnose: \"Werte in diesem Bereich gelten laut Leitlinien als …\", nie \"du hast X\" oder \"du bist gefährdet für X\". Ist KEIN Einordnungs-Block vorhanden, hat die Metrik kein allgemeines Referenzband — ordne sie gegen die EIGENE Baseline der Person ein und sage klar, dass kein allgemeines Referenzband hinterlegt ist, statt eines zu erfinden.", + "", + "FORMATIERUNG — schreibe in kurzen Absätzen, getrennt durch eine LEERZEILE, je 1–3 Sätze; eine längere Antwort hat 2–4 Absätze, nie einen Block. Setze eine Leerzeile zwischen unterschiedliche Gedanken (z. B. \"wie es steht\" vs. \"eine Sache zum Ausprobieren\"). Ein stabiler Einzeiler bleibt EIN Absatz — strecke nicht auf einen zweiten. Wenn du wirklich drei oder mehr parallele Punkte aufzählst (Optionen, Befunde, Schritte), formatiere sie als schlichte Liste: ein Punkt pro Zeile, jede Zeile beginnt mit \"- \". Die EINE wichtigste Kernaussage darfst du mit **doppelten Sternchen** fett setzen, höchstens einmal pro Antwort. Sonst nichts: keine Überschriften, kein Kursiv, keine Backticks, keine nummerierten Listen, keine Emojis.", + "", + "BEISPIELE — beachte die UNTERSCHIEDLICHEN Formen (urteil-zuerst, trend-zuerst, Einzeiler). Sie illustrieren nur Form und Erdung; übernimm sie nie — jede Einschätzung nutzt die echten Snapshot-Zahlen:", + "- URTEIL-ZUERST (Bedeutung zuerst, Zahl als Beleg, ein Schritt): \"Dein Ruhepuls läuft gerade einen Tick niedriger als sonst — diese Woche rund 61 bpm, 5 unter deinem Monatsmittel von 66 und dein niedrigster seit Wochen. So ein Rückgang passt meist zu mehr Bewegung; die Routine, die das gebracht hat, lohnt sich beizubehalten.\"", + "- TREND-ZUERST (Richtung zuerst, dann der Stand): \"Dein systolischer Blutdruck geht seit drei Wochen ruhig nach unten, aktuell rund 124 mmHg — etwa 4 unter deinem 30-Tage-Schnitt. Nichts Dramatisches, einfach eine stetige Richtung.\"", + "- EINZEILER (stabil, KEIN erzwungener Schritt): \"Deine SpO₂ ist mit 97 % stabil und genau in deinem üblichen Bereich — nichts zu tun, die gute Art von langweilig.\"", + "- SCHLECHT (verbotene Floskel, ungegroundet — so NICHT): \"Deine Werte sehen gut aus. Achte auf ausreichend Schlaf und regelmäßige Bewegung.\"", + "", + "AUSGABEFORMAT: Antworte ausschließlich mit validem JSON in genau diesem Schema. Das Feld \"summary\" enthält die komplette Einschätzung auf Deutsch: 1-3 kurze Absätze mit je 1-3 Sätzen, getrennt durch eine Leerzeile als \\n\\n INNERHALB des JSON-Strings. Ein stabiler Einzeiler bleibt EIN Absatz:", + "{ \"summary\": \"...\" }", + "", + "ARCHETYP — SCHLAF:", + "- Das ist die nächtliche Schlafdauer. Die Snapshot-Werte sind in Minuten; rechne für deinen Text in Stunden um (z.B. 450 min = 7,5 h). Bewerte sowohl die typische Dauer als auch ihre KONSTANZ: stabile ~7–9 h sind das günstige Muster; hohe Schwankung von Nacht zu Nacht ist selbst dann erwähnenswert, wenn der Mittelwert passt.", + "- Vergleiche die jüngsten Nächte mit der eigenen Wochen-/Monats-Baseline. Ein Referenzband von 7–9 h ist eine grobe Erwachsenen-Orientierung, kein Urteil — Kurz- und Langschläfer variieren.", + "- Schließe NUR DANN mit einem machbaren Schritt zu konstanterem oder ausreichenderem Schlaf, wenn der Befund einen nahelegt — nie die verbotene Floskel 'achte auf ausreichend Schlaf'; sei konkret (z.B. eine festere Einschlafzeit). Ist der Schlaf bereits konstant und ausreichend, erkenne das an und nenne stattdessen einen Punkt, den man im Auge behalten kann, statt einen Schritt zu erzwingen.", + "", + "METRIK: Sleep duration (Einheit: min).", + "Kein Extrem ist gut — der Wert sollte im Normalband liegen; Abweichungen in beide Richtungen sind erwähnenswert.", + "Grobes Referenzband (Population): 420–540 min (nur Orientierung — die EIGENE Baseline führt)." + ], + "messages": [ + { + "role": "user", + "content": [ + "Datum: 2026-06-15", + "OPENER HINT: Eröffne damit, was das heute für den Nutzer bedeutet, und dann die Zahl dahinter.", + "Schreibe eine kurze Einschätzung zu Sleep duration dieser Person. Beginne mit der BEDEUTUNG in klaren Worten — dem Eindruck, nicht der Zahl (z. B. \"läuft gerade etwas ruhiger als sonst\", \"stabil und genau da, wo es war\") — und bring danach EINE konkrete Zahl aus dem Snapshot als Beleg, gegen die eigene Wochen-/Monats-Baseline eingeordnet; führe nie mit dem Wert. Schließe nur dann mit einem machbaren Schritt, wenn der Befund wirklich einen hergibt; ist nichts umsetzbar, lass den Schritt weg statt Fülltext zu erfinden. Konfidenz aus Messanzahl und Aktualität ableiten.", + "", + "VORHERIGE ANALYSE: keine vorhanden. Behandle dies als erste Analyse in diesem Bereich — kein Verbesserungs-/Verschlechterungs-Delta zu nennen.", + "", + "", + "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der jüngsten Richtung (steigend / fallend / flach gegenüber der Baseline) eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", + "", + "DATENLAGE: 89 Tages-Buckets im Fenster, letzter Wert vor 1 Tag.", + "", + "WIEDERHOLUNG: du hast für diese Metrik bereits 3-mal in Folge eine ähnliche \"keine wesentliche Änderung\"-Einschätzung gegeben. Wiederhole NICHT erneut dasselbe Niveau. Benenne die Kontinuität in einem kurzen Nebensatz (\"weiterhin stabil, 4 Checks in Folge\") und wende dich dann einem ANDEREN Aspekt zu — Konstanz, Tageszeit-Muster oder einer korrelierten Metrik — oder sage, falls nichts wirklich umsetzbar ist, dies klar und lass den erzwungenen Schritt weg.", + "", + "ZUSAMMENHÄNGE (statistisch geprüfte Assoziationen in DEINEN Daten, die diese Metrik betreffen — beschreibend, NIE kausal):", + "- Wenn die Schlafdauer höher liegt, scheint das Gewicht am Folgetag nach bisheriger Datenlage eher höher auszufallen. Das ist ein Muster, das Beachtung verdient, keine Ursache. (n=45, r=0.98)", + "- Wenn der Blutzucker höher liegt, scheint die Schlafdauer am Folgetag nach bisheriger Datenlage eher höher auszufallen. Das ist ein Muster, das Beachtung verdient, keine Ursache. (n=22, r=0.95)", + "Du DARFST höchstens EINEN davon einflechten, wenn er echten Mehrwert bringt — als \"Zusammenhang\" / \"geht tendenziell einher mit\" formuliert, nie als Ursache, und ohne die Zahlen zu verändern. Lass sie weg, wenn der eigene Befund der Metrik die stärkere Geschichte ist.", + "", + "", + "EINORDNUNGS-KONTEXT (Leitlinien-Bänder — serverseitig berechnet; nennen, NICHT neu rechnen):", + "- Der aktuelle Wert 423.85 min liegt im Band \"the recommended adult range\" und liegt nahe der UNTEREN Grenze dieses Bands (nächste Grenze bei 420 min).", + "- Leitlinien-Bänder (NSF 2015; AASM/SRS 2015; das mittlere Band ist das Ziel — beide Extreme sind ungünstiger):", + " - unter 420 min: below the recommended adult range", + " - 420–540 min: the recommended adult range", + " - ab 540 min: above the recommended adult range", + "- Werte im Band \"the recommended adult range\" gelten laut diesen Leitlinien als ein gesunder / risikoarmer Bereich.", + "- Benenne, wo der Wert liegt und was dieses Band praktisch bedeutet. Beurteile jeden Trend GEMÄSS dieser Position: eine Verschiebung tief in einem günstigen Band ist eine Randnotiz; dieselbe Verschiebung nahe an oder über einer Grenze ist die Schlagzeile. Rahme die Konsequenz ohne Diagnose (\"Werte in diesem Bereich gelten als …\", nie \"du hast / bist gefährdet für …\").", + "", + "", + "{\"locale\":\"de\",\"promptVersion\":\"6.3.0\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"SLEEP_DURATION\",\"metric\":{\"id\":\"SLEEP_DURATION\",\"displayName\":\"Sleep duration\",\"unit\":\"min\",\"direction\":\"target-band\",\"normalRange\":{\"low\":420,\"high\":540}},\"profile\":{\"ageYears\":42,\"sex\":\"MALE\"},\"dataCoverage\":{\"totalMeasurements\":89,\"totalSpanDays\":88,\"newestMeasurementDaysAgo\":1},\"SLEEP_DURATION\":{\"signal\":{\"metric\":\"Sleep duration\",\"unit\":\"min\",\"current\":423.85,\"currentWindowDays\":20,\"baseline\":404.11,\"baselineLabel\":\"your recent-weeks average\",\"delta\":19.74,\"deltaPct\":4.9,\"spread\":25.3,\"outsideNormalSwing\":false,\"direction\":\"target-band\",\"n\":20,\"newestDaysAgo\":1,\"normalRange\":{\"low\":420,\"high\":540},\"placement\":\"in band\"},\"summary\":{\"points\":89,\"start\":417,\"end\":380,\"delta\":-37,\"mean\":410.85,\"min\":375,\"max\":445},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":384,\"max\":384,\"mean\":384,\"n\":1},{\"date\":\"2026-05-27\",\"min\":389,\"max\":389,\"mean\":389,\"n\":1},{\"date\":\"2026-05-28\",\"min\":395,\"max\":395,\"mean\":395,\"n\":1},{\"date\":\"2026-05-29\",\"min\":401,\"max\":401,\"mean\":401,\"n\":1},{\"date\":\"2026-05-30\",\"min\":408,\"max\":408,\"mean\":408,\"n\":1},{\"date\":\"2026-05-31\",\"min\":415,\"max\":415,\"mean\":415,\"n\":1},{\"date\":\"2026-06-01\",\"min\":422,\"max\":422,\"mean\":422,\"n\":1},{\"date\":\"2026-06-02\",\"min\":428,\"max\":428,\"mean\":428,\"n\":1},{\"date\":\"2026-06-03\",\"min\":434,\"max\":434,\"mean\":434,\"n\":1},{\"date\":\"2026-06-04\",\"min\":438,\"max\":438,\"mean\":438,\"n\":1},{\"date\":\"2026-06-05\",\"min\":442,\"max\":442,\"mean\":442,\"n\":1},{\"date\":\"2026-06-06\",\"min\":444,\"max\":444,\"mean\":444,\"n\":1},{\"date\":\"2026-06-07\",\"min\":445,\"max\":445,\"mean\":445,\"n\":1},{\"date\":\"2026-06-08\",\"min\":444,\"max\":444,\"mean\":444,\"n\":1},{\"date\":\"2026-06-09\",\"min\":443,\"max\":443,\"mean\":443,\"n\":1},{\"date\":\"2026-06-10\",\"min\":439,\"max\":439,\"mean\":439,\"n\":1},{\"date\":\"2026-06-11\",\"min\":435,\"max\":435,\"mean\":435,\"n\":1},{\"date\":\"2026-06-12\",\"min\":430,\"max\":430,\"mean\":430,\"n\":1},{\"date\":\"2026-06-13\",\"min\":424,\"max\":424,\"mean\":424,\"n\":1},{\"date\":\"2026-06-14\",\"min\":417,\"max\":417,\"mean\":417,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":375,\"max\":380,\"mean\":376.6,\"n\":5},{\"weekISO\":\"2026-W13\",\"min\":379,\"max\":414,\"mean\":394.86,\"n\":7},{\"weekISO\":\"2026-W14\",\"min\":421,\"max\":445,\"mean\":435.57,\"n\":7},{\"weekISO\":\"2026-W15\",\"min\":418,\"max\":445,\"mean\":434,\"n\":7},{\"weekISO\":\"2026-W16\",\"min\":378,\"max\":411,\"mean\":392.57,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":375,\"max\":391,\"mean\":380.43,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":397,\"max\":436,\"mean\":417.14,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":438,\"max\":445,\"mean\":442.29,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":394,\"max\":433,\"mean\":413.71,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":375,\"max\":388,\"mean\":379,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":379,\"max\":379,\"mean\":379,\"n\":1}],\"monthly\":[],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":1,\"value\":417}}}" + ] + } + ], + "temperature": 0.45, + "maxTokens": 700, + "seed": 1618033, + "responseFormat": "json", + "timeoutMs": 60000 + } + ], + "enqueued": [], + "result": { + "hasProvider": true, + "text": "Das ist kein JSON.", + "cached": false, + "updatedAt": "2026-06-15T10:00:00.000Z" + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "5df7980e558674aefbfe894525e4d2721f281a3dcf776465d7cc1ba0983b42fd", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "e348f34b369dce59f42c06c8bcf06a163f86dc61913f8af96d3bb5a49e55f4f6", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:SLEEP_DURATION", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "42e3295479aedb8d71869250de66ec31d8d36bbf3b2243a5f20a4b0b36dd7849", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:SLEEP_DURATION", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "6075377b17914e90f2c3beb7ecf88d8bd454427d0d32ab205552f79a05dd3c94", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + } + ] + }, + "SLEEP_DURATION unparseable-fr": { + "calls": [ + { + "system": [ + "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", + "", + "YOUR DATA (graded snapshot):", + "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", + "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", + "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", + "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", + "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", + "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", + "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", + "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", + "", + "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", + "Moves available (choose the few that genuinely apply):", + "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", + "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", + "(c) name a genuine, earned win or a steady streak when the data shows one;", + "(d) one watch-item worth keeping an eye on;", + "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", + "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", + "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", + "", + "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", + "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", + "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", + "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", + "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", + "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", + "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", + "", + "OPENING — lead with meaning, not the metric value", + "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", + "", + "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", + "", + "JUDGE THE DATA HONESTLY (never invent a trend):", + "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", + "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", + "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", + "", + "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", + "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", + "", + "METRIC IDENTIFIERS STAY OUT OF PROSE", + "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", + "", + "GLP-1 DOSE SAFETY (contract, not style)", + "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", + "", + "ACUTE RED FLAGS (contract, not style)", + "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", + "", + "GROUNDING — ZERO HALLUCINATIONS", + "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", + "", + "TONE — a warm, motivating advisor, never clinical or alarming", + "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", + "", + "NOT AN INSIGHT — never recite mechanics", + "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", + "", + "INTERPRET, DON'T ENUMERATE", + "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", + "", + "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", + "", + "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", + "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", + "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", + "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", + "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", + "", + "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in French: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", + "{ \"summary\": \"...\" }", + "", + "ARCHETYPE — SLEEP:", + "- This is nightly sleep duration. The snapshot's values are in minutes; convert to hours for your prose (e.g. 450 min = 7.5 h). Assess both the typical duration and its CONSISTENCY: a stable ~7–9 h is the favourable pattern; high night-to-night variability is itself worth naming even when the mean looks fine.", + "- Compare the recent nights to the user's own weekly/monthly baseline. A reference band of 7–9 h is a coarse adult guide, not a verdict — short or long sleepers vary.", + "- Close with one doable step toward more consistent or sufficient sleep ONLY when the finding implies one — never the forbidden 'get enough sleep' platitude; be specific (e.g. a steadier wind-down time). When sleep is already consistent and sufficient, affirm it and name one thing worth keeping an eye on instead of manufacturing a step.", + "", + "METRIC: Sleep duration (unit: min).", + "Neither extreme is good — the value should sit inside its normal band; deviations in either direction are worth noting.", + "Coarse population reference band: 420–540 min (a placement aid only — the user's OWN baseline leads).", + "", + "OUTPUT LANGUAGE: Répondez en français. Utilisez le vouvoiement par défaut ; tutoyez si l'utilisateur tutoie en premier. Utilisez un vocabulaire de santé français naturel." + ], + "messages": [ + { + "role": "user", + "content": [ + "Date: 2026-06-15", + "OPENER HINT: Open with what this means for the user today, then the number behind it.", + "Write one short assessment of this person's sleep duration. Open with what it MEANS in plain words — the read, not the number (e.g. \"running a little calmer than usual\", \"steady and right where it's been\") — then bring in ONE concrete number from the snapshot right after as support, placed against their own weekly/monthly baseline; never lead with the value. Close with one doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the measurement count and recency.", + "", + "PREVIOUS ANALYSIS: none on file. Treat this as the user's first analysis for this domain — no improvement/regression delta to surface.", + "", + "", + "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der jüngsten Richtung (steigend / fallend / flach gegenüber der Baseline) eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", + "", + "DATENLAGE: 89 Tages-Buckets im Fenster, letzter Wert vor 1 Tag.", + "", + "WIEDERHOLUNG: du hast für diese Metrik bereits 3-mal in Folge eine ähnliche \"keine wesentliche Änderung\"-Einschätzung gegeben. Wiederhole NICHT erneut dasselbe Niveau. Benenne die Kontinuität in einem kurzen Nebensatz (\"weiterhin stabil, 4 Checks in Folge\") und wende dich dann einem ANDEREN Aspekt zu — Konstanz, Tageszeit-Muster oder einer korrelierten Metrik — oder sage, falls nichts wirklich umsetzbar ist, dies klar und lass den erzwungenen Schritt weg.", + "", + "ZUSAMMENHÄNGE (statistisch geprüfte Assoziationen in DEINEN Daten, die diese Metrik betreffen — beschreibend, NIE kausal):", + "- Quand la durée de sommeil monte, le poids du lendemain semble avoir tendance à augmenter, au vu des données actuelles. C'est une tendance à surveiller, pas une cause. (n=45, r=0.98)", + "- Quand la glycémie monte, la durée de sommeil du lendemain semble avoir tendance à augmenter, au vu des données actuelles. C'est une tendance à surveiller, pas une cause. (n=22, r=0.95)", + "Du DARFST höchstens EINEN davon einflechten, wenn er echten Mehrwert bringt — als \"Zusammenhang\" / \"geht tendenziell einher mit\" formuliert, nie als Ursache, und ohne die Zahlen zu verändern. Lass sie weg, wenn der eigene Befund der Metrik die stärkere Geschichte ist.", + "", + "", + "INTERPRETATION CONTEXT (guideline bands — computed server-side; state, do NOT recompute):", + "- Current value 423.85 min is in the \"the recommended adult range\" band, and sits near the LOWER boundary of this band (nearest boundary at 420 min).", + "- Guideline bands (NSF 2015; AASM/SRS 2015; the middle band is the target — both extremes are less favourable):", + " - below 420 min: below the recommended adult range", + " - 420–540 min: the recommended adult range", + " - 540 min and above: above the recommended adult range", + "- Values in the \"the recommended adult range\" band are considered a healthy / low-risk range per these guidelines.", + "- Name where the value sits and what that band means in plain words. Judge any trend BY this position: a shift deep inside a favourable band is a footnote; the same shift approaching or crossing a boundary is the headline. Frame consequence without diagnosis (\"values in this range are considered …\", never \"you have / are at risk of …\").", + "", + "", + "{\"locale\":\"fr\",\"promptVersion\":\"6.3.0\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"SLEEP_DURATION\",\"metric\":{\"id\":\"SLEEP_DURATION\",\"displayName\":\"Sleep duration\",\"unit\":\"min\",\"direction\":\"target-band\",\"normalRange\":{\"low\":420,\"high\":540}},\"profile\":{\"ageYears\":42,\"sex\":\"MALE\"},\"dataCoverage\":{\"totalMeasurements\":89,\"totalSpanDays\":88,\"newestMeasurementDaysAgo\":1},\"SLEEP_DURATION\":{\"signal\":{\"metric\":\"Sleep duration\",\"unit\":\"min\",\"current\":423.85,\"currentWindowDays\":20,\"baseline\":404.11,\"baselineLabel\":\"your recent-weeks average\",\"delta\":19.74,\"deltaPct\":4.9,\"spread\":25.3,\"outsideNormalSwing\":false,\"direction\":\"target-band\",\"n\":20,\"newestDaysAgo\":1,\"normalRange\":{\"low\":420,\"high\":540},\"placement\":\"in band\"},\"summary\":{\"points\":89,\"start\":417,\"end\":380,\"delta\":-37,\"mean\":410.85,\"min\":375,\"max\":445},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":384,\"max\":384,\"mean\":384,\"n\":1},{\"date\":\"2026-05-27\",\"min\":389,\"max\":389,\"mean\":389,\"n\":1},{\"date\":\"2026-05-28\",\"min\":395,\"max\":395,\"mean\":395,\"n\":1},{\"date\":\"2026-05-29\",\"min\":401,\"max\":401,\"mean\":401,\"n\":1},{\"date\":\"2026-05-30\",\"min\":408,\"max\":408,\"mean\":408,\"n\":1},{\"date\":\"2026-05-31\",\"min\":415,\"max\":415,\"mean\":415,\"n\":1},{\"date\":\"2026-06-01\",\"min\":422,\"max\":422,\"mean\":422,\"n\":1},{\"date\":\"2026-06-02\",\"min\":428,\"max\":428,\"mean\":428,\"n\":1},{\"date\":\"2026-06-03\",\"min\":434,\"max\":434,\"mean\":434,\"n\":1},{\"date\":\"2026-06-04\",\"min\":438,\"max\":438,\"mean\":438,\"n\":1},{\"date\":\"2026-06-05\",\"min\":442,\"max\":442,\"mean\":442,\"n\":1},{\"date\":\"2026-06-06\",\"min\":444,\"max\":444,\"mean\":444,\"n\":1},{\"date\":\"2026-06-07\",\"min\":445,\"max\":445,\"mean\":445,\"n\":1},{\"date\":\"2026-06-08\",\"min\":444,\"max\":444,\"mean\":444,\"n\":1},{\"date\":\"2026-06-09\",\"min\":443,\"max\":443,\"mean\":443,\"n\":1},{\"date\":\"2026-06-10\",\"min\":439,\"max\":439,\"mean\":439,\"n\":1},{\"date\":\"2026-06-11\",\"min\":435,\"max\":435,\"mean\":435,\"n\":1},{\"date\":\"2026-06-12\",\"min\":430,\"max\":430,\"mean\":430,\"n\":1},{\"date\":\"2026-06-13\",\"min\":424,\"max\":424,\"mean\":424,\"n\":1},{\"date\":\"2026-06-14\",\"min\":417,\"max\":417,\"mean\":417,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":375,\"max\":380,\"mean\":376.6,\"n\":5},{\"weekISO\":\"2026-W13\",\"min\":379,\"max\":414,\"mean\":394.86,\"n\":7},{\"weekISO\":\"2026-W14\",\"min\":421,\"max\":445,\"mean\":435.57,\"n\":7},{\"weekISO\":\"2026-W15\",\"min\":418,\"max\":445,\"mean\":434,\"n\":7},{\"weekISO\":\"2026-W16\",\"min\":378,\"max\":411,\"mean\":392.57,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":375,\"max\":391,\"mean\":380.43,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":397,\"max\":436,\"mean\":417.14,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":438,\"max\":445,\"mean\":442.29,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":394,\"max\":433,\"mean\":413.71,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":375,\"max\":388,\"mean\":379,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":379,\"max\":379,\"mean\":379,\"n\":1}],\"monthly\":[],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":1,\"value\":417}}}" + ] + } + ], + "temperature": 0.45, + "maxTokens": 700, + "seed": 1618033, + "responseFormat": "json", + "timeoutMs": 60000 + } + ], + "enqueued": [], + "result": { + "hasProvider": false, + "text": "Stable, comme d’habitude. Sleep duration est à 423,9 min en ce moment — au-dessus de ta moyenne habituelle de 404,1 min. C’est dans tes repères habituels — rien qui appelle une réaction maintenant.", + "cached": true, + "updatedAt": null + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "5df7980e558674aefbfe894525e4d2721f281a3dcf776465d7cc1ba0983b42fd", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "e348f34b369dce59f42c06c8bcf06a163f86dc61913f8af96d3bb5a49e55f4f6", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:SLEEP_DURATION", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "42e3295479aedb8d71869250de66ec31d8d36bbf3b2243a5f20a4b0b36dd7849", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:SLEEP_DURATION", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "6075377b17914e90f2c3beb7ecf88d8bd454427d0d32ab205552f79a05dd3c94", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:SLEEP_DURATION", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + } + ] + }, + "SLEEP_DURATION blocked-es": { + "calls": [ + { + "system": [ + "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", + "", + "YOUR DATA (graded snapshot):", + "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", + "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", + "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", + "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", + "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", + "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", + "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", + "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", + "", + "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", + "Moves available (choose the few that genuinely apply):", + "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", + "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", + "(c) name a genuine, earned win or a steady streak when the data shows one;", + "(d) one watch-item worth keeping an eye on;", + "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", + "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", + "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", + "", + "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", + "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", + "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", + "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", + "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", + "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", + "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", + "", + "OPENING — lead with meaning, not the metric value", + "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", + "", + "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", + "", + "JUDGE THE DATA HONESTLY (never invent a trend):", + "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", + "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", + "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", + "", + "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", + "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", + "", + "METRIC IDENTIFIERS STAY OUT OF PROSE", + "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", + "", + "GLP-1 DOSE SAFETY (contract, not style)", + "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", + "", + "ACUTE RED FLAGS (contract, not style)", + "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", + "", + "GROUNDING — ZERO HALLUCINATIONS", + "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", + "", + "TONE — a warm, motivating advisor, never clinical or alarming", + "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", + "", + "NOT AN INSIGHT — never recite mechanics", + "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", + "", + "INTERPRET, DON'T ENUMERATE", + "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", + "", + "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", + "", + "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", + "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", + "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", + "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", + "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", + "", + "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in Spanish: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", + "{ \"summary\": \"...\" }", + "", + "ARCHETYPE — SLEEP:", + "- This is nightly sleep duration. The snapshot's values are in minutes; convert to hours for your prose (e.g. 450 min = 7.5 h). Assess both the typical duration and its CONSISTENCY: a stable ~7–9 h is the favourable pattern; high night-to-night variability is itself worth naming even when the mean looks fine.", + "- Compare the recent nights to the user's own weekly/monthly baseline. A reference band of 7–9 h is a coarse adult guide, not a verdict — short or long sleepers vary.", + "- Close with one doable step toward more consistent or sufficient sleep ONLY when the finding implies one — never the forbidden 'get enough sleep' platitude; be specific (e.g. a steadier wind-down time). When sleep is already consistent and sufficient, affirm it and name one thing worth keeping an eye on instead of manufacturing a step.", + "", + "METRIC: Sleep duration (unit: min).", + "Neither extreme is good — the value should sit inside its normal band; deviations in either direction are worth noting.", + "Coarse population reference band: 420–540 min (a placement aid only — the user's OWN baseline leads).", + "", + "OUTPUT LANGUAGE: Responda en español (registro peninsular formal — usted). Use un vocabulario sanitario en español natural." + ], + "messages": [ + { + "role": "user", + "content": [ + "Date: 2026-06-15", + "OPENER HINT: Open with what this means for the user today, then the number behind it.", + "Write one short assessment of this person's sleep duration. Open with what it MEANS in plain words — the read, not the number (e.g. \"running a little calmer than usual\", \"steady and right where it's been\") — then bring in ONE concrete number from the snapshot right after as support, placed against their own weekly/monthly baseline; never lead with the value. Close with one doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the measurement count and recency.", + "", + "PREVIOUS ANALYSIS: none on file. Treat this as the user's first analysis for this domain — no improvement/regression delta to surface.", + "", + "", + "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der jüngsten Richtung (steigend / fallend / flach gegenüber der Baseline) eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", + "", + "DATENLAGE: 89 Tages-Buckets im Fenster, letzter Wert vor 1 Tag.", + "", + "WIEDERHOLUNG: du hast für diese Metrik bereits 3-mal in Folge eine ähnliche \"keine wesentliche Änderung\"-Einschätzung gegeben. Wiederhole NICHT erneut dasselbe Niveau. Benenne die Kontinuität in einem kurzen Nebensatz (\"weiterhin stabil, 4 Checks in Folge\") und wende dich dann einem ANDEREN Aspekt zu — Konstanz, Tageszeit-Muster oder einer korrelierten Metrik — oder sage, falls nichts wirklich umsetzbar ist, dies klar und lass den erzwungenen Schritt weg.", + "", + "ZUSAMMENHÄNGE (statistisch geprüfte Assoziationen in DEINEN Daten, die diese Metrik betreffen — beschreibend, NIE kausal):", + "- Cuando la duración del sueño es mayor, el peso del día siguiente parece tender a subir, según los datos disponibles. Es un patrón que conviene observar, no una causa. (n=45, r=0.98)", + "- Cuando la glucosa en sangre es mayor, la duración del sueño del día siguiente parece tender a subir, según los datos disponibles. Es un patrón que conviene observar, no una causa. (n=22, r=0.95)", + "Du DARFST höchstens EINEN davon einflechten, wenn er echten Mehrwert bringt — als \"Zusammenhang\" / \"geht tendenziell einher mit\" formuliert, nie als Ursache, und ohne die Zahlen zu verändern. Lass sie weg, wenn der eigene Befund der Metrik die stärkere Geschichte ist.", + "", + "", + "INTERPRETATION CONTEXT (guideline bands — computed server-side; state, do NOT recompute):", + "- Current value 423.85 min is in the \"the recommended adult range\" band, and sits near the LOWER boundary of this band (nearest boundary at 420 min).", + "- Guideline bands (NSF 2015; AASM/SRS 2015; the middle band is the target — both extremes are less favourable):", + " - below 420 min: below the recommended adult range", + " - 420–540 min: the recommended adult range", + " - 540 min and above: above the recommended adult range", + "- Values in the \"the recommended adult range\" band are considered a healthy / low-risk range per these guidelines.", + "- Name where the value sits and what that band means in plain words. Judge any trend BY this position: a shift deep inside a favourable band is a footnote; the same shift approaching or crossing a boundary is the headline. Frame consequence without diagnosis (\"values in this range are considered …\", never \"you have / are at risk of …\").", + "", + "", + "{\"locale\":\"es\",\"promptVersion\":\"6.3.0\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"SLEEP_DURATION\",\"metric\":{\"id\":\"SLEEP_DURATION\",\"displayName\":\"Sleep duration\",\"unit\":\"min\",\"direction\":\"target-band\",\"normalRange\":{\"low\":420,\"high\":540}},\"profile\":{\"ageYears\":42,\"sex\":\"MALE\"},\"dataCoverage\":{\"totalMeasurements\":89,\"totalSpanDays\":88,\"newestMeasurementDaysAgo\":1},\"SLEEP_DURATION\":{\"signal\":{\"metric\":\"Sleep duration\",\"unit\":\"min\",\"current\":423.85,\"currentWindowDays\":20,\"baseline\":404.11,\"baselineLabel\":\"your recent-weeks average\",\"delta\":19.74,\"deltaPct\":4.9,\"spread\":25.3,\"outsideNormalSwing\":false,\"direction\":\"target-band\",\"n\":20,\"newestDaysAgo\":1,\"normalRange\":{\"low\":420,\"high\":540},\"placement\":\"in band\"},\"summary\":{\"points\":89,\"start\":417,\"end\":380,\"delta\":-37,\"mean\":410.85,\"min\":375,\"max\":445},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":384,\"max\":384,\"mean\":384,\"n\":1},{\"date\":\"2026-05-27\",\"min\":389,\"max\":389,\"mean\":389,\"n\":1},{\"date\":\"2026-05-28\",\"min\":395,\"max\":395,\"mean\":395,\"n\":1},{\"date\":\"2026-05-29\",\"min\":401,\"max\":401,\"mean\":401,\"n\":1},{\"date\":\"2026-05-30\",\"min\":408,\"max\":408,\"mean\":408,\"n\":1},{\"date\":\"2026-05-31\",\"min\":415,\"max\":415,\"mean\":415,\"n\":1},{\"date\":\"2026-06-01\",\"min\":422,\"max\":422,\"mean\":422,\"n\":1},{\"date\":\"2026-06-02\",\"min\":428,\"max\":428,\"mean\":428,\"n\":1},{\"date\":\"2026-06-03\",\"min\":434,\"max\":434,\"mean\":434,\"n\":1},{\"date\":\"2026-06-04\",\"min\":438,\"max\":438,\"mean\":438,\"n\":1},{\"date\":\"2026-06-05\",\"min\":442,\"max\":442,\"mean\":442,\"n\":1},{\"date\":\"2026-06-06\",\"min\":444,\"max\":444,\"mean\":444,\"n\":1},{\"date\":\"2026-06-07\",\"min\":445,\"max\":445,\"mean\":445,\"n\":1},{\"date\":\"2026-06-08\",\"min\":444,\"max\":444,\"mean\":444,\"n\":1},{\"date\":\"2026-06-09\",\"min\":443,\"max\":443,\"mean\":443,\"n\":1},{\"date\":\"2026-06-10\",\"min\":439,\"max\":439,\"mean\":439,\"n\":1},{\"date\":\"2026-06-11\",\"min\":435,\"max\":435,\"mean\":435,\"n\":1},{\"date\":\"2026-06-12\",\"min\":430,\"max\":430,\"mean\":430,\"n\":1},{\"date\":\"2026-06-13\",\"min\":424,\"max\":424,\"mean\":424,\"n\":1},{\"date\":\"2026-06-14\",\"min\":417,\"max\":417,\"mean\":417,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":375,\"max\":380,\"mean\":376.6,\"n\":5},{\"weekISO\":\"2026-W13\",\"min\":379,\"max\":414,\"mean\":394.86,\"n\":7},{\"weekISO\":\"2026-W14\",\"min\":421,\"max\":445,\"mean\":435.57,\"n\":7},{\"weekISO\":\"2026-W15\",\"min\":418,\"max\":445,\"mean\":434,\"n\":7},{\"weekISO\":\"2026-W16\",\"min\":378,\"max\":411,\"mean\":392.57,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":375,\"max\":391,\"mean\":380.43,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":397,\"max\":436,\"mean\":417.14,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":438,\"max\":445,\"mean\":442.29,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":394,\"max\":433,\"mean\":413.71,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":375,\"max\":388,\"mean\":379,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":379,\"max\":379,\"mean\":379,\"n\":1}],\"monthly\":[],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":1,\"value\":417}}}" + ] + } + ], + "temperature": 0.45, + "maxTokens": 700, + "seed": 1618033, + "responseFormat": "json", + "timeoutMs": 60000 + } + ], + "enqueued": [], + "result": { + "hasProvider": false, + "text": "Estable, como de costumbre. Sleep duration está en 423,9 min ahora mismo — por encima de tu promedio habitual de 404,1 min. Eso queda dentro de tu rango habitual — nada que pida una reacción ahora.", + "cached": true, + "updatedAt": null + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "5df7980e558674aefbfe894525e4d2721f281a3dcf776465d7cc1ba0983b42fd", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "e348f34b369dce59f42c06c8bcf06a163f86dc61913f8af96d3bb5a49e55f4f6", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:SLEEP_DURATION", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "42e3295479aedb8d71869250de66ec31d8d36bbf3b2243a5f20a4b0b36dd7849", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:SLEEP_DURATION", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "6075377b17914e90f2c3beb7ecf88d8bd454427d0d32ab205552f79a05dd3c94", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:SLEEP_DURATION", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:SLEEP_DURATION", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + } + ] + }, + "SLEEP_DURATION bare-error-en": { + "calls": [], + "enqueued": [], + "result": { + "hasProvider": true, + "text": null, + "cached": false, + "updatedAt": null, + "insufficient": true + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "5df7980e558674aefbfe894525e4d2721f281a3dcf776465d7cc1ba0983b42fd", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "e348f34b369dce59f42c06c8bcf06a163f86dc61913f8af96d3bb5a49e55f4f6", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:SLEEP_DURATION", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "42e3295479aedb8d71869250de66ec31d8d36bbf3b2243a5f20a4b0b36dd7849", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:SLEEP_DURATION", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "6075377b17914e90f2c3beb7ecf88d8bd454427d0d32ab205552f79a05dd3c94", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:SLEEP_DURATION", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:SLEEP_DURATION", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + } + ] + }, + "SLEEP_DURATION bare-error-de": { + "calls": [], + "enqueued": [], + "result": { + "hasProvider": true, + "text": null, + "cached": false, + "updatedAt": null, + "insufficient": true + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "5df7980e558674aefbfe894525e4d2721f281a3dcf776465d7cc1ba0983b42fd", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "e348f34b369dce59f42c06c8bcf06a163f86dc61913f8af96d3bb5a49e55f4f6", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:SLEEP_DURATION", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "42e3295479aedb8d71869250de66ec31d8d36bbf3b2243a5f20a4b0b36dd7849", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:SLEEP_DURATION", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "6075377b17914e90f2c3beb7ecf88d8bd454427d0d32ab205552f79a05dd3c94", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:SLEEP_DURATION", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:SLEEP_DURATION", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + } + ] + }, + "SLEEP_DURATION keyless-en": { + "calls": [], + "enqueued": [], + "result": { + "hasProvider": false, + "text": "Steady and much as usual. Sleep duration is at 423.9 min right now — higher than your usual average of 404.1 min. That sits in your usual range — nothing you need to act on right now.", + "cached": true, + "updatedAt": null + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "5df7980e558674aefbfe894525e4d2721f281a3dcf776465d7cc1ba0983b42fd", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "e348f34b369dce59f42c06c8bcf06a163f86dc61913f8af96d3bb5a49e55f4f6", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:SLEEP_DURATION", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "42e3295479aedb8d71869250de66ec31d8d36bbf3b2243a5f20a4b0b36dd7849", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:SLEEP_DURATION", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "6075377b17914e90f2c3beb7ecf88d8bd454427d0d32ab205552f79a05dd3c94", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:SLEEP_DURATION", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:SLEEP_DURATION", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + } + ] + }, + "SLEEP_DURATION keyless-de": { + "calls": [], + "enqueued": [], + "result": { + "hasProvider": false, + "text": "Stabil und wie gewohnt. Sleep duration liegt aktuell bei 423,9 min — höher als dein üblicher Schnitt von 404,1 min. Das ist in deinem gewohnten Rahmen — nichts, worauf du jetzt reagieren musst.", + "cached": true, + "updatedAt": null + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "5df7980e558674aefbfe894525e4d2721f281a3dcf776465d7cc1ba0983b42fd", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "e348f34b369dce59f42c06c8bcf06a163f86dc61913f8af96d3bb5a49e55f4f6", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:SLEEP_DURATION", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "42e3295479aedb8d71869250de66ec31d8d36bbf3b2243a5f20a4b0b36dd7849", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:SLEEP_DURATION", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "6075377b17914e90f2c3beb7ecf88d8bd454427d0d32ab205552f79a05dd3c94", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:SLEEP_DURATION", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:SLEEP_DURATION", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + } + ] + }, + "SLEEP_DURATION keyless-read-only": { + "calls": [], + "enqueued": [], + "result": { + "hasProvider": false, + "text": "No assessment on this one right now. These numbers become readable across several weeks rather than single days, and taken at consistent times so they stay comparable. The signal that counts is several metrics drifting the same unfavourable way at once, not one value on one day.", + "cached": true, + "updatedAt": null + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "5df7980e558674aefbfe894525e4d2721f281a3dcf776465d7cc1ba0983b42fd", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "e348f34b369dce59f42c06c8bcf06a163f86dc61913f8af96d3bb5a49e55f4f6", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:SLEEP_DURATION", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "42e3295479aedb8d71869250de66ec31d8d36bbf3b2243a5f20a4b0b36dd7849", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:SLEEP_DURATION", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "6075377b17914e90f2c3beb7ecf88d8bd454427d0d32ab205552f79a05dd3c94", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:SLEEP_DURATION", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:SLEEP_DURATION", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + } + ] + }, + "VO2_MAX no-data": { + "calls": [], + "enqueued": [], + "result": { + "hasProvider": true, + "text": null, + "cached": false, + "updatedAt": null, + "insufficient": true + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "5df7980e558674aefbfe894525e4d2721f281a3dcf776465d7cc1ba0983b42fd", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "e348f34b369dce59f42c06c8bcf06a163f86dc61913f8af96d3bb5a49e55f4f6", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "f9d4889e34e8decefbd5042f4e511a36493df422e8da996c3300bf24e1f2ee88", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "674e1cd14b6fcb0f502db368a80074380c0f241e2abcfc5c3829d9afdf83224e", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:RESTING_HEART_RATE", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:SLEEP_DURATION", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "42e3295479aedb8d71869250de66ec31d8d36bbf3b2243a5f20a4b0b36dd7849", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:SLEEP_DURATION", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "6075377b17914e90f2c3beb7ecf88d8bd454427d0d32ab205552f79a05dd3c94", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "metric:SLEEP_DURATION", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "metric:SLEEP_DURATION", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + } + ] + } +} diff --git a/tests/integration/__golden__/status-parity/mood.json b/tests/integration/__golden__/status-parity/mood.json new file mode 100644 index 000000000..0d7f88835 --- /dev/null +++ b/tests/integration/__golden__/status-parity/mood.json @@ -0,0 +1,1542 @@ +{ + "read-only-cold": { + "calls": [], + "enqueued": [ + { + "userId": "parity-rich", + "metric": "mood", + "locale": "en" + } + ], + "result": { + "hasProvider": true, + "text": null, + "cached": false, + "updatedAt": null, + "preparing": true, + "revalidating": false + }, + "notes": [] + }, + "fresh-en": { + "calls": [ + { + "system": [ + "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", + "", + "YOUR DATA (graded snapshot):", + "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", + "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", + "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", + "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", + "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", + "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", + "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", + "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", + "", + "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", + "Moves available (choose the few that genuinely apply):", + "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", + "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", + "(c) name a genuine, earned win or a steady streak when the data shows one;", + "(d) one watch-item worth keeping an eye on;", + "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", + "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", + "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", + "", + "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", + "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", + "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", + "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", + "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", + "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", + "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", + "", + "OPENING — lead with meaning, not the metric value", + "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", + "", + "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", + "", + "JUDGE THE DATA HONESTLY (never invent a trend):", + "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", + "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", + "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", + "", + "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", + "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", + "", + "METRIC IDENTIFIERS STAY OUT OF PROSE", + "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", + "", + "GLP-1 DOSE SAFETY (contract, not style)", + "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", + "", + "ACUTE RED FLAGS (contract, not style)", + "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", + "", + "GROUNDING — ZERO HALLUCINATIONS", + "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", + "", + "TONE — a warm, motivating advisor, never clinical or alarming", + "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", + "", + "NOT AN INSIGHT — never recite mechanics", + "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", + "", + "INTERPRET, DON'T ENUMERATE", + "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", + "", + "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", + "", + "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", + "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", + "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", + "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", + "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", + "", + "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in English: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", + "{ \"summary\": \"...\" }", + "", + "METRIC — MOOD / WELL-BEING:", + "- Scale 1 (very bad) to 5 (very good). The snapshot carries mood.signal (the finished comparison) + mood.summary + mood.series (graded daily means), mood.target (green/orange band), mood.latestDayFocus and, where present, mood.tags (most frequent mood tags).", + "- The weekly pattern matters: lead from mood.signal — current, delta vs baselineLabel and outsideNormalSwing are already computed. STATE them, do NOT recompute. Treat outsideNormalSwing: false as \"inside the usual swing — not a finding\".", + "- Name persistent phases seriously but calmly: several days below ~2.5 or above ~4.5 are worth noting — as an observation, never a diagnosis and never alarming.", + "- When mood stays very low for several days (clearly below ~2.5), offer a support pathway calmly and in an autonomy-preserving way — e.g. \"if the strain persists, talking it through with someone you trust or a professional can help\". Never alarming, never a diagnosis, no emergency tone; a gentle option, not an imperative.", + "- Tags: if mood.tags is present, you may name a recurring tag as possible context, without claiming a link.", + "- Cross-metric: crossMetricContext carries correlations to weight, blood pressure and pulse; mention only when present and |r| > 0.4 — as an association, never a cause. Do not force a cross-link without a clear pattern.", + "- One message: close with ONE doable, kind step ONLY when the finding implies one (e.g. on good days, briefly note what helped). When mood is steadily good, acknowledge that honestly and name one thing worth keeping an eye on instead of manufacturing a step." + ], + "messages": [ + { + "role": "user", + "content": [ + "Date: 2026-06-15", + "OPENER HINT: Open with what this means for the user today, then the number behind it.", + "Write one short assessment of this person's mood. Open with what the recent pattern MEANS in plain words — the read, not the number (e.g. \"a brighter stretch than usual\", \"steady, holding where it's been\") — then bring in the recent level right after as support, placed against their own weekly/monthly baseline; never lead with the value. Close with one kind, doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the entry count and recency.", + "", + "PREVIOUS ANALYSIS: none on file. Treat this as the user's first analysis for this domain — no improvement/regression delta to surface.", + "", + "", + "VARIETY: to keep your cards from reading the same, you may open this one by leading with the recent direction of travel (rising / falling / flat vs the baseline) — only if the data genuinely supports that angle; never force it, never invent a trend.", + "", + "DATA STRENGTH: 70 day-buckets in the window, latest reading today.", + "", + "", + "{\"locale\":\"en\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"mood\",\"dataCoverage\":{\"totalEntries\":70,\"totalSpanDays\":69,\"newestEntryDaysAgo\":0},\"mood\":{\"signal\":{\"metric\":\"your mood\",\"unit\":\"/5\",\"current\":3.15,\"currentWindowDays\":20,\"baseline\":3.24,\"baselineLabel\":\"your recent-weeks average\",\"delta\":-0.09,\"deltaPct\":-2.7,\"spread\":0.75,\"outsideNormalSwing\":false,\"direction\":\"higher-better\",\"n\":20,\"newestDaysAgo\":0,\"normalRange\":{\"low\":3.5,\"high\":5},\"placement\":\"below band\"},\"summary\":{\"points\":70,\"start\":1,\"end\":2,\"delta\":1,\"mean\":3.03,\"min\":1,\"max\":5},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W15\",\"min\":2,\"max\":5,\"mean\":3.33,\"n\":6},{\"weekISO\":\"2026-W16\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":1,\"max\":5,\"mean\":3,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":1,\"max\":4,\"mean\":2.57,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1}],\"monthly\":[],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":0,\"value\":1,\"deltaToPreviousDailyPoint\":-2},\"target\":{\"greenMin\":3.5,\"greenMax\":5,\"orangeMin\":2,\"orangeMax\":3.5,\"inTargetPctLast30DailyPoints\":40},\"tags\":null,\"tagInfluence\":{\"flat\":null,\"structured\":[{\"tag\":\"alcohol\",\"labelKey\":\"mood.tag.alcohol\",\"label\":null,\"categoryKey\":\"health\",\"icon\":\"Wine\",\"withDays\":24,\"withoutDays\":46,\"withAvg\":2.96,\"withoutAvg\":3.07,\"delta\":-0.11,\"pValue\":0.7704186651066071,\"confidence\":\"low\"}]},\"betterDays\":[{\"source\":\"metric\",\"key\":\"weight\",\"labelKey\":null,\"label\":null,\"categoryKey\":null,\"icon\":null,\"direction\":\"up\",\"n\":35,\"confidence\":\"medium\",\"effectSize\":0.523,\"delta\":null,\"r\":0.523},{\"source\":\"tag\",\"key\":\"alcohol\",\"labelKey\":\"mood.tag.alcohol\",\"label\":null,\"categoryKey\":\"health\",\"icon\":\"Wine\",\"direction\":\"down\",\"n\":24,\"confidence\":\"low\",\"effectSize\":0.07670850767085077,\"delta\":-0.11,\"r\":null}],\"factorCrosstab\":null,\"narratives\":[{\"kind\":\"trend\",\"messageKey\":\"insights.mood.narrative.trendUp\",\"vars\":{\"direction\":\"up\",\"delta\":\"0.9\",\"days\":\"7\"},\"strength\":0.86},{\"kind\":\"weekday-dip\",\"messageKey\":\"insights.mood.narrative.weekdayDip\",\"vars\":{\"weekdayKey\":\"charts.weekdaysFull.mon\",\"delta\":\"0.3\"},\"strength\":0.33},{\"kind\":\"in-target\",\"messageKey\":\"insights.mood.narrative.inTarget\",\"vars\":{\"pct\":\"40\"},\"strength\":0.2},{\"kind\":\"streak\",\"messageKey\":\"insights.mood.narrative.streak\",\"vars\":{\"days\":\"70\"},\"strength\":0.15}],\"timeOfDay\":{\"buckets\":[{\"bucket\":\"morning\",\"avgScore\":2.91,\"count\":35},{\"bucket\":\"afternoon\",\"avgScore\":null,\"count\":0},{\"bucket\":\"evening\",\"avgScore\":null,\"count\":0},{\"bucket\":\"night\",\"avgScore\":3.14,\"count\":35}],\"best\":\"night\",\"worst\":\"morning\"},\"stability\":{\"score\":6,\"stdDev\":1.414,\"band\":\"veryVariable\",\"days\":70}},\"crossMetricContext\":{\"weight\":{\"summary\":{\"points\":70,\"start\":84.2,\"end\":82.38,\"delta\":-1.82,\"mean\":83.19,\"min\":82.03,\"max\":84.48},\"correlation\":{\"r\":0.523,\"strength\":\"moderat\",\"n\":35}},\"bloodPressureSystolic\":{\"summary\":{\"points\":94,\"start\":128,\"end\":125,\"delta\":-3,\"mean\":125.39,\"min\":116,\"max\":134},\"correlation\":null},\"pulse\":{\"summary\":{\"points\":140,\"start\":70,\"end\":72,\"delta\":2,\"mean\":70.37,\"min\":65,\"max\":75},\"correlation\":null}}}" + ] + } + ], + "temperature": 0.45, + "maxTokens": 250, + "seed": 1618033, + "responseFormat": "json", + "timeoutMs": 60000 + } + ], + "enqueued": [], + "result": { + "hasProvider": true, + "text": "Parity note for fresh-en.", + "cached": false, + "updatedAt": "2026-06-15T10:00:00.000Z" + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "mood", + "locale": "en", + "text": "Parity note for fresh-en.", + "items": null, + "inputHash": null, + "snapshotHash": "7dd495a6d92c3bc42320044bbbcbe89154afa13e0a954d9f62dee6239bced6f4", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + } + ] + }, + "cached-en": { + "calls": [], + "enqueued": [], + "result": { + "hasProvider": true, + "text": "Parity note for fresh-en.", + "cached": true, + "updatedAt": "2026-06-15T10:00:00.000Z" + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "mood", + "locale": "en", + "text": "Parity note for fresh-en.", + "items": null, + "inputHash": null, + "snapshotHash": "7dd495a6d92c3bc42320044bbbcbe89154afa13e0a954d9f62dee6239bced6f4", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + } + ] + }, + "forced-unchanged-en": { + "calls": [], + "enqueued": [], + "result": { + "hasProvider": true, + "text": "Parity note for fresh-en.", + "cached": true, + "updatedAt": "2026-06-15T10:00:00.000Z" + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "mood", + "locale": "en", + "text": "Parity note for fresh-en.", + "items": null, + "inputHash": null, + "snapshotHash": "7dd495a6d92c3bc42320044bbbcbe89154afa13e0a954d9f62dee6239bced6f4", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + } + ] + }, + "next-day-en": { + "calls": [ + { + "system": [ + "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", + "", + "YOUR DATA (graded snapshot):", + "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", + "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", + "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", + "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", + "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", + "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", + "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", + "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", + "", + "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", + "Moves available (choose the few that genuinely apply):", + "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", + "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", + "(c) name a genuine, earned win or a steady streak when the data shows one;", + "(d) one watch-item worth keeping an eye on;", + "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", + "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", + "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", + "", + "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", + "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", + "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", + "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", + "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", + "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", + "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", + "", + "OPENING — lead with meaning, not the metric value", + "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", + "", + "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", + "", + "JUDGE THE DATA HONESTLY (never invent a trend):", + "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", + "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", + "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", + "", + "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", + "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", + "", + "METRIC IDENTIFIERS STAY OUT OF PROSE", + "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", + "", + "GLP-1 DOSE SAFETY (contract, not style)", + "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", + "", + "ACUTE RED FLAGS (contract, not style)", + "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", + "", + "GROUNDING — ZERO HALLUCINATIONS", + "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", + "", + "TONE — a warm, motivating advisor, never clinical or alarming", + "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", + "", + "NOT AN INSIGHT — never recite mechanics", + "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", + "", + "INTERPRET, DON'T ENUMERATE", + "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", + "", + "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", + "", + "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", + "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", + "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", + "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", + "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", + "", + "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in English: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", + "{ \"summary\": \"...\" }", + "", + "METRIC — MOOD / WELL-BEING:", + "- Scale 1 (very bad) to 5 (very good). The snapshot carries mood.signal (the finished comparison) + mood.summary + mood.series (graded daily means), mood.target (green/orange band), mood.latestDayFocus and, where present, mood.tags (most frequent mood tags).", + "- The weekly pattern matters: lead from mood.signal — current, delta vs baselineLabel and outsideNormalSwing are already computed. STATE them, do NOT recompute. Treat outsideNormalSwing: false as \"inside the usual swing — not a finding\".", + "- Name persistent phases seriously but calmly: several days below ~2.5 or above ~4.5 are worth noting — as an observation, never a diagnosis and never alarming.", + "- When mood stays very low for several days (clearly below ~2.5), offer a support pathway calmly and in an autonomy-preserving way — e.g. \"if the strain persists, talking it through with someone you trust or a professional can help\". Never alarming, never a diagnosis, no emergency tone; a gentle option, not an imperative.", + "- Tags: if mood.tags is present, you may name a recurring tag as possible context, without claiming a link.", + "- Cross-metric: crossMetricContext carries correlations to weight, blood pressure and pulse; mention only when present and |r| > 0.4 — as an association, never a cause. Do not force a cross-link without a clear pattern.", + "- One message: close with ONE doable, kind step ONLY when the finding implies one (e.g. on good days, briefly note what helped). When mood is steadily good, acknowledge that honestly and name one thing worth keeping an eye on instead of manufacturing a step." + ], + "messages": [ + { + "role": "user", + "content": [ + "Date: 2026-06-16", + "OPENER HINT: Open with the direction of change over time, then say where it stands now.", + "Write one short assessment of this person's mood. Open with what the recent pattern MEANS in plain words — the read, not the number (e.g. \"a brighter stretch than usual\", \"steady, holding where it's been\") — then bring in the recent level right after as support, placed against their own weekly/monthly baseline; never lead with the value. Close with one kind, doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the entry count and recency.", + "", + "PREVIOUS ANALYSIS (1 day ago, 2026-06-15):", + "Parity note for fresh-en.", + "", + "INSTRUCTION: When summarising the current state, explicitly call out what improved, what regressed, and what stayed the same compared to that previous snapshot. Use phrases like \"down 4 mmHg from your last check\" or \"same elevated trend as last week\". Do not invent numbers — only compare what's in the snapshot below.", + "", + "", + "VARIETY: to keep your cards from reading the same, you may open this one by leading with the recent direction of travel (rising / falling / flat vs the baseline) — only if the data genuinely supports that angle; never force it, never invent a trend.", + "", + "DATA STRENGTH: 70 day-buckets in the window, latest reading 1 day ago.", + "", + "", + "{\"locale\":\"en\",\"generatedForDay\":\"2026-06-16\",\"focus\":\"mood\",\"dataCoverage\":{\"totalEntries\":70,\"totalSpanDays\":69,\"newestEntryDaysAgo\":1},\"mood\":{\"signal\":{\"metric\":\"your mood\",\"unit\":\"/5\",\"current\":3.05,\"currentWindowDays\":21,\"baseline\":3.24,\"baselineLabel\":\"your recent-weeks average\",\"delta\":-0.19,\"deltaPct\":-5.9,\"spread\":0.75,\"outsideNormalSwing\":false,\"direction\":\"higher-better\",\"n\":21,\"newestDaysAgo\":1,\"normalRange\":{\"low\":3.5,\"high\":5},\"placement\":\"below band\"},\"summary\":{\"points\":70,\"start\":1,\"end\":2,\"delta\":1,\"mean\":3.03,\"min\":1,\"max\":5},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-15\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W15\",\"min\":2,\"max\":5,\"mean\":3.33,\"n\":6},{\"weekISO\":\"2026-W16\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":1,\"max\":5,\"mean\":3,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":1,\"max\":4,\"mean\":2.57,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1}],\"monthly\":[],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":1,\"value\":1,\"deltaToPreviousDailyPoint\":-2},\"target\":{\"greenMin\":3.5,\"greenMax\":5,\"orangeMin\":2,\"orangeMax\":3.5,\"inTargetPctLast30DailyPoints\":41.4},\"tags\":null,\"tagInfluence\":{\"flat\":null,\"structured\":[{\"tag\":\"alcohol\",\"labelKey\":\"mood.tag.alcohol\",\"label\":null,\"categoryKey\":\"health\",\"icon\":\"Wine\",\"withDays\":24,\"withoutDays\":46,\"withAvg\":2.96,\"withoutAvg\":3.07,\"delta\":-0.11,\"pValue\":0.7704186651066071,\"confidence\":\"low\"}]},\"betterDays\":[{\"source\":\"metric\",\"key\":\"weight\",\"labelKey\":null,\"label\":null,\"categoryKey\":null,\"icon\":null,\"direction\":\"up\",\"n\":35,\"confidence\":\"medium\",\"effectSize\":0.523,\"delta\":null,\"r\":0.523},{\"source\":\"tag\",\"key\":\"alcohol\",\"labelKey\":\"mood.tag.alcohol\",\"label\":null,\"categoryKey\":\"health\",\"icon\":\"Wine\",\"direction\":\"down\",\"n\":24,\"confidence\":\"low\",\"effectSize\":0.07670850767085077,\"delta\":-0.11,\"r\":null}],\"factorCrosstab\":null,\"narratives\":[{\"kind\":\"weekday-dip\",\"messageKey\":\"insights.mood.narrative.weekdayDip\",\"vars\":{\"weekdayKey\":\"charts.weekdaysFull.mon\",\"delta\":\"0.3\"},\"strength\":0.33},{\"kind\":\"in-target\",\"messageKey\":\"insights.mood.narrative.inTarget\",\"vars\":{\"pct\":\"41\"},\"strength\":0.2}],\"timeOfDay\":{\"buckets\":[{\"bucket\":\"morning\",\"avgScore\":2.91,\"count\":35},{\"bucket\":\"afternoon\",\"avgScore\":null,\"count\":0},{\"bucket\":\"evening\",\"avgScore\":null,\"count\":0},{\"bucket\":\"night\",\"avgScore\":3.14,\"count\":35}],\"best\":\"night\",\"worst\":\"morning\"},\"stability\":{\"score\":6,\"stdDev\":1.414,\"band\":\"veryVariable\",\"days\":70}},\"crossMetricContext\":{\"weight\":{\"summary\":{\"points\":70,\"start\":84.2,\"end\":82.38,\"delta\":-1.82,\"mean\":83.19,\"min\":82.03,\"max\":84.48},\"correlation\":{\"r\":0.523,\"strength\":\"moderat\",\"n\":35}},\"bloodPressureSystolic\":{\"summary\":{\"points\":94,\"start\":128,\"end\":125,\"delta\":-3,\"mean\":125.39,\"min\":116,\"max\":134},\"correlation\":null},\"pulse\":{\"summary\":{\"points\":140,\"start\":70,\"end\":72,\"delta\":2,\"mean\":70.37,\"min\":65,\"max\":75},\"correlation\":null}}}" + ] + } + ], + "temperature": 0.45, + "maxTokens": 250, + "seed": 1618033, + "responseFormat": "json", + "timeoutMs": 60000 + } + ], + "enqueued": [], + "result": { + "hasProvider": true, + "text": "Parity note for next-day-en.", + "cached": false, + "updatedAt": "2026-06-16T10:00:00.000Z" + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "mood", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "4725ab784136da8774685c2314e10f49ac53a0e2ed524e97b1d9bb62cf3f2176", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + } + ] + }, + "read-only-stale": { + "calls": [], + "enqueued": [ + { + "userId": "parity-rich", + "metric": "mood", + "locale": "en" + } + ], + "result": { + "hasProvider": true, + "text": "Parity note for next-day-en.", + "cached": true, + "updatedAt": "2026-06-16T10:00:00.000Z", + "preparing": false, + "revalidating": true + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "mood", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "4725ab784136da8774685c2314e10f49ac53a0e2ed524e97b1d9bb62cf3f2176", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + } + ] + }, + "prose-de": { + "calls": [ + { + "system": [ + "Du schreibst eine kurze, persönliche Einschätzung zu EINER Gesundheitsmetrik dieses Nutzers. Du kennst seine Daten und ordnest sie gegen seine EIGENE Baseline ein — nicht gegen Bevölkerungsnormen. Deine fachliche Grundlage sind anerkannte Leitlinien (ESH 2023, WHO, AASM, DGE, DEGAM); du beziehst dich darauf, ohne zu diagnostizieren.", + "", + "DEINE DATENGRUNDLAGE (graded snapshot):", + "- signal: der fertige Vergleich — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` ist das recent-Mittel; `baseline` ist der EIGENE längere Durchschnitt; `delta`/`deltaPct` sind die VORZEICHENBEHAFTETE Änderung gegenüber dieser Baseline; `spread` ist die normale Schwankung; `outsideNormalSwing` ist das vorab berechnete Urteil, ob die Änderung diese Schwankung übersteigt. Diese Werte sind bereits berechnet — nenne sie, rechne sie NICHT neu. `contributors[]` (nur Score-Komposite) benennt die 1–2 Teil-Scores, die den Score bewegt haben.", + "- summary: { points, start, end, delta, mean, min, max } — die Eckwerte des gesamten Lesefensters.", + "- series.recent[]: die jüngsten ~21 Tage, je ein Eintrag pro Tag ({ date, min, max, mean, n }).", + "- series.weekly[]: die ~10 Wochen davor, je ein ISO-Wochen-Aggregat.", + "- series.monthly[]: die ~12 Monate davor, je ein Monatsaggregat.", + "- series.yearly[]: alles ältere, je ein Jahr ({ year, mean, min, max, n, slope }).", + "- Die jüngsten Tage liegen also EINZELN vor, ältere Zeiträume als Wochen-, Monats- und Jahresaggregate. Ziehe die series nur für einen konkreten Tag heran; führe sonst mit dem signal-Block.", + "- Ein Slice kann leer sein, wenn die Historie kurz ist. Dann nicht so tun, als gäbe es ihn. Ist `signal.baseline` null, gibt es noch keine etablierte Baseline; sage das ehrlich, statt einen Vergleich zu erfinden.", + "", + "SO BAUST DU DIE EINSCHÄTZUNG — wähle die 2–3 BAUSTEINE, die zu DIESEN Daten passen, und ordne sie so, dass sie als EIN zusammenhängender Gedanke lesen, keine feste Checkliste. Führe gemäß dem OPENER-HINWEIS im User-Prompt, wenn einer mitgegeben ist — eröffne NICHT immer mit der Zahl. Variiere die Form von Metrik zu Metrik; eröffne nie zwei Metriken hintereinander gleich.", + "Verfügbare Bausteine (nimm die wenigen, die wirklich zutreffen):", + "(a) der aktuelle Befund — `current` aus dem signal-Block (oder ein Wert aus series.recent für eine Tagesfrage);", + "(b) gegen die EIGENE Baseline einordnen über das vorab berechnete vorzeichenbehaftete `delta` gegenüber `baselineLabel` — nenne es, leite es nicht neu her. Behandle `outsideNormalSwing: false` als \"innerhalb der üblichen Schwankung — kein Befund\" und ERFINDE KEINEN Trend; `outsideNormalSwing: true` ist die echte, meldenswerte Änderung. Ein `normalRange`/`placement` ist nur ein grober sekundärer Anker — das persönliche delta führt;", + "(c) einen echten, verdienten Erfolg oder eine stabile Serie benennen, wenn die Daten ihn zeigen;", + "(d) einen Punkt zum Im-Auge-behalten;", + "(e) einen Zusammenhang NUR, wenn ein r-Wert vorhanden und |r| > 0,4 ist, formuliert als \"bewegt sich mit\" / \"zeigt sich oft zusammen mit\", nie kausal;", + "(f) GENAU EINEN machbaren nächsten Schritt — und nur, wenn der Befund wirklich einen hergibt. Eine Botschaft = ein Verhalten, keine Liste. Ist der Wert stabil und im grünen Bereich, ERZWINGE KEINEN Schritt — bestätige kurz und nenne stattdessen einen Punkt zum Beobachten. Ein erfundener Schritt ist genau die Floskel, die wir vermeiden.", + "Synthese statt Aufzählung: die Geschichte dessen, was die Daten bedeuten, zählt mehr als das Wiederholen jeder Zahl.", + "", + "TONALITÄT — ein warmer, motivierender Premium-Begleiter (jemand hat sich die Daten dieser Person wirklich angesehen und etwas Wertvolles für sie gefunden):", + "- Zweite Person (\"dein Blutdruck\", \"deine Werte\"), warm, direkt, ehrlich.", + "- Motivierend und vorwärtsgewandt: Wenn die Daten es hergeben, benenne den echten Erfolg klar und baue ein wenig Schwung auf — die Person soll sich gesehen und unterstützt fühlen, nicht belehrt. Die Ermutigung muss durch die Zahlen VERDIENT sein, nie ein reflexhaftes Kompliment.", + "- Autonomie-unterstützend: \"kann helfen\", \"einen Versuch wert\", nie \"du musst\".", + "- Nie alarmierend, nie moralisierend, nie diagnostisch — keine Krankheitsbehauptung. Formuliere als begründete Beobachtung, nicht als ärztlichen Rat.", + "- Auch ungünstige Werte ehrlich benennen: Befund → gegen die eigene Baseline einordnen → ein kleiner machbarer Schritt, als Chance formuliert, nicht als Versagen. Nicht verharmlosen, nicht dramatisieren.", + "- Keine Floskeln und keine bloße Zahlenwiederholung: jede warme Zeile ist an eine echte Zahl oder eine echte Veränderung geknüpft. Ein generischer Positiv-Einstieg (\"Deine Werte sehen gut aus\") ist verboten — verdiene die Ermutigung mit dem konkreten Befund.", + "", + "ERÖFFNUNG — mit der Bedeutung führen, nicht mit dem Messwert", + "Beginne mit dem Eindruck in klaren Worten — was das heute über die Person sagt — und bring die Zahl direkt danach als Beleg, nie als Erstes. Eine Zeile, die mit einem Wert eröffnet (\"Dein Ruhepuls ist 61 …\"), liest sich wie ein Ablesen; dieselbe Zeile, die mit der Bedeutung eröffnet (\"Dein Herz läuft diese Woche etwas ruhiger — rund 61 …\"), liest sich wie ein Begleiter, der wirklich hingesehen hat. Jede Aussage bleibt an einer echten Zahl verankert; nur die REIHENFOLGE ändert sich, Bedeutung zuerst und die Zahl direkt dahinter. Das gilt, ob die Fläche eine Metrik abdeckt oder viele.", + "", + "LÄNGE: 2-4 Sätze, ca. 30-60 Wörter — ist der Wert aber stabil und nichts zu tun, ist EIN knapper Satz besser, als ihn auf Länge zu strecken. Knapp und hochwertig. Keine bloße Zahlenwiederholung, kein Fülltext, keine generischen Floskeln.", + "", + "DATENLAGE EHRLICH BEWERTEN (nie einen Trend erfinden):", + "- Nur wenige Messpunkte/Einträge/Ereignisse oder ein leerer recent-Slice: ehrlich sagen, dass es für eine belastbare Tendenz noch zu wenige Daten sind, und EINEN Hinweis geben (z.B. regelmäßiger erfassen). Keinen Trend behaupten. Was \"wenige\" bedeutet, hängt von der Metrik ab — bei Stimmung sind es Einträge, bei der Einnahmetreue geplante Dosen.", + "- Jüngste Messung deutlich älter als ~7 Tage (dataCoverage.newestMeasurementDaysAgo): darauf hinweisen, dass die Werte nicht mehr aktuell sind.", + "- Korrelationen NUR erwähnen, wenn der r-Wert im Snapshot vorhanden ist und |r| > 0.4 ist. Fehlt das Feld, keine Korrelation interpretieren oder erfinden. Immer als \"Zusammenhang\" formulieren, nie als \"Ursache\".", + "", + "VERBOTENE FLOSKELN (signalisieren ungegroundeten Fülltext — nie ausgeben, außer im Disclaimer)", + "\"achte auf ausreichend Schlaf\", \"trinke genug Wasser\", \"regelmäßige Bewegung\", \"ärztlicher Rat empfohlen\".", + "", + "METRIK-IDENTIFIER GEHÖREN NICHT IN DEN FLIESSTEXT", + "Schreibe niemals Datenbank- bzw. Enum-Namen wie \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" oder \"SLEEP_DURATION\" in nutzersichtbare Strings, und schreibe nie das wörtliche \"metric:\"-Chart-Token in den Fließtext. Verweise auf jede Metrik mit der natürlichsprachlichen Bezeichnung, die der Nutzer sieht — \"deine Systole\", \"dein Gewicht\", \"dein Puls\", \"deine Stimmung\", \"deine Medikamentenadhärenz\", \"dein Ruhepuls\", \"deine Schlafdauer\", \"deine Schritte\".", + "", + "GLP-1-DOSIS-SICHERHEIT (Vertrag, kein Stil)", + "Verschreibe, empfiehl oder ändere NIEMALS Medikamenten-Dosen, auch wenn der Snapshot einen GLP-1-Rezeptoragonisten benennt (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). Du darfst den benannten Wirkstoff und die aktuelle Titrationsstufe NENNEN (\"Woche 3 auf 7,5 mg\"), wenn der Snapshot sie trägt, aber schreibe nie \"erhöhe auf X mg\", \"erwäge die nächste Stufe Y mg\", \"bleibe auf Z mg\" oder eine Variante davon. Ein Plateau rahmt die nächste Entscheidung immer als Gespräch mit der behandelnden Ärztin. Bist du unsicher, ob etwas dosis-präskriptiv ist, behandle es so — und verweise an die Klinik.", + "", + "AKUTE WARNZEICHEN (Vertrag, kein Stil)", + "Beschreibt der Nutzer ein akutes Krisenzeichen — Brustschmerz oder Druck auf der Brust, Ohnmacht oder Beinahe-Ohnmacht (Synkope), ein plötzliches schweres Symptom (z. B. stärkster Kopfschmerz aller Zeiten, plötzliche Schwäche oder Sprachstörung, Atemnot), einen hypertensiven Notfallwert (systolisch ≥ 180 oder diastolisch ≥ 120 mit Symptomen), einen schwer-niedrigen oder sehr hohen Glukosewert (unter 54 mg/dL (3,0 mmol/L), oder unter 70 mg/dL (3,9 mmol/L) mit Symptomen, oder ≥ 250 mg/dL (13,9 mmol/L) mit Symptomen), anhaltendes Fieber ≥ 38,5 °C (101,3 °F) oder Gedanken an Suizid bzw. Selbstverletzung — sage in einem ruhigen Satz klar, dass das jetzt umgehende ärztliche Hilfe oder den Notruf braucht, und coache es NICHT anhand der Daten. Diagnostiziere nicht, benenne keine Erkrankung, schätze keinen Schweregrad aus den Zahlen — verweise nur auf umgehende Hilfe bzw. den Notruf und höre auf. Das ist eine geschlossene Liste; außerhalb davon bleibe wie gewohnt ruhig und nicht alarmierend.", + "", + "ERDUNG — NULL HALLUZINATIONEN", + "Jede Aussage muss auf einer Zahl beruhen, die im übergebenen Snapshot sichtbar ist. Lässt sich die Aussage keinem Snapshot-Feld zuordnen, lass sie weg und erfinde nie einen Messwert, um einer Anfrage zu entsprechen. Ordne jeden Wert gegen die EIGENE Baseline des Nutzers ein (jüngeres vs. längeres Mittel), nie gegen eine Bevölkerungsnorm. Nenne einen Zusammenhang nur, wenn der r-Wert vorhanden und |r| > 0,4 ist — immer als \"Zusammenhang\", nie als \"Ursache\".", + "", + "TONALITÄT — ein warmer, motivierender Begleiter, nie klinisch oder alarmierend", + "Schreibe in der zweiten Person, warm und direkt. Wenn die Daten es hergeben, benenne den echten Erfolg klar und baue ein wenig Schwung auf — die Person soll sich gesehen und unterstützt fühlen, nicht belehrt. Die Ermutigung muss durch die Zahlen VERDIENT sein, nie ein reflexhaftes Kompliment. Lob ist verankert, konkret und angemessen: an einer echten Zahl oder Veränderung festgemacht, nicht auf andere Nutzer übertragbar, und stiller Anerkennung für stille Erfolge. Über-Bestätigung ist eine Sicherheitsregression, keine Wärme — bestätige nie einen sich verschlechternden Trend oder eine unsichere Entscheidung; würdige die Mühe, nicht die Entscheidung, und bleib neutral (\"nichts zu tun — die gute Art von langweilig\"), wenn es nichts zu loben gibt, statt ein Kompliment zu erfinden. Sei autonomie-unterstützend (\"einen Versuch wert\", \"kann helfen\", nie \"du musst\"). Benenne auch ungünstige Werte ehrlich — Befund, dann gegen die eigene Baseline einordnen, dann ein kleiner machbarer Schritt, als Chance formuliert. Nie alarmierend, nie moralisierend, nie diagnostisch. Keine Floskeln und keine bloße Zahlenwiederholung. Beginne NICHT mit einem Kompliment über Datenmenge oder -qualität; verbotene Eröffnungen sind u.a. \"Datengrundlage ist sehr stark\", \"Your data foundation is strong\", \"Du hast eine solide Baseline\", \"Großartiger Datensatz\", ein generisches \"Deine Werte sehen gut aus\" und jede sinngemäße Umformulierung.", + "", + "KEINE EINSCHÄTZUNG — nie Mechanik nacherzählen", + "Eine Messanzahl (\"3-mal erfasst\"), ein Erfassungsrhythmus (\"alle paar Tage gemessen\") oder Bedien-Mechanik ist KEINE Einschätzung und gehört für sich genommen nicht in den Text. Nutze eine solche Angabe NUR, wenn sie eine Konsequenz trägt, und nenne dann die Konsequenz, nicht die Zahl: z. B. \"diese Woche zu wenige Werte für eine Tendenz — eine Morgenmessung würde es klären\", nicht \"du hast diese Woche zweimal gemessen\". Jeder Satz muss sich verdienen, indem er sagt, was ein Wert BEDEUTET oder was daraus folgt — nie, indem er erzählt, wie die Daten erhoben wurden.", + "", + "EINORDNEN, NICHT AUFZÄHLEN", + "Sag, was ein Wert BEDEUTET, nicht nur, was er ist. Trägt der User-Prompt einen EINORDNUNGS-KONTEXT-Block, nutze ihn: benenne, wo der Wert auf der Leitlinien-Skala liegt, was dieses Band in klaren, würdevollen Worten bedeutet und was praktisch folgt. Beurteile den Trend IM KONTEXT — ein \"leicht steigender\" Wert tief in einem günstigen Band ist eine Randnotiz; dieselbe Bewegung an oder nahe einer Grenze ist die Schlagzeile. Rahme jede Konsequenz OHNE Diagnose: \"Werte in diesem Bereich gelten laut Leitlinien als …\", nie \"du hast X\" oder \"du bist gefährdet für X\". Ist KEIN Einordnungs-Block vorhanden, hat die Metrik kein allgemeines Referenzband — ordne sie gegen die EIGENE Baseline der Person ein und sage klar, dass kein allgemeines Referenzband hinterlegt ist, statt eines zu erfinden.", + "", + "FORMATIERUNG — schreibe in kurzen Absätzen, getrennt durch eine LEERZEILE, je 1–3 Sätze; eine längere Antwort hat 2–4 Absätze, nie einen Block. Setze eine Leerzeile zwischen unterschiedliche Gedanken (z. B. \"wie es steht\" vs. \"eine Sache zum Ausprobieren\"). Ein stabiler Einzeiler bleibt EIN Absatz — strecke nicht auf einen zweiten. Wenn du wirklich drei oder mehr parallele Punkte aufzählst (Optionen, Befunde, Schritte), formatiere sie als schlichte Liste: ein Punkt pro Zeile, jede Zeile beginnt mit \"- \". Die EINE wichtigste Kernaussage darfst du mit **doppelten Sternchen** fett setzen, höchstens einmal pro Antwort. Sonst nichts: keine Überschriften, kein Kursiv, keine Backticks, keine nummerierten Listen, keine Emojis.", + "", + "BEISPIELE — beachte die UNTERSCHIEDLICHEN Formen (urteil-zuerst, trend-zuerst, Einzeiler). Sie illustrieren nur Form und Erdung; übernimm sie nie — jede Einschätzung nutzt die echten Snapshot-Zahlen:", + "- URTEIL-ZUERST (Bedeutung zuerst, Zahl als Beleg, ein Schritt): \"Dein Ruhepuls läuft gerade einen Tick niedriger als sonst — diese Woche rund 61 bpm, 5 unter deinem Monatsmittel von 66 und dein niedrigster seit Wochen. So ein Rückgang passt meist zu mehr Bewegung; die Routine, die das gebracht hat, lohnt sich beizubehalten.\"", + "- TREND-ZUERST (Richtung zuerst, dann der Stand): \"Dein systolischer Blutdruck geht seit drei Wochen ruhig nach unten, aktuell rund 124 mmHg — etwa 4 unter deinem 30-Tage-Schnitt. Nichts Dramatisches, einfach eine stetige Richtung.\"", + "- EINZEILER (stabil, KEIN erzwungener Schritt): \"Deine SpO₂ ist mit 97 % stabil und genau in deinem üblichen Bereich — nichts zu tun, die gute Art von langweilig.\"", + "- SCHLECHT (verbotene Floskel, ungegroundet — so NICHT): \"Deine Werte sehen gut aus. Achte auf ausreichend Schlaf und regelmäßige Bewegung.\"", + "", + "AUSGABEFORMAT: Antworte ausschließlich mit validem JSON in genau diesem Schema. Das Feld \"summary\" enthält die komplette Einschätzung auf Deutsch: 1-3 kurze Absätze mit je 1-3 Sätzen, getrennt durch eine Leerzeile als \\n\\n INNERHALB des JSON-Strings. Ein stabiler Einzeiler bleibt EIN Absatz:", + "{ \"summary\": \"...\" }", + "", + "METRIK — STIMMUNG / WOHLBEFINDEN:", + "- Skala 1 (sehr schlecht) bis 5 (sehr gut). Der Snapshot trägt mood.signal (der fertige Vergleich) + mood.summary + mood.series (graded, Tagesmittel), mood.target (grünes/oranges Band), mood.latestDayFocus und ggf. mood.tags (häufigste Stimmungs-Tags).", + "- Wochenmuster zählt: Führe mit mood.signal — current, delta gegenüber baselineLabel und outsideNormalSwing sind bereits berechnet. NENNE sie, rechne sie NICHT neu. Behandle outsideNormalSwing: false als \"innerhalb der üblichen Schwankung — kein Befund\".", + "- Anhaltende Phasen ernst, aber ruhig benennen: mehrere Tage unter ~2.5 oder über ~4.5 sind erwähnenswert — als Beobachtung, nie als Diagnose und nie alarmierend.", + "- Bei anhaltend sehr niedriger Stimmung (mehrere Tage deutlich unter ~2.5): biete ruhig und autonomie-wahrend einen Unterstützungs-Pfad an — etwa \"bei anhaltender Belastung kann ein Gespräch mit einer Vertrauensperson oder einer Fachperson helfen\". Nie alarmierend, nie als Diagnose, kein Notfall-Ton; eine sanfte Option, kein Imperativ.", + "- Tags: Falls mood.tags vorhanden, kannst du ein wiederkehrendes Tag als möglichen Kontext nennen, ohne einen Zusammenhang zu behaupten.", + "- Cross-Metrik: crossMetricContext trägt Korrelationen zu Gewicht, Blutdruck und Puls; nur erwähnen, wenn vorhanden und |r| > 0.4 — als Zusammenhang, nie als Ursache. Erzwinge keinen Querverweis ohne klares Muster.", + "- Eine Botschaft: Schließe NUR DANN mit EINEM machbaren, freundlichen Schritt, wenn der Befund einen nahelegt (z.B. an guten Tagen kurz festhalten, was geholfen hat). Bei stabil guter Stimmung das ehrlich anerkennen und stattdessen einen Punkt nennen, den man im Auge behalten kann, statt einen Schritt zu erzwingen." + ], + "messages": [ + { + "role": "user", + "content": [ + "Datum: 2026-06-15", + "OPENER HINT: Eröffne damit, was das heute für den Nutzer bedeutet, und dann die Zahl dahinter.", + "Schreibe eine kurze Einschätzung zur Stimmung dieser Person. Beginne mit der BEDEUTUNG des jüngsten Musters in klaren Worten — dem Eindruck, nicht der Zahl (z. B. \"eine hellere Phase als sonst\", \"stabil, hält sich, wo sie war\") — und bring danach das jüngste Niveau als Beleg, gegen die eigene Wochen-/Monats-Baseline eingeordnet; führe nie mit dem Wert. Schließe nur dann mit einem freundlichen, machbaren Schritt, wenn der Befund wirklich einen hergibt; ist nichts umsetzbar, lass den Schritt weg statt Fülltext zu erfinden. Konfidenz aus Eintragsanzahl und Aktualität ableiten.", + "", + "VORHERIGE ANALYSE: keine vorhanden. Behandle dies als erste Analyse in diesem Bereich — kein Verbesserungs-/Verschlechterungs-Delta zu nennen.", + "", + "", + "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der jüngsten Richtung (steigend / fallend / flach gegenüber der Baseline) eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", + "", + "DATENLAGE: 70 Tages-Buckets im Fenster, letzter Wert heute.", + "", + "", + "{\"locale\":\"de\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"mood\",\"dataCoverage\":{\"totalEntries\":70,\"totalSpanDays\":69,\"newestEntryDaysAgo\":0},\"mood\":{\"signal\":{\"metric\":\"deine Stimmung\",\"unit\":\"/5\",\"current\":3.15,\"currentWindowDays\":20,\"baseline\":3.24,\"baselineLabel\":\"your recent-weeks average\",\"delta\":-0.09,\"deltaPct\":-2.7,\"spread\":0.75,\"outsideNormalSwing\":false,\"direction\":\"higher-better\",\"n\":20,\"newestDaysAgo\":0,\"normalRange\":{\"low\":3.5,\"high\":5},\"placement\":\"below band\"},\"summary\":{\"points\":70,\"start\":1,\"end\":2,\"delta\":1,\"mean\":3.03,\"min\":1,\"max\":5},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W15\",\"min\":2,\"max\":5,\"mean\":3.33,\"n\":6},{\"weekISO\":\"2026-W16\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":1,\"max\":5,\"mean\":3,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":1,\"max\":4,\"mean\":2.57,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1}],\"monthly\":[],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":0,\"value\":1,\"deltaToPreviousDailyPoint\":-2},\"target\":{\"greenMin\":3.5,\"greenMax\":5,\"orangeMin\":2,\"orangeMax\":3.5,\"inTargetPctLast30DailyPoints\":40},\"tags\":null,\"tagInfluence\":{\"flat\":null,\"structured\":[{\"tag\":\"alcohol\",\"labelKey\":\"mood.tag.alcohol\",\"label\":null,\"categoryKey\":\"health\",\"icon\":\"Wine\",\"withDays\":24,\"withoutDays\":46,\"withAvg\":2.96,\"withoutAvg\":3.07,\"delta\":-0.11,\"pValue\":0.7704186651066071,\"confidence\":\"low\"}]},\"betterDays\":[{\"source\":\"metric\",\"key\":\"weight\",\"labelKey\":null,\"label\":null,\"categoryKey\":null,\"icon\":null,\"direction\":\"up\",\"n\":35,\"confidence\":\"medium\",\"effectSize\":0.523,\"delta\":null,\"r\":0.523},{\"source\":\"tag\",\"key\":\"alcohol\",\"labelKey\":\"mood.tag.alcohol\",\"label\":null,\"categoryKey\":\"health\",\"icon\":\"Wine\",\"direction\":\"down\",\"n\":24,\"confidence\":\"low\",\"effectSize\":0.07670850767085077,\"delta\":-0.11,\"r\":null}],\"factorCrosstab\":null,\"narratives\":[{\"kind\":\"trend\",\"messageKey\":\"insights.mood.narrative.trendUp\",\"vars\":{\"direction\":\"up\",\"delta\":\"0.9\",\"days\":\"7\"},\"strength\":0.86},{\"kind\":\"weekday-dip\",\"messageKey\":\"insights.mood.narrative.weekdayDip\",\"vars\":{\"weekdayKey\":\"charts.weekdaysFull.mon\",\"delta\":\"0.3\"},\"strength\":0.33},{\"kind\":\"in-target\",\"messageKey\":\"insights.mood.narrative.inTarget\",\"vars\":{\"pct\":\"40\"},\"strength\":0.2},{\"kind\":\"streak\",\"messageKey\":\"insights.mood.narrative.streak\",\"vars\":{\"days\":\"70\"},\"strength\":0.15}],\"timeOfDay\":{\"buckets\":[{\"bucket\":\"morning\",\"avgScore\":2.91,\"count\":35},{\"bucket\":\"afternoon\",\"avgScore\":null,\"count\":0},{\"bucket\":\"evening\",\"avgScore\":null,\"count\":0},{\"bucket\":\"night\",\"avgScore\":3.14,\"count\":35}],\"best\":\"night\",\"worst\":\"morning\"},\"stability\":{\"score\":6,\"stdDev\":1.414,\"band\":\"veryVariable\",\"days\":70}},\"crossMetricContext\":{\"weight\":{\"summary\":{\"points\":70,\"start\":84.2,\"end\":82.38,\"delta\":-1.82,\"mean\":83.19,\"min\":82.03,\"max\":84.48},\"correlation\":{\"r\":0.523,\"strength\":\"moderat\",\"n\":35}},\"bloodPressureSystolic\":{\"summary\":{\"points\":94,\"start\":128,\"end\":125,\"delta\":-3,\"mean\":125.39,\"min\":116,\"max\":134},\"correlation\":null},\"pulse\":{\"summary\":{\"points\":140,\"start\":70,\"end\":72,\"delta\":2,\"mean\":70.37,\"min\":65,\"max\":75},\"correlation\":null}}}" + ] + } + ], + "temperature": 0.45, + "maxTokens": 250, + "seed": 1618033, + "responseFormat": "json", + "timeoutMs": 60000 + } + ], + "enqueued": [], + "result": { + "hasProvider": true, + "text": "Das ist kein JSON.", + "cached": false, + "updatedAt": "2026-06-15T10:00:00.000Z" + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "mood", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "96cac39d4f62d35aa78b2eb75dfcda28ae0640443fa5d848feac7a97c08852d4", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "mood", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "4725ab784136da8774685c2314e10f49ac53a0e2ed524e97b1d9bb62cf3f2176", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + } + ] + }, + "unparseable-fr": { + "calls": [ + { + "system": [ + "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", + "", + "YOUR DATA (graded snapshot):", + "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", + "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", + "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", + "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", + "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", + "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", + "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", + "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", + "", + "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", + "Moves available (choose the few that genuinely apply):", + "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", + "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", + "(c) name a genuine, earned win or a steady streak when the data shows one;", + "(d) one watch-item worth keeping an eye on;", + "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", + "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", + "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", + "", + "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", + "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", + "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", + "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", + "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", + "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", + "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", + "", + "OPENING — lead with meaning, not the metric value", + "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", + "", + "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", + "", + "JUDGE THE DATA HONESTLY (never invent a trend):", + "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", + "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", + "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", + "", + "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", + "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", + "", + "METRIC IDENTIFIERS STAY OUT OF PROSE", + "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", + "", + "GLP-1 DOSE SAFETY (contract, not style)", + "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", + "", + "ACUTE RED FLAGS (contract, not style)", + "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", + "", + "GROUNDING — ZERO HALLUCINATIONS", + "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", + "", + "TONE — a warm, motivating advisor, never clinical or alarming", + "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", + "", + "NOT AN INSIGHT — never recite mechanics", + "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", + "", + "INTERPRET, DON'T ENUMERATE", + "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", + "", + "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", + "", + "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", + "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", + "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", + "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", + "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", + "", + "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in French: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", + "{ \"summary\": \"...\" }", + "", + "METRIC — MOOD / WELL-BEING:", + "- Scale 1 (very bad) to 5 (very good). The snapshot carries mood.signal (the finished comparison) + mood.summary + mood.series (graded daily means), mood.target (green/orange band), mood.latestDayFocus and, where present, mood.tags (most frequent mood tags).", + "- The weekly pattern matters: lead from mood.signal — current, delta vs baselineLabel and outsideNormalSwing are already computed. STATE them, do NOT recompute. Treat outsideNormalSwing: false as \"inside the usual swing — not a finding\".", + "- Name persistent phases seriously but calmly: several days below ~2.5 or above ~4.5 are worth noting — as an observation, never a diagnosis and never alarming.", + "- When mood stays very low for several days (clearly below ~2.5), offer a support pathway calmly and in an autonomy-preserving way — e.g. \"if the strain persists, talking it through with someone you trust or a professional can help\". Never alarming, never a diagnosis, no emergency tone; a gentle option, not an imperative.", + "- Tags: if mood.tags is present, you may name a recurring tag as possible context, without claiming a link.", + "- Cross-metric: crossMetricContext carries correlations to weight, blood pressure and pulse; mention only when present and |r| > 0.4 — as an association, never a cause. Do not force a cross-link without a clear pattern.", + "- One message: close with ONE doable, kind step ONLY when the finding implies one (e.g. on good days, briefly note what helped). When mood is steadily good, acknowledge that honestly and name one thing worth keeping an eye on instead of manufacturing a step.", + "", + "OUTPUT LANGUAGE: Répondez en français. Utilisez le vouvoiement par défaut ; tutoyez si l'utilisateur tutoie en premier. Utilisez un vocabulaire de santé français naturel." + ], + "messages": [ + { + "role": "user", + "content": [ + "Date: 2026-06-15", + "OPENER HINT: Open with what this means for the user today, then the number behind it.", + "Write one short assessment of this person's mood. Open with what the recent pattern MEANS in plain words — the read, not the number (e.g. \"a brighter stretch than usual\", \"steady, holding where it's been\") — then bring in the recent level right after as support, placed against their own weekly/monthly baseline; never lead with the value. Close with one kind, doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the entry count and recency.", + "", + "PREVIOUS ANALYSIS: none on file. Treat this as the user's first analysis for this domain — no improvement/regression delta to surface.", + "", + "", + "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der jüngsten Richtung (steigend / fallend / flach gegenüber der Baseline) eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", + "", + "DATENLAGE: 70 Tages-Buckets im Fenster, letzter Wert heute.", + "", + "", + "{\"locale\":\"fr\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"mood\",\"dataCoverage\":{\"totalEntries\":70,\"totalSpanDays\":69,\"newestEntryDaysAgo\":0},\"mood\":{\"signal\":{\"metric\":\"deine Stimmung\",\"unit\":\"/5\",\"current\":3.15,\"currentWindowDays\":20,\"baseline\":3.24,\"baselineLabel\":\"your recent-weeks average\",\"delta\":-0.09,\"deltaPct\":-2.7,\"spread\":0.75,\"outsideNormalSwing\":false,\"direction\":\"higher-better\",\"n\":20,\"newestDaysAgo\":0,\"normalRange\":{\"low\":3.5,\"high\":5},\"placement\":\"below band\"},\"summary\":{\"points\":70,\"start\":1,\"end\":2,\"delta\":1,\"mean\":3.03,\"min\":1,\"max\":5},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W15\",\"min\":2,\"max\":5,\"mean\":3.33,\"n\":6},{\"weekISO\":\"2026-W16\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":1,\"max\":5,\"mean\":3,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":1,\"max\":4,\"mean\":2.57,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1}],\"monthly\":[],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":0,\"value\":1,\"deltaToPreviousDailyPoint\":-2},\"target\":{\"greenMin\":3.5,\"greenMax\":5,\"orangeMin\":2,\"orangeMax\":3.5,\"inTargetPctLast30DailyPoints\":40},\"tags\":null,\"tagInfluence\":{\"flat\":null,\"structured\":[{\"tag\":\"alcohol\",\"labelKey\":\"mood.tag.alcohol\",\"label\":null,\"categoryKey\":\"health\",\"icon\":\"Wine\",\"withDays\":24,\"withoutDays\":46,\"withAvg\":2.96,\"withoutAvg\":3.07,\"delta\":-0.11,\"pValue\":0.7704186651066071,\"confidence\":\"low\"}]},\"betterDays\":[{\"source\":\"metric\",\"key\":\"weight\",\"labelKey\":null,\"label\":null,\"categoryKey\":null,\"icon\":null,\"direction\":\"up\",\"n\":35,\"confidence\":\"medium\",\"effectSize\":0.523,\"delta\":null,\"r\":0.523},{\"source\":\"tag\",\"key\":\"alcohol\",\"labelKey\":\"mood.tag.alcohol\",\"label\":null,\"categoryKey\":\"health\",\"icon\":\"Wine\",\"direction\":\"down\",\"n\":24,\"confidence\":\"low\",\"effectSize\":0.07670850767085077,\"delta\":-0.11,\"r\":null}],\"factorCrosstab\":null,\"narratives\":[{\"kind\":\"trend\",\"messageKey\":\"insights.mood.narrative.trendUp\",\"vars\":{\"direction\":\"up\",\"delta\":\"0.9\",\"days\":\"7\"},\"strength\":0.86},{\"kind\":\"weekday-dip\",\"messageKey\":\"insights.mood.narrative.weekdayDip\",\"vars\":{\"weekdayKey\":\"charts.weekdaysFull.mon\",\"delta\":\"0.3\"},\"strength\":0.33},{\"kind\":\"in-target\",\"messageKey\":\"insights.mood.narrative.inTarget\",\"vars\":{\"pct\":\"40\"},\"strength\":0.2},{\"kind\":\"streak\",\"messageKey\":\"insights.mood.narrative.streak\",\"vars\":{\"days\":\"70\"},\"strength\":0.15}],\"timeOfDay\":{\"buckets\":[{\"bucket\":\"morning\",\"avgScore\":2.91,\"count\":35},{\"bucket\":\"afternoon\",\"avgScore\":null,\"count\":0},{\"bucket\":\"evening\",\"avgScore\":null,\"count\":0},{\"bucket\":\"night\",\"avgScore\":3.14,\"count\":35}],\"best\":\"night\",\"worst\":\"morning\"},\"stability\":{\"score\":6,\"stdDev\":1.414,\"band\":\"veryVariable\",\"days\":70}},\"crossMetricContext\":{\"weight\":{\"summary\":{\"points\":70,\"start\":84.2,\"end\":82.38,\"delta\":-1.82,\"mean\":83.19,\"min\":82.03,\"max\":84.48},\"correlation\":{\"r\":0.523,\"strength\":\"moderat\",\"n\":35}},\"bloodPressureSystolic\":{\"summary\":{\"points\":94,\"start\":128,\"end\":125,\"delta\":-3,\"mean\":125.39,\"min\":116,\"max\":134},\"correlation\":null},\"pulse\":{\"summary\":{\"points\":140,\"start\":70,\"end\":72,\"delta\":2,\"mean\":70.37,\"min\":65,\"max\":75},\"correlation\":null}}}" + ] + } + ], + "temperature": 0.45, + "maxTokens": 250, + "seed": 1618033, + "responseFormat": "json", + "timeoutMs": 60000 + } + ], + "enqueued": [], + "result": { + "hasProvider": false, + "text": "Stable, comme d’habitude. Ton humeur est à 3,2 en ce moment — en dessous de ta moyenne habituelle de 3,2. C’est dans tes repères habituels — rien qui appelle une réaction maintenant.", + "cached": true, + "updatedAt": null + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "mood", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "96cac39d4f62d35aa78b2eb75dfcda28ae0640443fa5d848feac7a97c08852d4", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "mood", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "4725ab784136da8774685c2314e10f49ac53a0e2ed524e97b1d9bb62cf3f2176", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "mood", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + } + ] + }, + "blocked-es": { + "calls": [ + { + "system": [ + "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", + "", + "YOUR DATA (graded snapshot):", + "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", + "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", + "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", + "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", + "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", + "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", + "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", + "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", + "", + "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", + "Moves available (choose the few that genuinely apply):", + "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", + "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", + "(c) name a genuine, earned win or a steady streak when the data shows one;", + "(d) one watch-item worth keeping an eye on;", + "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", + "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", + "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", + "", + "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", + "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", + "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", + "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", + "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", + "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", + "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", + "", + "OPENING — lead with meaning, not the metric value", + "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", + "", + "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", + "", + "JUDGE THE DATA HONESTLY (never invent a trend):", + "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", + "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", + "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", + "", + "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", + "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", + "", + "METRIC IDENTIFIERS STAY OUT OF PROSE", + "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", + "", + "GLP-1 DOSE SAFETY (contract, not style)", + "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", + "", + "ACUTE RED FLAGS (contract, not style)", + "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", + "", + "GROUNDING — ZERO HALLUCINATIONS", + "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", + "", + "TONE — a warm, motivating advisor, never clinical or alarming", + "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", + "", + "NOT AN INSIGHT — never recite mechanics", + "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", + "", + "INTERPRET, DON'T ENUMERATE", + "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", + "", + "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", + "", + "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", + "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", + "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", + "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", + "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", + "", + "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in Spanish: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", + "{ \"summary\": \"...\" }", + "", + "METRIC — MOOD / WELL-BEING:", + "- Scale 1 (very bad) to 5 (very good). The snapshot carries mood.signal (the finished comparison) + mood.summary + mood.series (graded daily means), mood.target (green/orange band), mood.latestDayFocus and, where present, mood.tags (most frequent mood tags).", + "- The weekly pattern matters: lead from mood.signal — current, delta vs baselineLabel and outsideNormalSwing are already computed. STATE them, do NOT recompute. Treat outsideNormalSwing: false as \"inside the usual swing — not a finding\".", + "- Name persistent phases seriously but calmly: several days below ~2.5 or above ~4.5 are worth noting — as an observation, never a diagnosis and never alarming.", + "- When mood stays very low for several days (clearly below ~2.5), offer a support pathway calmly and in an autonomy-preserving way — e.g. \"if the strain persists, talking it through with someone you trust or a professional can help\". Never alarming, never a diagnosis, no emergency tone; a gentle option, not an imperative.", + "- Tags: if mood.tags is present, you may name a recurring tag as possible context, without claiming a link.", + "- Cross-metric: crossMetricContext carries correlations to weight, blood pressure and pulse; mention only when present and |r| > 0.4 — as an association, never a cause. Do not force a cross-link without a clear pattern.", + "- One message: close with ONE doable, kind step ONLY when the finding implies one (e.g. on good days, briefly note what helped). When mood is steadily good, acknowledge that honestly and name one thing worth keeping an eye on instead of manufacturing a step.", + "", + "OUTPUT LANGUAGE: Responda en español (registro peninsular formal — usted). Use un vocabulario sanitario en español natural." + ], + "messages": [ + { + "role": "user", + "content": [ + "Date: 2026-06-15", + "OPENER HINT: Open with what this means for the user today, then the number behind it.", + "Write one short assessment of this person's mood. Open with what the recent pattern MEANS in plain words — the read, not the number (e.g. \"a brighter stretch than usual\", \"steady, holding where it's been\") — then bring in the recent level right after as support, placed against their own weekly/monthly baseline; never lead with the value. Close with one kind, doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the entry count and recency.", + "", + "PREVIOUS ANALYSIS: none on file. Treat this as the user's first analysis for this domain — no improvement/regression delta to surface.", + "", + "", + "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der jüngsten Richtung (steigend / fallend / flach gegenüber der Baseline) eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", + "", + "DATENLAGE: 70 Tages-Buckets im Fenster, letzter Wert heute.", + "", + "", + "{\"locale\":\"es\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"mood\",\"dataCoverage\":{\"totalEntries\":70,\"totalSpanDays\":69,\"newestEntryDaysAgo\":0},\"mood\":{\"signal\":{\"metric\":\"deine Stimmung\",\"unit\":\"/5\",\"current\":3.15,\"currentWindowDays\":20,\"baseline\":3.24,\"baselineLabel\":\"your recent-weeks average\",\"delta\":-0.09,\"deltaPct\":-2.7,\"spread\":0.75,\"outsideNormalSwing\":false,\"direction\":\"higher-better\",\"n\":20,\"newestDaysAgo\":0,\"normalRange\":{\"low\":3.5,\"high\":5},\"placement\":\"below band\"},\"summary\":{\"points\":70,\"start\":1,\"end\":2,\"delta\":1,\"mean\":3.03,\"min\":1,\"max\":5},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W15\",\"min\":2,\"max\":5,\"mean\":3.33,\"n\":6},{\"weekISO\":\"2026-W16\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":1,\"max\":5,\"mean\":3,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":1,\"max\":4,\"mean\":2.57,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1}],\"monthly\":[],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":0,\"value\":1,\"deltaToPreviousDailyPoint\":-2},\"target\":{\"greenMin\":3.5,\"greenMax\":5,\"orangeMin\":2,\"orangeMax\":3.5,\"inTargetPctLast30DailyPoints\":40},\"tags\":null,\"tagInfluence\":{\"flat\":null,\"structured\":[{\"tag\":\"alcohol\",\"labelKey\":\"mood.tag.alcohol\",\"label\":null,\"categoryKey\":\"health\",\"icon\":\"Wine\",\"withDays\":24,\"withoutDays\":46,\"withAvg\":2.96,\"withoutAvg\":3.07,\"delta\":-0.11,\"pValue\":0.7704186651066071,\"confidence\":\"low\"}]},\"betterDays\":[{\"source\":\"metric\",\"key\":\"weight\",\"labelKey\":null,\"label\":null,\"categoryKey\":null,\"icon\":null,\"direction\":\"up\",\"n\":35,\"confidence\":\"medium\",\"effectSize\":0.523,\"delta\":null,\"r\":0.523},{\"source\":\"tag\",\"key\":\"alcohol\",\"labelKey\":\"mood.tag.alcohol\",\"label\":null,\"categoryKey\":\"health\",\"icon\":\"Wine\",\"direction\":\"down\",\"n\":24,\"confidence\":\"low\",\"effectSize\":0.07670850767085077,\"delta\":-0.11,\"r\":null}],\"factorCrosstab\":null,\"narratives\":[{\"kind\":\"trend\",\"messageKey\":\"insights.mood.narrative.trendUp\",\"vars\":{\"direction\":\"up\",\"delta\":\"0.9\",\"days\":\"7\"},\"strength\":0.86},{\"kind\":\"weekday-dip\",\"messageKey\":\"insights.mood.narrative.weekdayDip\",\"vars\":{\"weekdayKey\":\"charts.weekdaysFull.mon\",\"delta\":\"0.3\"},\"strength\":0.33},{\"kind\":\"in-target\",\"messageKey\":\"insights.mood.narrative.inTarget\",\"vars\":{\"pct\":\"40\"},\"strength\":0.2},{\"kind\":\"streak\",\"messageKey\":\"insights.mood.narrative.streak\",\"vars\":{\"days\":\"70\"},\"strength\":0.15}],\"timeOfDay\":{\"buckets\":[{\"bucket\":\"morning\",\"avgScore\":2.91,\"count\":35},{\"bucket\":\"afternoon\",\"avgScore\":null,\"count\":0},{\"bucket\":\"evening\",\"avgScore\":null,\"count\":0},{\"bucket\":\"night\",\"avgScore\":3.14,\"count\":35}],\"best\":\"night\",\"worst\":\"morning\"},\"stability\":{\"score\":6,\"stdDev\":1.414,\"band\":\"veryVariable\",\"days\":70}},\"crossMetricContext\":{\"weight\":{\"summary\":{\"points\":70,\"start\":84.2,\"end\":82.38,\"delta\":-1.82,\"mean\":83.19,\"min\":82.03,\"max\":84.48},\"correlation\":{\"r\":0.523,\"strength\":\"moderat\",\"n\":35}},\"bloodPressureSystolic\":{\"summary\":{\"points\":94,\"start\":128,\"end\":125,\"delta\":-3,\"mean\":125.39,\"min\":116,\"max\":134},\"correlation\":null},\"pulse\":{\"summary\":{\"points\":140,\"start\":70,\"end\":72,\"delta\":2,\"mean\":70.37,\"min\":65,\"max\":75},\"correlation\":null}}}" + ] + } + ], + "temperature": 0.45, + "maxTokens": 250, + "seed": 1618033, + "responseFormat": "json", + "timeoutMs": 60000 + } + ], + "enqueued": [], + "result": { + "hasProvider": false, + "text": "Estable, como de costumbre. Tu estado de ánimo está en 3,2 ahora mismo — por debajo de tu promedio habitual de 3,2. Eso queda dentro de tu rango habitual — nada que pida una reacción ahora.", + "cached": true, + "updatedAt": null + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "mood", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "96cac39d4f62d35aa78b2eb75dfcda28ae0640443fa5d848feac7a97c08852d4", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "mood", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "4725ab784136da8774685c2314e10f49ac53a0e2ed524e97b1d9bb62cf3f2176", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "mood", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "mood", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + } + ] + }, + "bare-error-en": { + "calls": [ + { + "system": [ + "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", + "", + "YOUR DATA (graded snapshot):", + "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", + "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", + "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", + "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", + "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", + "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", + "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", + "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", + "", + "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", + "Moves available (choose the few that genuinely apply):", + "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", + "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", + "(c) name a genuine, earned win or a steady streak when the data shows one;", + "(d) one watch-item worth keeping an eye on;", + "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", + "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", + "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", + "", + "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", + "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", + "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", + "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", + "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", + "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", + "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", + "", + "OPENING — lead with meaning, not the metric value", + "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", + "", + "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", + "", + "JUDGE THE DATA HONESTLY (never invent a trend):", + "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", + "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", + "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", + "", + "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", + "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", + "", + "METRIC IDENTIFIERS STAY OUT OF PROSE", + "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", + "", + "GLP-1 DOSE SAFETY (contract, not style)", + "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", + "", + "ACUTE RED FLAGS (contract, not style)", + "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", + "", + "GROUNDING — ZERO HALLUCINATIONS", + "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", + "", + "TONE — a warm, motivating advisor, never clinical or alarming", + "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", + "", + "NOT AN INSIGHT — never recite mechanics", + "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", + "", + "INTERPRET, DON'T ENUMERATE", + "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", + "", + "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", + "", + "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", + "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", + "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", + "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", + "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", + "", + "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in English: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", + "{ \"summary\": \"...\" }", + "", + "METRIC — MOOD / WELL-BEING:", + "- Scale 1 (very bad) to 5 (very good). The snapshot carries mood.signal (the finished comparison) + mood.summary + mood.series (graded daily means), mood.target (green/orange band), mood.latestDayFocus and, where present, mood.tags (most frequent mood tags).", + "- The weekly pattern matters: lead from mood.signal — current, delta vs baselineLabel and outsideNormalSwing are already computed. STATE them, do NOT recompute. Treat outsideNormalSwing: false as \"inside the usual swing — not a finding\".", + "- Name persistent phases seriously but calmly: several days below ~2.5 or above ~4.5 are worth noting — as an observation, never a diagnosis and never alarming.", + "- When mood stays very low for several days (clearly below ~2.5), offer a support pathway calmly and in an autonomy-preserving way — e.g. \"if the strain persists, talking it through with someone you trust or a professional can help\". Never alarming, never a diagnosis, no emergency tone; a gentle option, not an imperative.", + "- Tags: if mood.tags is present, you may name a recurring tag as possible context, without claiming a link.", + "- Cross-metric: crossMetricContext carries correlations to weight, blood pressure and pulse; mention only when present and |r| > 0.4 — as an association, never a cause. Do not force a cross-link without a clear pattern.", + "- One message: close with ONE doable, kind step ONLY when the finding implies one (e.g. on good days, briefly note what helped). When mood is steadily good, acknowledge that honestly and name one thing worth keeping an eye on instead of manufacturing a step." + ], + "messages": [ + { + "role": "user", + "content": [ + "Date: 2026-06-15", + "OPENER HINT: Open with what this means for the user today, then the number behind it.", + "Write one short assessment of this person's mood. Open with what the recent pattern MEANS in plain words — the read, not the number (e.g. \"a brighter stretch than usual\", \"steady, holding where it's been\") — then bring in the recent level right after as support, placed against their own weekly/monthly baseline; never lead with the value. Close with one kind, doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the entry count and recency.", + "", + "PREVIOUS ANALYSIS: none on file. Treat this as the user's first analysis for this domain — no improvement/regression delta to surface.", + "", + "", + "VARIETY: to keep your cards from reading the same, you may open this one by leading with the single most recent value and where it sits — only if the data genuinely supports that angle; never force it, never invent a trend.", + "", + "DATA STRENGTH: 0 day-buckets in the window, no dated reading. — too few points for a reliable trend; hedge accordingly and avoid claiming a direction.", + "", + "", + "{\"locale\":\"en\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"mood\",\"dataCoverage\":{\"totalEntries\":0,\"totalSpanDays\":0,\"newestEntryDaysAgo\":null},\"mood\":{\"summary\":null,\"series\":{\"recent\":[],\"weekly\":[],\"monthly\":[],\"yearly\":[]},\"latestDayFocus\":null,\"target\":{\"greenMin\":3.5,\"greenMax\":5,\"orangeMin\":2,\"orangeMax\":3.5,\"inTargetPctLast30DailyPoints\":null},\"tags\":null,\"tagInfluence\":null,\"betterDays\":null,\"factorCrosstab\":null,\"narratives\":null,\"timeOfDay\":null,\"stability\":null},\"crossMetricContext\":null}" + ] + } + ], + "temperature": 0.45, + "maxTokens": 250, + "seed": 1618033, + "responseFormat": "json", + "timeoutMs": 60000 + } + ], + "enqueued": [], + "result": { + "hasProvider": false, + "text": "No assessment on this one right now. Mood reads over weeks rather than single days — recurring patterns, and how they line up with your other metrics, are what carry. A sustained low stretch is the one that deserves attention.", + "cached": true, + "updatedAt": null + }, + "notes": [ + { + "userId": "parity-bare", + "metric": "mood", + "locale": "en", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "error" + }, + { + "userId": "parity-rich", + "metric": "mood", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "96cac39d4f62d35aa78b2eb75dfcda28ae0640443fa5d848feac7a97c08852d4", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "mood", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "4725ab784136da8774685c2314e10f49ac53a0e2ed524e97b1d9bb62cf3f2176", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "mood", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "mood", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + } + ] + }, + "bare-error-de": { + "calls": [ + { + "system": [ + "Du schreibst eine kurze, persönliche Einschätzung zu EINER Gesundheitsmetrik dieses Nutzers. Du kennst seine Daten und ordnest sie gegen seine EIGENE Baseline ein — nicht gegen Bevölkerungsnormen. Deine fachliche Grundlage sind anerkannte Leitlinien (ESH 2023, WHO, AASM, DGE, DEGAM); du beziehst dich darauf, ohne zu diagnostizieren.", + "", + "DEINE DATENGRUNDLAGE (graded snapshot):", + "- signal: der fertige Vergleich — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` ist das recent-Mittel; `baseline` ist der EIGENE längere Durchschnitt; `delta`/`deltaPct` sind die VORZEICHENBEHAFTETE Änderung gegenüber dieser Baseline; `spread` ist die normale Schwankung; `outsideNormalSwing` ist das vorab berechnete Urteil, ob die Änderung diese Schwankung übersteigt. Diese Werte sind bereits berechnet — nenne sie, rechne sie NICHT neu. `contributors[]` (nur Score-Komposite) benennt die 1–2 Teil-Scores, die den Score bewegt haben.", + "- summary: { points, start, end, delta, mean, min, max } — die Eckwerte des gesamten Lesefensters.", + "- series.recent[]: die jüngsten ~21 Tage, je ein Eintrag pro Tag ({ date, min, max, mean, n }).", + "- series.weekly[]: die ~10 Wochen davor, je ein ISO-Wochen-Aggregat.", + "- series.monthly[]: die ~12 Monate davor, je ein Monatsaggregat.", + "- series.yearly[]: alles ältere, je ein Jahr ({ year, mean, min, max, n, slope }).", + "- Die jüngsten Tage liegen also EINZELN vor, ältere Zeiträume als Wochen-, Monats- und Jahresaggregate. Ziehe die series nur für einen konkreten Tag heran; führe sonst mit dem signal-Block.", + "- Ein Slice kann leer sein, wenn die Historie kurz ist. Dann nicht so tun, als gäbe es ihn. Ist `signal.baseline` null, gibt es noch keine etablierte Baseline; sage das ehrlich, statt einen Vergleich zu erfinden.", + "", + "SO BAUST DU DIE EINSCHÄTZUNG — wähle die 2–3 BAUSTEINE, die zu DIESEN Daten passen, und ordne sie so, dass sie als EIN zusammenhängender Gedanke lesen, keine feste Checkliste. Führe gemäß dem OPENER-HINWEIS im User-Prompt, wenn einer mitgegeben ist — eröffne NICHT immer mit der Zahl. Variiere die Form von Metrik zu Metrik; eröffne nie zwei Metriken hintereinander gleich.", + "Verfügbare Bausteine (nimm die wenigen, die wirklich zutreffen):", + "(a) der aktuelle Befund — `current` aus dem signal-Block (oder ein Wert aus series.recent für eine Tagesfrage);", + "(b) gegen die EIGENE Baseline einordnen über das vorab berechnete vorzeichenbehaftete `delta` gegenüber `baselineLabel` — nenne es, leite es nicht neu her. Behandle `outsideNormalSwing: false` als \"innerhalb der üblichen Schwankung — kein Befund\" und ERFINDE KEINEN Trend; `outsideNormalSwing: true` ist die echte, meldenswerte Änderung. Ein `normalRange`/`placement` ist nur ein grober sekundärer Anker — das persönliche delta führt;", + "(c) einen echten, verdienten Erfolg oder eine stabile Serie benennen, wenn die Daten ihn zeigen;", + "(d) einen Punkt zum Im-Auge-behalten;", + "(e) einen Zusammenhang NUR, wenn ein r-Wert vorhanden und |r| > 0,4 ist, formuliert als \"bewegt sich mit\" / \"zeigt sich oft zusammen mit\", nie kausal;", + "(f) GENAU EINEN machbaren nächsten Schritt — und nur, wenn der Befund wirklich einen hergibt. Eine Botschaft = ein Verhalten, keine Liste. Ist der Wert stabil und im grünen Bereich, ERZWINGE KEINEN Schritt — bestätige kurz und nenne stattdessen einen Punkt zum Beobachten. Ein erfundener Schritt ist genau die Floskel, die wir vermeiden.", + "Synthese statt Aufzählung: die Geschichte dessen, was die Daten bedeuten, zählt mehr als das Wiederholen jeder Zahl.", + "", + "TONALITÄT — ein warmer, motivierender Premium-Begleiter (jemand hat sich die Daten dieser Person wirklich angesehen und etwas Wertvolles für sie gefunden):", + "- Zweite Person (\"dein Blutdruck\", \"deine Werte\"), warm, direkt, ehrlich.", + "- Motivierend und vorwärtsgewandt: Wenn die Daten es hergeben, benenne den echten Erfolg klar und baue ein wenig Schwung auf — die Person soll sich gesehen und unterstützt fühlen, nicht belehrt. Die Ermutigung muss durch die Zahlen VERDIENT sein, nie ein reflexhaftes Kompliment.", + "- Autonomie-unterstützend: \"kann helfen\", \"einen Versuch wert\", nie \"du musst\".", + "- Nie alarmierend, nie moralisierend, nie diagnostisch — keine Krankheitsbehauptung. Formuliere als begründete Beobachtung, nicht als ärztlichen Rat.", + "- Auch ungünstige Werte ehrlich benennen: Befund → gegen die eigene Baseline einordnen → ein kleiner machbarer Schritt, als Chance formuliert, nicht als Versagen. Nicht verharmlosen, nicht dramatisieren.", + "- Keine Floskeln und keine bloße Zahlenwiederholung: jede warme Zeile ist an eine echte Zahl oder eine echte Veränderung geknüpft. Ein generischer Positiv-Einstieg (\"Deine Werte sehen gut aus\") ist verboten — verdiene die Ermutigung mit dem konkreten Befund.", + "", + "ERÖFFNUNG — mit der Bedeutung führen, nicht mit dem Messwert", + "Beginne mit dem Eindruck in klaren Worten — was das heute über die Person sagt — und bring die Zahl direkt danach als Beleg, nie als Erstes. Eine Zeile, die mit einem Wert eröffnet (\"Dein Ruhepuls ist 61 …\"), liest sich wie ein Ablesen; dieselbe Zeile, die mit der Bedeutung eröffnet (\"Dein Herz läuft diese Woche etwas ruhiger — rund 61 …\"), liest sich wie ein Begleiter, der wirklich hingesehen hat. Jede Aussage bleibt an einer echten Zahl verankert; nur die REIHENFOLGE ändert sich, Bedeutung zuerst und die Zahl direkt dahinter. Das gilt, ob die Fläche eine Metrik abdeckt oder viele.", + "", + "LÄNGE: 2-4 Sätze, ca. 30-60 Wörter — ist der Wert aber stabil und nichts zu tun, ist EIN knapper Satz besser, als ihn auf Länge zu strecken. Knapp und hochwertig. Keine bloße Zahlenwiederholung, kein Fülltext, keine generischen Floskeln.", + "", + "DATENLAGE EHRLICH BEWERTEN (nie einen Trend erfinden):", + "- Nur wenige Messpunkte/Einträge/Ereignisse oder ein leerer recent-Slice: ehrlich sagen, dass es für eine belastbare Tendenz noch zu wenige Daten sind, und EINEN Hinweis geben (z.B. regelmäßiger erfassen). Keinen Trend behaupten. Was \"wenige\" bedeutet, hängt von der Metrik ab — bei Stimmung sind es Einträge, bei der Einnahmetreue geplante Dosen.", + "- Jüngste Messung deutlich älter als ~7 Tage (dataCoverage.newestMeasurementDaysAgo): darauf hinweisen, dass die Werte nicht mehr aktuell sind.", + "- Korrelationen NUR erwähnen, wenn der r-Wert im Snapshot vorhanden ist und |r| > 0.4 ist. Fehlt das Feld, keine Korrelation interpretieren oder erfinden. Immer als \"Zusammenhang\" formulieren, nie als \"Ursache\".", + "", + "VERBOTENE FLOSKELN (signalisieren ungegroundeten Fülltext — nie ausgeben, außer im Disclaimer)", + "\"achte auf ausreichend Schlaf\", \"trinke genug Wasser\", \"regelmäßige Bewegung\", \"ärztlicher Rat empfohlen\".", + "", + "METRIK-IDENTIFIER GEHÖREN NICHT IN DEN FLIESSTEXT", + "Schreibe niemals Datenbank- bzw. Enum-Namen wie \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" oder \"SLEEP_DURATION\" in nutzersichtbare Strings, und schreibe nie das wörtliche \"metric:\"-Chart-Token in den Fließtext. Verweise auf jede Metrik mit der natürlichsprachlichen Bezeichnung, die der Nutzer sieht — \"deine Systole\", \"dein Gewicht\", \"dein Puls\", \"deine Stimmung\", \"deine Medikamentenadhärenz\", \"dein Ruhepuls\", \"deine Schlafdauer\", \"deine Schritte\".", + "", + "GLP-1-DOSIS-SICHERHEIT (Vertrag, kein Stil)", + "Verschreibe, empfiehl oder ändere NIEMALS Medikamenten-Dosen, auch wenn der Snapshot einen GLP-1-Rezeptoragonisten benennt (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). Du darfst den benannten Wirkstoff und die aktuelle Titrationsstufe NENNEN (\"Woche 3 auf 7,5 mg\"), wenn der Snapshot sie trägt, aber schreibe nie \"erhöhe auf X mg\", \"erwäge die nächste Stufe Y mg\", \"bleibe auf Z mg\" oder eine Variante davon. Ein Plateau rahmt die nächste Entscheidung immer als Gespräch mit der behandelnden Ärztin. Bist du unsicher, ob etwas dosis-präskriptiv ist, behandle es so — und verweise an die Klinik.", + "", + "AKUTE WARNZEICHEN (Vertrag, kein Stil)", + "Beschreibt der Nutzer ein akutes Krisenzeichen — Brustschmerz oder Druck auf der Brust, Ohnmacht oder Beinahe-Ohnmacht (Synkope), ein plötzliches schweres Symptom (z. B. stärkster Kopfschmerz aller Zeiten, plötzliche Schwäche oder Sprachstörung, Atemnot), einen hypertensiven Notfallwert (systolisch ≥ 180 oder diastolisch ≥ 120 mit Symptomen), einen schwer-niedrigen oder sehr hohen Glukosewert (unter 54 mg/dL (3,0 mmol/L), oder unter 70 mg/dL (3,9 mmol/L) mit Symptomen, oder ≥ 250 mg/dL (13,9 mmol/L) mit Symptomen), anhaltendes Fieber ≥ 38,5 °C (101,3 °F) oder Gedanken an Suizid bzw. Selbstverletzung — sage in einem ruhigen Satz klar, dass das jetzt umgehende ärztliche Hilfe oder den Notruf braucht, und coache es NICHT anhand der Daten. Diagnostiziere nicht, benenne keine Erkrankung, schätze keinen Schweregrad aus den Zahlen — verweise nur auf umgehende Hilfe bzw. den Notruf und höre auf. Das ist eine geschlossene Liste; außerhalb davon bleibe wie gewohnt ruhig und nicht alarmierend.", + "", + "ERDUNG — NULL HALLUZINATIONEN", + "Jede Aussage muss auf einer Zahl beruhen, die im übergebenen Snapshot sichtbar ist. Lässt sich die Aussage keinem Snapshot-Feld zuordnen, lass sie weg und erfinde nie einen Messwert, um einer Anfrage zu entsprechen. Ordne jeden Wert gegen die EIGENE Baseline des Nutzers ein (jüngeres vs. längeres Mittel), nie gegen eine Bevölkerungsnorm. Nenne einen Zusammenhang nur, wenn der r-Wert vorhanden und |r| > 0,4 ist — immer als \"Zusammenhang\", nie als \"Ursache\".", + "", + "TONALITÄT — ein warmer, motivierender Begleiter, nie klinisch oder alarmierend", + "Schreibe in der zweiten Person, warm und direkt. Wenn die Daten es hergeben, benenne den echten Erfolg klar und baue ein wenig Schwung auf — die Person soll sich gesehen und unterstützt fühlen, nicht belehrt. Die Ermutigung muss durch die Zahlen VERDIENT sein, nie ein reflexhaftes Kompliment. Lob ist verankert, konkret und angemessen: an einer echten Zahl oder Veränderung festgemacht, nicht auf andere Nutzer übertragbar, und stiller Anerkennung für stille Erfolge. Über-Bestätigung ist eine Sicherheitsregression, keine Wärme — bestätige nie einen sich verschlechternden Trend oder eine unsichere Entscheidung; würdige die Mühe, nicht die Entscheidung, und bleib neutral (\"nichts zu tun — die gute Art von langweilig\"), wenn es nichts zu loben gibt, statt ein Kompliment zu erfinden. Sei autonomie-unterstützend (\"einen Versuch wert\", \"kann helfen\", nie \"du musst\"). Benenne auch ungünstige Werte ehrlich — Befund, dann gegen die eigene Baseline einordnen, dann ein kleiner machbarer Schritt, als Chance formuliert. Nie alarmierend, nie moralisierend, nie diagnostisch. Keine Floskeln und keine bloße Zahlenwiederholung. Beginne NICHT mit einem Kompliment über Datenmenge oder -qualität; verbotene Eröffnungen sind u.a. \"Datengrundlage ist sehr stark\", \"Your data foundation is strong\", \"Du hast eine solide Baseline\", \"Großartiger Datensatz\", ein generisches \"Deine Werte sehen gut aus\" und jede sinngemäße Umformulierung.", + "", + "KEINE EINSCHÄTZUNG — nie Mechanik nacherzählen", + "Eine Messanzahl (\"3-mal erfasst\"), ein Erfassungsrhythmus (\"alle paar Tage gemessen\") oder Bedien-Mechanik ist KEINE Einschätzung und gehört für sich genommen nicht in den Text. Nutze eine solche Angabe NUR, wenn sie eine Konsequenz trägt, und nenne dann die Konsequenz, nicht die Zahl: z. B. \"diese Woche zu wenige Werte für eine Tendenz — eine Morgenmessung würde es klären\", nicht \"du hast diese Woche zweimal gemessen\". Jeder Satz muss sich verdienen, indem er sagt, was ein Wert BEDEUTET oder was daraus folgt — nie, indem er erzählt, wie die Daten erhoben wurden.", + "", + "EINORDNEN, NICHT AUFZÄHLEN", + "Sag, was ein Wert BEDEUTET, nicht nur, was er ist. Trägt der User-Prompt einen EINORDNUNGS-KONTEXT-Block, nutze ihn: benenne, wo der Wert auf der Leitlinien-Skala liegt, was dieses Band in klaren, würdevollen Worten bedeutet und was praktisch folgt. Beurteile den Trend IM KONTEXT — ein \"leicht steigender\" Wert tief in einem günstigen Band ist eine Randnotiz; dieselbe Bewegung an oder nahe einer Grenze ist die Schlagzeile. Rahme jede Konsequenz OHNE Diagnose: \"Werte in diesem Bereich gelten laut Leitlinien als …\", nie \"du hast X\" oder \"du bist gefährdet für X\". Ist KEIN Einordnungs-Block vorhanden, hat die Metrik kein allgemeines Referenzband — ordne sie gegen die EIGENE Baseline der Person ein und sage klar, dass kein allgemeines Referenzband hinterlegt ist, statt eines zu erfinden.", + "", + "FORMATIERUNG — schreibe in kurzen Absätzen, getrennt durch eine LEERZEILE, je 1–3 Sätze; eine längere Antwort hat 2–4 Absätze, nie einen Block. Setze eine Leerzeile zwischen unterschiedliche Gedanken (z. B. \"wie es steht\" vs. \"eine Sache zum Ausprobieren\"). Ein stabiler Einzeiler bleibt EIN Absatz — strecke nicht auf einen zweiten. Wenn du wirklich drei oder mehr parallele Punkte aufzählst (Optionen, Befunde, Schritte), formatiere sie als schlichte Liste: ein Punkt pro Zeile, jede Zeile beginnt mit \"- \". Die EINE wichtigste Kernaussage darfst du mit **doppelten Sternchen** fett setzen, höchstens einmal pro Antwort. Sonst nichts: keine Überschriften, kein Kursiv, keine Backticks, keine nummerierten Listen, keine Emojis.", + "", + "BEISPIELE — beachte die UNTERSCHIEDLICHEN Formen (urteil-zuerst, trend-zuerst, Einzeiler). Sie illustrieren nur Form und Erdung; übernimm sie nie — jede Einschätzung nutzt die echten Snapshot-Zahlen:", + "- URTEIL-ZUERST (Bedeutung zuerst, Zahl als Beleg, ein Schritt): \"Dein Ruhepuls läuft gerade einen Tick niedriger als sonst — diese Woche rund 61 bpm, 5 unter deinem Monatsmittel von 66 und dein niedrigster seit Wochen. So ein Rückgang passt meist zu mehr Bewegung; die Routine, die das gebracht hat, lohnt sich beizubehalten.\"", + "- TREND-ZUERST (Richtung zuerst, dann der Stand): \"Dein systolischer Blutdruck geht seit drei Wochen ruhig nach unten, aktuell rund 124 mmHg — etwa 4 unter deinem 30-Tage-Schnitt. Nichts Dramatisches, einfach eine stetige Richtung.\"", + "- EINZEILER (stabil, KEIN erzwungener Schritt): \"Deine SpO₂ ist mit 97 % stabil und genau in deinem üblichen Bereich — nichts zu tun, die gute Art von langweilig.\"", + "- SCHLECHT (verbotene Floskel, ungegroundet — so NICHT): \"Deine Werte sehen gut aus. Achte auf ausreichend Schlaf und regelmäßige Bewegung.\"", + "", + "AUSGABEFORMAT: Antworte ausschließlich mit validem JSON in genau diesem Schema. Das Feld \"summary\" enthält die komplette Einschätzung auf Deutsch: 1-3 kurze Absätze mit je 1-3 Sätzen, getrennt durch eine Leerzeile als \\n\\n INNERHALB des JSON-Strings. Ein stabiler Einzeiler bleibt EIN Absatz:", + "{ \"summary\": \"...\" }", + "", + "METRIK — STIMMUNG / WOHLBEFINDEN:", + "- Skala 1 (sehr schlecht) bis 5 (sehr gut). Der Snapshot trägt mood.signal (der fertige Vergleich) + mood.summary + mood.series (graded, Tagesmittel), mood.target (grünes/oranges Band), mood.latestDayFocus und ggf. mood.tags (häufigste Stimmungs-Tags).", + "- Wochenmuster zählt: Führe mit mood.signal — current, delta gegenüber baselineLabel und outsideNormalSwing sind bereits berechnet. NENNE sie, rechne sie NICHT neu. Behandle outsideNormalSwing: false als \"innerhalb der üblichen Schwankung — kein Befund\".", + "- Anhaltende Phasen ernst, aber ruhig benennen: mehrere Tage unter ~2.5 oder über ~4.5 sind erwähnenswert — als Beobachtung, nie als Diagnose und nie alarmierend.", + "- Bei anhaltend sehr niedriger Stimmung (mehrere Tage deutlich unter ~2.5): biete ruhig und autonomie-wahrend einen Unterstützungs-Pfad an — etwa \"bei anhaltender Belastung kann ein Gespräch mit einer Vertrauensperson oder einer Fachperson helfen\". Nie alarmierend, nie als Diagnose, kein Notfall-Ton; eine sanfte Option, kein Imperativ.", + "- Tags: Falls mood.tags vorhanden, kannst du ein wiederkehrendes Tag als möglichen Kontext nennen, ohne einen Zusammenhang zu behaupten.", + "- Cross-Metrik: crossMetricContext trägt Korrelationen zu Gewicht, Blutdruck und Puls; nur erwähnen, wenn vorhanden und |r| > 0.4 — als Zusammenhang, nie als Ursache. Erzwinge keinen Querverweis ohne klares Muster.", + "- Eine Botschaft: Schließe NUR DANN mit EINEM machbaren, freundlichen Schritt, wenn der Befund einen nahelegt (z.B. an guten Tagen kurz festhalten, was geholfen hat). Bei stabil guter Stimmung das ehrlich anerkennen und stattdessen einen Punkt nennen, den man im Auge behalten kann, statt einen Schritt zu erzwingen." + ], + "messages": [ + { + "role": "user", + "content": [ + "Datum: 2026-06-15", + "OPENER HINT: Eröffne damit, was das heute für den Nutzer bedeutet, und dann die Zahl dahinter.", + "Schreibe eine kurze Einschätzung zur Stimmung dieser Person. Beginne mit der BEDEUTUNG des jüngsten Musters in klaren Worten — dem Eindruck, nicht der Zahl (z. B. \"eine hellere Phase als sonst\", \"stabil, hält sich, wo sie war\") — und bring danach das jüngste Niveau als Beleg, gegen die eigene Wochen-/Monats-Baseline eingeordnet; führe nie mit dem Wert. Schließe nur dann mit einem freundlichen, machbaren Schritt, wenn der Befund wirklich einen hergibt; ist nichts umsetzbar, lass den Schritt weg statt Fülltext zu erfinden. Konfidenz aus Eintragsanzahl und Aktualität ableiten.", + "", + "VORHERIGE ANALYSE: keine vorhanden. Behandle dies als erste Analyse in diesem Bereich — kein Verbesserungs-/Verschlechterungs-Delta zu nennen.", + "", + "", + "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit dem aktuellsten Einzelwert und seiner Einordnung eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", + "", + "DATENLAGE: 0 Tages-Buckets im Fenster, kein datierter Wert. — zu wenige Punkte für einen belastbaren Trend; entsprechend vorsichtig formulieren, keine Richtung behaupten.", + "", + "", + "{\"locale\":\"de\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"mood\",\"dataCoverage\":{\"totalEntries\":0,\"totalSpanDays\":0,\"newestEntryDaysAgo\":null},\"mood\":{\"summary\":null,\"series\":{\"recent\":[],\"weekly\":[],\"monthly\":[],\"yearly\":[]},\"latestDayFocus\":null,\"target\":{\"greenMin\":3.5,\"greenMax\":5,\"orangeMin\":2,\"orangeMax\":3.5,\"inTargetPctLast30DailyPoints\":null},\"tags\":null,\"tagInfluence\":null,\"betterDays\":null,\"factorCrosstab\":null,\"narratives\":null,\"timeOfDay\":null,\"stability\":null},\"crossMetricContext\":null}" + ] + } + ], + "temperature": 0.45, + "maxTokens": 250, + "seed": 1618033, + "responseFormat": "json", + "timeoutMs": 60000 + } + ], + "enqueued": [], + "result": { + "hasProvider": false, + "text": "Für diese Karte liegt gerade keine Einschätzung vor. Stimmung liest sich über Wochen, nicht über einzelne Tage — wiederkehrende Muster und wie sie zu den anderen Werten passen, sind das, was trägt. Eine anhaltende Phase niedriger Stimmung ist die, die Aufmerksamkeit verdient.", + "cached": true, + "updatedAt": null + }, + "notes": [ + { + "userId": "parity-bare", + "metric": "mood", + "locale": "de", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "error" + }, + { + "userId": "parity-bare", + "metric": "mood", + "locale": "en", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "error" + }, + { + "userId": "parity-rich", + "metric": "mood", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "96cac39d4f62d35aa78b2eb75dfcda28ae0640443fa5d848feac7a97c08852d4", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "mood", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "4725ab784136da8774685c2314e10f49ac53a0e2ed524e97b1d9bb62cf3f2176", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "mood", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "mood", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + } + ] + }, + "keyless-en": { + "calls": [], + "enqueued": [], + "result": { + "hasProvider": false, + "text": "Steady and much as usual. Your mood is at 3.2 right now — lower than your usual average of 3.2. That sits in your usual range — nothing you need to act on right now.", + "cached": true, + "updatedAt": null + }, + "notes": [ + { + "userId": "parity-bare", + "metric": "mood", + "locale": "de", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "error" + }, + { + "userId": "parity-bare", + "metric": "mood", + "locale": "en", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "error" + }, + { + "userId": "parity-rich", + "metric": "mood", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "96cac39d4f62d35aa78b2eb75dfcda28ae0640443fa5d848feac7a97c08852d4", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "mood", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "4725ab784136da8774685c2314e10f49ac53a0e2ed524e97b1d9bb62cf3f2176", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "mood", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "mood", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + } + ] + }, + "keyless-de": { + "calls": [], + "enqueued": [], + "result": { + "hasProvider": false, + "text": "Stabil und wie gewohnt. Deine Stimmung liegt aktuell bei 3,2 — niedriger als dein üblicher Schnitt von 3,2. Das ist in deinem gewohnten Rahmen — nichts, worauf du jetzt reagieren musst.", + "cached": true, + "updatedAt": null + }, + "notes": [ + { + "userId": "parity-bare", + "metric": "mood", + "locale": "de", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "error" + }, + { + "userId": "parity-bare", + "metric": "mood", + "locale": "en", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "error" + }, + { + "userId": "parity-rich", + "metric": "mood", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "96cac39d4f62d35aa78b2eb75dfcda28ae0640443fa5d848feac7a97c08852d4", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "mood", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "4725ab784136da8774685c2314e10f49ac53a0e2ed524e97b1d9bb62cf3f2176", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "mood", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "mood", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + } + ] + }, + "keyless-read-only": { + "calls": [], + "enqueued": [], + "result": { + "hasProvider": false, + "text": "No assessment on this one right now. Mood reads over weeks rather than single days — recurring patterns, and how they line up with your other metrics, are what carry. A sustained low stretch is the one that deserves attention.", + "cached": true, + "updatedAt": null + }, + "notes": [ + { + "userId": "parity-bare", + "metric": "mood", + "locale": "de", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "error" + }, + { + "userId": "parity-bare", + "metric": "mood", + "locale": "en", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "error" + }, + { + "userId": "parity-rich", + "metric": "mood", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "96cac39d4f62d35aa78b2eb75dfcda28ae0640443fa5d848feac7a97c08852d4", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "mood", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "4725ab784136da8774685c2314e10f49ac53a0e2ed524e97b1d9bb62cf3f2176", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "mood", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "mood", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + } + ] + } +} diff --git a/tests/integration/__golden__/status-parity/pulse.json b/tests/integration/__golden__/status-parity/pulse.json new file mode 100644 index 000000000..d1fb79925 --- /dev/null +++ b/tests/integration/__golden__/status-parity/pulse.json @@ -0,0 +1,1535 @@ +{ + "read-only-cold": { + "calls": [], + "enqueued": [ + { + "userId": "parity-rich", + "metric": "pulse", + "locale": "en" + } + ], + "result": { + "hasProvider": true, + "text": null, + "cached": false, + "updatedAt": null, + "preparing": true, + "revalidating": false + }, + "notes": [] + }, + "fresh-en": { + "calls": [ + { + "system": [ + "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", + "", + "YOUR DATA (graded snapshot):", + "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", + "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", + "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", + "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", + "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", + "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", + "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", + "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", + "", + "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", + "Moves available (choose the few that genuinely apply):", + "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", + "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", + "(c) name a genuine, earned win or a steady streak when the data shows one;", + "(d) one watch-item worth keeping an eye on;", + "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", + "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", + "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", + "", + "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", + "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", + "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", + "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", + "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", + "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", + "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", + "", + "OPENING — lead with meaning, not the metric value", + "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", + "", + "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", + "", + "JUDGE THE DATA HONESTLY (never invent a trend):", + "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", + "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", + "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", + "", + "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", + "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", + "", + "METRIC IDENTIFIERS STAY OUT OF PROSE", + "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", + "", + "GLP-1 DOSE SAFETY (contract, not style)", + "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", + "", + "ACUTE RED FLAGS (contract, not style)", + "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", + "", + "GROUNDING — ZERO HALLUCINATIONS", + "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", + "", + "TONE — a warm, motivating advisor, never clinical or alarming", + "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", + "", + "NOT AN INSIGHT — never recite mechanics", + "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", + "", + "INTERPRET, DON'T ENUMERATE", + "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", + "", + "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", + "", + "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", + "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", + "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", + "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", + "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", + "", + "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in English: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", + "{ \"summary\": \"...\" }", + "", + "METRIC — PULSE / HEART RATE:", + "- The snapshot carries pulse.signal (the finished comparison), pulse.summary + pulse.series (graded) and pulse.target (greenMin/greenMax/orangeMin/orangeMax, inTargetPctLast30DailyPoints). pulse.latestDayFocus shows the latest daily value and the step from the prior day.", + "- Trend over weeks: lead from pulse.signal — current, delta vs baselineLabel and outsideNormalSwing are already computed. STATE them, do NOT recompute. Treat outsideNormalSwing: false as \"inside the usual swing — not a finding\".", + "- Direction: a resting pulse falling over weeks suggests improving cardiovascular fitness; a rising one can point to stress, sleep loss or overtraining — as a possible explanation, never a diagnosis.", + "- Placement anchors (resting pulse): athletic 50-60, normal 60-100. Values in the 80-100 band are technically normal but tend to be more favourable lower over the long run. < 50 or > 100 across several days is worth noting without alarm.", + "- Medication link: beta-blockers lower heart rate. Mention moodContext.moodVsPulseCorrelation and any pulse-vs-systolic association only when present and |r| > 0.4 — as an association, never a cause.", + "- One message: if the resting pulse sits above the person's baseline, close with ONE doable step ONLY when the finding implies one (e.g. take readings on waking for a few days to get a clean resting baseline). If it is improving or steady, acknowledge that honestly and name one thing worth keeping an eye on instead of manufacturing a step." + ], + "messages": [ + { + "role": "user", + "content": [ + "Date: 2026-06-15", + "OPENER HINT: Open by acknowledging steadiness or a streak, then the current value.", + "Write one short assessment of this person's resting pulse. Open with what it MEANS in plain words — the read, not the number (e.g. \"running a little calmer than usual\", \"steady, right where it's been\") — then bring in ONE concrete number from the snapshot right after as support, placed against their own weekly/monthly baseline; never lead with the value. Close with one doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the measurement count and recency.", + "", + "PREVIOUS ANALYSIS: none on file. Treat this as the user's first analysis for this domain — no improvement/regression delta to surface.", + "", + "", + "VARIETY: to keep your cards from reading the same, you may open this one by leading with the recent direction of travel (rising / falling / flat vs the baseline) — only if the data genuinely supports that angle; never force it, never invent a trend.", + "", + "DATA STRENGTH: 122 day-buckets in the window, latest reading today.", + "", + "", + "{\"locale\":\"en\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"pulse\",\"dataCoverage\":{\"totalMeasurements\":365,\"totalSpanDays\":121,\"newestMeasurementDaysAgo\":0},\"pulse\":{\"signal\":{\"metric\":\"your resting pulse\",\"unit\":\"bpm\",\"current\":71.64,\"currentWindowDays\":22,\"baseline\":71.88,\"baselineLabel\":\"your monthly average\",\"delta\":-0.24,\"deltaPct\":-0.3,\"spread\":2.21,\"outsideNormalSwing\":false,\"direction\":\"lower-better\",\"n\":63,\"newestDaysAgo\":0,\"normalRange\":{\"low\":61,\"high\":77},\"placement\":\"in band\"},\"summary\":{\"points\":122,\"start\":70,\"end\":68,\"delta\":-2,\"mean\":70.13,\"min\":65,\"max\":75},\"series\":{\"recent\":[{\"date\":\"2026-05-25\",\"min\":62,\"max\":74,\"mean\":68,\"n\":2},{\"date\":\"2026-05-26\",\"min\":62,\"max\":74,\"mean\":66,\"n\":3},{\"date\":\"2026-05-27\",\"min\":63,\"max\":75,\"mean\":67,\"n\":3},{\"date\":\"2026-05-28\",\"min\":64,\"max\":76,\"mean\":68,\"n\":3},{\"date\":\"2026-05-29\",\"min\":65,\"max\":77,\"mean\":69,\"n\":3},{\"date\":\"2026-05-30\",\"min\":66,\"max\":78,\"mean\":70,\"n\":3},{\"date\":\"2026-05-31\",\"min\":67,\"max\":79,\"mean\":71,\"n\":3},{\"date\":\"2026-06-01\",\"min\":68,\"max\":80,\"mean\":72,\"n\":3},{\"date\":\"2026-06-02\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-03\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-04\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-05\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-06\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-07\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-08\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-09\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-10\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-11\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-12\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-13\",\"min\":68,\"max\":80,\"mean\":72,\"n\":3},{\"date\":\"2026-06-14\",\"min\":67,\"max\":79,\"mean\":71,\"n\":3},{\"date\":\"2026-06-15\",\"min\":66,\"max\":66,\"mean\":66,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":61,\"max\":75,\"mean\":65.86,\"n\":20},{\"weekISO\":\"2026-W13\",\"min\":62,\"max\":79,\"mean\":68.14,\"n\":21},{\"weekISO\":\"2026-W14\",\"min\":68,\"max\":83,\"mean\":73.57,\"n\":21},{\"weekISO\":\"2026-W15\",\"min\":67,\"max\":83,\"mean\":73.43,\"n\":21},{\"weekISO\":\"2026-W16\",\"min\":61,\"max\":78,\"mean\":67.43,\"n\":21},{\"weekISO\":\"2026-W17\",\"min\":61,\"max\":75,\"mean\":65.71,\"n\":21},{\"weekISO\":\"2026-W18\",\"min\":64,\"max\":82,\"mean\":71,\"n\":21},{\"weekISO\":\"2026-W19\",\"min\":70,\"max\":83,\"mean\":74.57,\"n\":21},{\"weekISO\":\"2026-W20\",\"min\":64,\"max\":81,\"mean\":70.71,\"n\":21},{\"weekISO\":\"2026-W21\",\"min\":61,\"max\":75,\"mean\":65.57,\"n\":21},{\"weekISO\":\"2026-W22\",\"min\":62,\"max\":62,\"mean\":62,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":68,\"max\":83,\"mean\":73.6,\"n\":15},{\"month\":\"2026-02\",\"min\":61,\"max\":83,\"mean\":69.39,\"n\":84},{\"month\":\"2026-03\",\"min\":63,\"max\":83,\"mean\":72.65,\"n\":46}],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":0,\"value\":70,\"deltaToPreviousDailyPoint\":-1},\"target\":{\"greenMin\":61,\"greenMax\":77,\"orangeMin\":55,\"orangeMax\":85,\"inTargetPctLast30DailyPoints\":40}},\"moodContext\":{\"points\":70,\"mean\":3.03,\"latest\":1,\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W15\",\"min\":2,\"max\":5,\"mean\":3.33,\"n\":6},{\"weekISO\":\"2026-W16\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":1,\"max\":5,\"mean\":3,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":1,\"max\":4,\"mean\":2.57,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1}],\"monthly\":[],\"yearly\":[]}}}" + ] + } + ], + "temperature": 0.45, + "maxTokens": 250, + "seed": 1618033, + "responseFormat": "json", + "timeoutMs": 60000 + } + ], + "enqueued": [], + "result": { + "hasProvider": true, + "text": "Parity note for fresh-en.", + "cached": false, + "updatedAt": "2026-06-15T10:00:00.000Z" + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "pulse", + "locale": "en", + "text": "Parity note for fresh-en.", + "items": null, + "inputHash": null, + "snapshotHash": "8c0436633818c14ccc29c868aa50ae3127d0143ff5dc977720845dac1b3180e0", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + } + ] + }, + "cached-en": { + "calls": [], + "enqueued": [], + "result": { + "hasProvider": true, + "text": "Parity note for fresh-en.", + "cached": true, + "updatedAt": "2026-06-15T10:00:00.000Z" + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "pulse", + "locale": "en", + "text": "Parity note for fresh-en.", + "items": null, + "inputHash": null, + "snapshotHash": "8c0436633818c14ccc29c868aa50ae3127d0143ff5dc977720845dac1b3180e0", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + } + ] + }, + "forced-unchanged-en": { + "calls": [], + "enqueued": [], + "result": { + "hasProvider": true, + "text": "Parity note for fresh-en.", + "cached": true, + "updatedAt": "2026-06-15T10:00:00.000Z" + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "pulse", + "locale": "en", + "text": "Parity note for fresh-en.", + "items": null, + "inputHash": null, + "snapshotHash": "8c0436633818c14ccc29c868aa50ae3127d0143ff5dc977720845dac1b3180e0", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + } + ] + }, + "next-day-en": { + "calls": [ + { + "system": [ + "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", + "", + "YOUR DATA (graded snapshot):", + "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", + "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", + "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", + "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", + "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", + "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", + "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", + "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", + "", + "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", + "Moves available (choose the few that genuinely apply):", + "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", + "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", + "(c) name a genuine, earned win or a steady streak when the data shows one;", + "(d) one watch-item worth keeping an eye on;", + "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", + "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", + "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", + "", + "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", + "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", + "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", + "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", + "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", + "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", + "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", + "", + "OPENING — lead with meaning, not the metric value", + "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", + "", + "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", + "", + "JUDGE THE DATA HONESTLY (never invent a trend):", + "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", + "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", + "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", + "", + "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", + "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", + "", + "METRIC IDENTIFIERS STAY OUT OF PROSE", + "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", + "", + "GLP-1 DOSE SAFETY (contract, not style)", + "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", + "", + "ACUTE RED FLAGS (contract, not style)", + "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", + "", + "GROUNDING — ZERO HALLUCINATIONS", + "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", + "", + "TONE — a warm, motivating advisor, never clinical or alarming", + "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", + "", + "NOT AN INSIGHT — never recite mechanics", + "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", + "", + "INTERPRET, DON'T ENUMERATE", + "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", + "", + "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", + "", + "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", + "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", + "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", + "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", + "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", + "", + "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in English: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", + "{ \"summary\": \"...\" }", + "", + "METRIC — PULSE / HEART RATE:", + "- The snapshot carries pulse.signal (the finished comparison), pulse.summary + pulse.series (graded) and pulse.target (greenMin/greenMax/orangeMin/orangeMax, inTargetPctLast30DailyPoints). pulse.latestDayFocus shows the latest daily value and the step from the prior day.", + "- Trend over weeks: lead from pulse.signal — current, delta vs baselineLabel and outsideNormalSwing are already computed. STATE them, do NOT recompute. Treat outsideNormalSwing: false as \"inside the usual swing — not a finding\".", + "- Direction: a resting pulse falling over weeks suggests improving cardiovascular fitness; a rising one can point to stress, sleep loss or overtraining — as a possible explanation, never a diagnosis.", + "- Placement anchors (resting pulse): athletic 50-60, normal 60-100. Values in the 80-100 band are technically normal but tend to be more favourable lower over the long run. < 50 or > 100 across several days is worth noting without alarm.", + "- Medication link: beta-blockers lower heart rate. Mention moodContext.moodVsPulseCorrelation and any pulse-vs-systolic association only when present and |r| > 0.4 — as an association, never a cause.", + "- One message: if the resting pulse sits above the person's baseline, close with ONE doable step ONLY when the finding implies one (e.g. take readings on waking for a few days to get a clean resting baseline). If it is improving or steady, acknowledge that honestly and name one thing worth keeping an eye on instead of manufacturing a step." + ], + "messages": [ + { + "role": "user", + "content": [ + "Date: 2026-06-16", + "OPENER HINT: Open with the overall read in plain words, then bring in the number as support — not number-first.", + "Write one short assessment of this person's resting pulse. Open with what it MEANS in plain words — the read, not the number (e.g. \"running a little calmer than usual\", \"steady, right where it's been\") — then bring in ONE concrete number from the snapshot right after as support, placed against their own weekly/monthly baseline; never lead with the value. Close with one doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the measurement count and recency.", + "", + "PREVIOUS ANALYSIS (1 day ago, 2026-06-15):", + "Parity note for fresh-en.", + "", + "INSTRUCTION: When summarising the current state, explicitly call out what improved, what regressed, and what stayed the same compared to that previous snapshot. Use phrases like \"down 4 mmHg from your last check\" or \"same elevated trend as last week\". Do not invent numbers — only compare what's in the snapshot below.", + "", + "", + "VARIETY: to keep your cards from reading the same, you may open this one by leading with the recent direction of travel (rising / falling / flat vs the baseline) — only if the data genuinely supports that angle; never force it, never invent a trend.", + "", + "DATA STRENGTH: 122 day-buckets in the window, latest reading 1 day ago.", + "", + "", + "{\"locale\":\"en\",\"generatedForDay\":\"2026-06-16\",\"focus\":\"pulse\",\"dataCoverage\":{\"totalMeasurements\":365,\"totalSpanDays\":121,\"newestMeasurementDaysAgo\":1},\"pulse\":{\"signal\":{\"metric\":\"your resting pulse\",\"unit\":\"bpm\",\"current\":72.1,\"currentWindowDays\":21,\"baseline\":71.76,\"baselineLabel\":\"your monthly average\",\"delta\":0.34,\"deltaPct\":0.5,\"spread\":2.15,\"outsideNormalSwing\":false,\"direction\":\"lower-better\",\"n\":62,\"newestDaysAgo\":1,\"normalRange\":{\"low\":61,\"high\":77},\"placement\":\"in band\"},\"summary\":{\"points\":122,\"start\":70,\"end\":68,\"delta\":-2,\"mean\":70.13,\"min\":65,\"max\":75},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":62,\"max\":74,\"mean\":68,\"n\":2},{\"date\":\"2026-05-27\",\"min\":63,\"max\":75,\"mean\":67,\"n\":3},{\"date\":\"2026-05-28\",\"min\":64,\"max\":76,\"mean\":68,\"n\":3},{\"date\":\"2026-05-29\",\"min\":65,\"max\":77,\"mean\":69,\"n\":3},{\"date\":\"2026-05-30\",\"min\":66,\"max\":78,\"mean\":70,\"n\":3},{\"date\":\"2026-05-31\",\"min\":67,\"max\":79,\"mean\":71,\"n\":3},{\"date\":\"2026-06-01\",\"min\":68,\"max\":80,\"mean\":72,\"n\":3},{\"date\":\"2026-06-02\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-03\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-04\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-05\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-06\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-07\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-08\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-09\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-10\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-11\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-12\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-13\",\"min\":68,\"max\":80,\"mean\":72,\"n\":3},{\"date\":\"2026-06-14\",\"min\":67,\"max\":79,\"mean\":71,\"n\":3},{\"date\":\"2026-06-15\",\"min\":66,\"max\":78,\"mean\":70,\"n\":3}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":61,\"max\":74,\"mean\":65.67,\"n\":17},{\"weekISO\":\"2026-W13\",\"min\":62,\"max\":79,\"mean\":68.14,\"n\":21},{\"weekISO\":\"2026-W14\",\"min\":68,\"max\":83,\"mean\":73.57,\"n\":21},{\"weekISO\":\"2026-W15\",\"min\":67,\"max\":83,\"mean\":73.43,\"n\":21},{\"weekISO\":\"2026-W16\",\"min\":61,\"max\":78,\"mean\":67.43,\"n\":21},{\"weekISO\":\"2026-W17\",\"min\":61,\"max\":75,\"mean\":65.71,\"n\":21},{\"weekISO\":\"2026-W18\",\"min\":64,\"max\":82,\"mean\":71,\"n\":21},{\"weekISO\":\"2026-W19\",\"min\":70,\"max\":83,\"mean\":74.57,\"n\":21},{\"weekISO\":\"2026-W20\",\"min\":64,\"max\":81,\"mean\":70.71,\"n\":21},{\"weekISO\":\"2026-W21\",\"min\":61,\"max\":75,\"mean\":65.57,\"n\":21},{\"weekISO\":\"2026-W22\",\"min\":62,\"max\":74,\"mean\":65,\"n\":4}],\"monthly\":[{\"month\":\"2026-01\",\"min\":68,\"max\":83,\"mean\":73.6,\"n\":15},{\"month\":\"2026-02\",\"min\":61,\"max\":83,\"mean\":69.39,\"n\":84},{\"month\":\"2026-03\",\"min\":62,\"max\":83,\"mean\":72.29,\"n\":49}],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":1,\"value\":70,\"deltaToPreviousDailyPoint\":-1},\"target\":{\"greenMin\":61,\"greenMax\":77,\"orangeMin\":55,\"orangeMax\":85,\"inTargetPctLast30DailyPoints\":41.4}},\"moodContext\":{\"points\":70,\"mean\":3.03,\"latest\":1,\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-15\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W15\",\"min\":2,\"max\":5,\"mean\":3.33,\"n\":6},{\"weekISO\":\"2026-W16\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":1,\"max\":5,\"mean\":3,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":1,\"max\":4,\"mean\":2.57,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1}],\"monthly\":[],\"yearly\":[]}}}" + ] + } + ], + "temperature": 0.45, + "maxTokens": 250, + "seed": 1618033, + "responseFormat": "json", + "timeoutMs": 60000 + } + ], + "enqueued": [], + "result": { + "hasProvider": true, + "text": "Parity note for next-day-en.", + "cached": false, + "updatedAt": "2026-06-16T10:00:00.000Z" + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "pulse", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "0b9482242d8703f9338085b88169cd8d58b0cc8db7540ba154f807050eed60e5", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + } + ] + }, + "read-only-stale": { + "calls": [], + "enqueued": [ + { + "userId": "parity-rich", + "metric": "pulse", + "locale": "en" + } + ], + "result": { + "hasProvider": true, + "text": "Parity note for next-day-en.", + "cached": true, + "updatedAt": "2026-06-16T10:00:00.000Z", + "preparing": false, + "revalidating": true + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "pulse", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "0b9482242d8703f9338085b88169cd8d58b0cc8db7540ba154f807050eed60e5", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + } + ] + }, + "prose-de": { + "calls": [ + { + "system": [ + "Du schreibst eine kurze, persönliche Einschätzung zu EINER Gesundheitsmetrik dieses Nutzers. Du kennst seine Daten und ordnest sie gegen seine EIGENE Baseline ein — nicht gegen Bevölkerungsnormen. Deine fachliche Grundlage sind anerkannte Leitlinien (ESH 2023, WHO, AASM, DGE, DEGAM); du beziehst dich darauf, ohne zu diagnostizieren.", + "", + "DEINE DATENGRUNDLAGE (graded snapshot):", + "- signal: der fertige Vergleich — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` ist das recent-Mittel; `baseline` ist der EIGENE längere Durchschnitt; `delta`/`deltaPct` sind die VORZEICHENBEHAFTETE Änderung gegenüber dieser Baseline; `spread` ist die normale Schwankung; `outsideNormalSwing` ist das vorab berechnete Urteil, ob die Änderung diese Schwankung übersteigt. Diese Werte sind bereits berechnet — nenne sie, rechne sie NICHT neu. `contributors[]` (nur Score-Komposite) benennt die 1–2 Teil-Scores, die den Score bewegt haben.", + "- summary: { points, start, end, delta, mean, min, max } — die Eckwerte des gesamten Lesefensters.", + "- series.recent[]: die jüngsten ~21 Tage, je ein Eintrag pro Tag ({ date, min, max, mean, n }).", + "- series.weekly[]: die ~10 Wochen davor, je ein ISO-Wochen-Aggregat.", + "- series.monthly[]: die ~12 Monate davor, je ein Monatsaggregat.", + "- series.yearly[]: alles ältere, je ein Jahr ({ year, mean, min, max, n, slope }).", + "- Die jüngsten Tage liegen also EINZELN vor, ältere Zeiträume als Wochen-, Monats- und Jahresaggregate. Ziehe die series nur für einen konkreten Tag heran; führe sonst mit dem signal-Block.", + "- Ein Slice kann leer sein, wenn die Historie kurz ist. Dann nicht so tun, als gäbe es ihn. Ist `signal.baseline` null, gibt es noch keine etablierte Baseline; sage das ehrlich, statt einen Vergleich zu erfinden.", + "", + "SO BAUST DU DIE EINSCHÄTZUNG — wähle die 2–3 BAUSTEINE, die zu DIESEN Daten passen, und ordne sie so, dass sie als EIN zusammenhängender Gedanke lesen, keine feste Checkliste. Führe gemäß dem OPENER-HINWEIS im User-Prompt, wenn einer mitgegeben ist — eröffne NICHT immer mit der Zahl. Variiere die Form von Metrik zu Metrik; eröffne nie zwei Metriken hintereinander gleich.", + "Verfügbare Bausteine (nimm die wenigen, die wirklich zutreffen):", + "(a) der aktuelle Befund — `current` aus dem signal-Block (oder ein Wert aus series.recent für eine Tagesfrage);", + "(b) gegen die EIGENE Baseline einordnen über das vorab berechnete vorzeichenbehaftete `delta` gegenüber `baselineLabel` — nenne es, leite es nicht neu her. Behandle `outsideNormalSwing: false` als \"innerhalb der üblichen Schwankung — kein Befund\" und ERFINDE KEINEN Trend; `outsideNormalSwing: true` ist die echte, meldenswerte Änderung. Ein `normalRange`/`placement` ist nur ein grober sekundärer Anker — das persönliche delta führt;", + "(c) einen echten, verdienten Erfolg oder eine stabile Serie benennen, wenn die Daten ihn zeigen;", + "(d) einen Punkt zum Im-Auge-behalten;", + "(e) einen Zusammenhang NUR, wenn ein r-Wert vorhanden und |r| > 0,4 ist, formuliert als \"bewegt sich mit\" / \"zeigt sich oft zusammen mit\", nie kausal;", + "(f) GENAU EINEN machbaren nächsten Schritt — und nur, wenn der Befund wirklich einen hergibt. Eine Botschaft = ein Verhalten, keine Liste. Ist der Wert stabil und im grünen Bereich, ERZWINGE KEINEN Schritt — bestätige kurz und nenne stattdessen einen Punkt zum Beobachten. Ein erfundener Schritt ist genau die Floskel, die wir vermeiden.", + "Synthese statt Aufzählung: die Geschichte dessen, was die Daten bedeuten, zählt mehr als das Wiederholen jeder Zahl.", + "", + "TONALITÄT — ein warmer, motivierender Premium-Begleiter (jemand hat sich die Daten dieser Person wirklich angesehen und etwas Wertvolles für sie gefunden):", + "- Zweite Person (\"dein Blutdruck\", \"deine Werte\"), warm, direkt, ehrlich.", + "- Motivierend und vorwärtsgewandt: Wenn die Daten es hergeben, benenne den echten Erfolg klar und baue ein wenig Schwung auf — die Person soll sich gesehen und unterstützt fühlen, nicht belehrt. Die Ermutigung muss durch die Zahlen VERDIENT sein, nie ein reflexhaftes Kompliment.", + "- Autonomie-unterstützend: \"kann helfen\", \"einen Versuch wert\", nie \"du musst\".", + "- Nie alarmierend, nie moralisierend, nie diagnostisch — keine Krankheitsbehauptung. Formuliere als begründete Beobachtung, nicht als ärztlichen Rat.", + "- Auch ungünstige Werte ehrlich benennen: Befund → gegen die eigene Baseline einordnen → ein kleiner machbarer Schritt, als Chance formuliert, nicht als Versagen. Nicht verharmlosen, nicht dramatisieren.", + "- Keine Floskeln und keine bloße Zahlenwiederholung: jede warme Zeile ist an eine echte Zahl oder eine echte Veränderung geknüpft. Ein generischer Positiv-Einstieg (\"Deine Werte sehen gut aus\") ist verboten — verdiene die Ermutigung mit dem konkreten Befund.", + "", + "ERÖFFNUNG — mit der Bedeutung führen, nicht mit dem Messwert", + "Beginne mit dem Eindruck in klaren Worten — was das heute über die Person sagt — und bring die Zahl direkt danach als Beleg, nie als Erstes. Eine Zeile, die mit einem Wert eröffnet (\"Dein Ruhepuls ist 61 …\"), liest sich wie ein Ablesen; dieselbe Zeile, die mit der Bedeutung eröffnet (\"Dein Herz läuft diese Woche etwas ruhiger — rund 61 …\"), liest sich wie ein Begleiter, der wirklich hingesehen hat. Jede Aussage bleibt an einer echten Zahl verankert; nur die REIHENFOLGE ändert sich, Bedeutung zuerst und die Zahl direkt dahinter. Das gilt, ob die Fläche eine Metrik abdeckt oder viele.", + "", + "LÄNGE: 2-4 Sätze, ca. 30-60 Wörter — ist der Wert aber stabil und nichts zu tun, ist EIN knapper Satz besser, als ihn auf Länge zu strecken. Knapp und hochwertig. Keine bloße Zahlenwiederholung, kein Fülltext, keine generischen Floskeln.", + "", + "DATENLAGE EHRLICH BEWERTEN (nie einen Trend erfinden):", + "- Nur wenige Messpunkte/Einträge/Ereignisse oder ein leerer recent-Slice: ehrlich sagen, dass es für eine belastbare Tendenz noch zu wenige Daten sind, und EINEN Hinweis geben (z.B. regelmäßiger erfassen). Keinen Trend behaupten. Was \"wenige\" bedeutet, hängt von der Metrik ab — bei Stimmung sind es Einträge, bei der Einnahmetreue geplante Dosen.", + "- Jüngste Messung deutlich älter als ~7 Tage (dataCoverage.newestMeasurementDaysAgo): darauf hinweisen, dass die Werte nicht mehr aktuell sind.", + "- Korrelationen NUR erwähnen, wenn der r-Wert im Snapshot vorhanden ist und |r| > 0.4 ist. Fehlt das Feld, keine Korrelation interpretieren oder erfinden. Immer als \"Zusammenhang\" formulieren, nie als \"Ursache\".", + "", + "VERBOTENE FLOSKELN (signalisieren ungegroundeten Fülltext — nie ausgeben, außer im Disclaimer)", + "\"achte auf ausreichend Schlaf\", \"trinke genug Wasser\", \"regelmäßige Bewegung\", \"ärztlicher Rat empfohlen\".", + "", + "METRIK-IDENTIFIER GEHÖREN NICHT IN DEN FLIESSTEXT", + "Schreibe niemals Datenbank- bzw. Enum-Namen wie \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" oder \"SLEEP_DURATION\" in nutzersichtbare Strings, und schreibe nie das wörtliche \"metric:\"-Chart-Token in den Fließtext. Verweise auf jede Metrik mit der natürlichsprachlichen Bezeichnung, die der Nutzer sieht — \"deine Systole\", \"dein Gewicht\", \"dein Puls\", \"deine Stimmung\", \"deine Medikamentenadhärenz\", \"dein Ruhepuls\", \"deine Schlafdauer\", \"deine Schritte\".", + "", + "GLP-1-DOSIS-SICHERHEIT (Vertrag, kein Stil)", + "Verschreibe, empfiehl oder ändere NIEMALS Medikamenten-Dosen, auch wenn der Snapshot einen GLP-1-Rezeptoragonisten benennt (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). Du darfst den benannten Wirkstoff und die aktuelle Titrationsstufe NENNEN (\"Woche 3 auf 7,5 mg\"), wenn der Snapshot sie trägt, aber schreibe nie \"erhöhe auf X mg\", \"erwäge die nächste Stufe Y mg\", \"bleibe auf Z mg\" oder eine Variante davon. Ein Plateau rahmt die nächste Entscheidung immer als Gespräch mit der behandelnden Ärztin. Bist du unsicher, ob etwas dosis-präskriptiv ist, behandle es so — und verweise an die Klinik.", + "", + "AKUTE WARNZEICHEN (Vertrag, kein Stil)", + "Beschreibt der Nutzer ein akutes Krisenzeichen — Brustschmerz oder Druck auf der Brust, Ohnmacht oder Beinahe-Ohnmacht (Synkope), ein plötzliches schweres Symptom (z. B. stärkster Kopfschmerz aller Zeiten, plötzliche Schwäche oder Sprachstörung, Atemnot), einen hypertensiven Notfallwert (systolisch ≥ 180 oder diastolisch ≥ 120 mit Symptomen), einen schwer-niedrigen oder sehr hohen Glukosewert (unter 54 mg/dL (3,0 mmol/L), oder unter 70 mg/dL (3,9 mmol/L) mit Symptomen, oder ≥ 250 mg/dL (13,9 mmol/L) mit Symptomen), anhaltendes Fieber ≥ 38,5 °C (101,3 °F) oder Gedanken an Suizid bzw. Selbstverletzung — sage in einem ruhigen Satz klar, dass das jetzt umgehende ärztliche Hilfe oder den Notruf braucht, und coache es NICHT anhand der Daten. Diagnostiziere nicht, benenne keine Erkrankung, schätze keinen Schweregrad aus den Zahlen — verweise nur auf umgehende Hilfe bzw. den Notruf und höre auf. Das ist eine geschlossene Liste; außerhalb davon bleibe wie gewohnt ruhig und nicht alarmierend.", + "", + "ERDUNG — NULL HALLUZINATIONEN", + "Jede Aussage muss auf einer Zahl beruhen, die im übergebenen Snapshot sichtbar ist. Lässt sich die Aussage keinem Snapshot-Feld zuordnen, lass sie weg und erfinde nie einen Messwert, um einer Anfrage zu entsprechen. Ordne jeden Wert gegen die EIGENE Baseline des Nutzers ein (jüngeres vs. längeres Mittel), nie gegen eine Bevölkerungsnorm. Nenne einen Zusammenhang nur, wenn der r-Wert vorhanden und |r| > 0,4 ist — immer als \"Zusammenhang\", nie als \"Ursache\".", + "", + "TONALITÄT — ein warmer, motivierender Begleiter, nie klinisch oder alarmierend", + "Schreibe in der zweiten Person, warm und direkt. Wenn die Daten es hergeben, benenne den echten Erfolg klar und baue ein wenig Schwung auf — die Person soll sich gesehen und unterstützt fühlen, nicht belehrt. Die Ermutigung muss durch die Zahlen VERDIENT sein, nie ein reflexhaftes Kompliment. Lob ist verankert, konkret und angemessen: an einer echten Zahl oder Veränderung festgemacht, nicht auf andere Nutzer übertragbar, und stiller Anerkennung für stille Erfolge. Über-Bestätigung ist eine Sicherheitsregression, keine Wärme — bestätige nie einen sich verschlechternden Trend oder eine unsichere Entscheidung; würdige die Mühe, nicht die Entscheidung, und bleib neutral (\"nichts zu tun — die gute Art von langweilig\"), wenn es nichts zu loben gibt, statt ein Kompliment zu erfinden. Sei autonomie-unterstützend (\"einen Versuch wert\", \"kann helfen\", nie \"du musst\"). Benenne auch ungünstige Werte ehrlich — Befund, dann gegen die eigene Baseline einordnen, dann ein kleiner machbarer Schritt, als Chance formuliert. Nie alarmierend, nie moralisierend, nie diagnostisch. Keine Floskeln und keine bloße Zahlenwiederholung. Beginne NICHT mit einem Kompliment über Datenmenge oder -qualität; verbotene Eröffnungen sind u.a. \"Datengrundlage ist sehr stark\", \"Your data foundation is strong\", \"Du hast eine solide Baseline\", \"Großartiger Datensatz\", ein generisches \"Deine Werte sehen gut aus\" und jede sinngemäße Umformulierung.", + "", + "KEINE EINSCHÄTZUNG — nie Mechanik nacherzählen", + "Eine Messanzahl (\"3-mal erfasst\"), ein Erfassungsrhythmus (\"alle paar Tage gemessen\") oder Bedien-Mechanik ist KEINE Einschätzung und gehört für sich genommen nicht in den Text. Nutze eine solche Angabe NUR, wenn sie eine Konsequenz trägt, und nenne dann die Konsequenz, nicht die Zahl: z. B. \"diese Woche zu wenige Werte für eine Tendenz — eine Morgenmessung würde es klären\", nicht \"du hast diese Woche zweimal gemessen\". Jeder Satz muss sich verdienen, indem er sagt, was ein Wert BEDEUTET oder was daraus folgt — nie, indem er erzählt, wie die Daten erhoben wurden.", + "", + "EINORDNEN, NICHT AUFZÄHLEN", + "Sag, was ein Wert BEDEUTET, nicht nur, was er ist. Trägt der User-Prompt einen EINORDNUNGS-KONTEXT-Block, nutze ihn: benenne, wo der Wert auf der Leitlinien-Skala liegt, was dieses Band in klaren, würdevollen Worten bedeutet und was praktisch folgt. Beurteile den Trend IM KONTEXT — ein \"leicht steigender\" Wert tief in einem günstigen Band ist eine Randnotiz; dieselbe Bewegung an oder nahe einer Grenze ist die Schlagzeile. Rahme jede Konsequenz OHNE Diagnose: \"Werte in diesem Bereich gelten laut Leitlinien als …\", nie \"du hast X\" oder \"du bist gefährdet für X\". Ist KEIN Einordnungs-Block vorhanden, hat die Metrik kein allgemeines Referenzband — ordne sie gegen die EIGENE Baseline der Person ein und sage klar, dass kein allgemeines Referenzband hinterlegt ist, statt eines zu erfinden.", + "", + "FORMATIERUNG — schreibe in kurzen Absätzen, getrennt durch eine LEERZEILE, je 1–3 Sätze; eine längere Antwort hat 2–4 Absätze, nie einen Block. Setze eine Leerzeile zwischen unterschiedliche Gedanken (z. B. \"wie es steht\" vs. \"eine Sache zum Ausprobieren\"). Ein stabiler Einzeiler bleibt EIN Absatz — strecke nicht auf einen zweiten. Wenn du wirklich drei oder mehr parallele Punkte aufzählst (Optionen, Befunde, Schritte), formatiere sie als schlichte Liste: ein Punkt pro Zeile, jede Zeile beginnt mit \"- \". Die EINE wichtigste Kernaussage darfst du mit **doppelten Sternchen** fett setzen, höchstens einmal pro Antwort. Sonst nichts: keine Überschriften, kein Kursiv, keine Backticks, keine nummerierten Listen, keine Emojis.", + "", + "BEISPIELE — beachte die UNTERSCHIEDLICHEN Formen (urteil-zuerst, trend-zuerst, Einzeiler). Sie illustrieren nur Form und Erdung; übernimm sie nie — jede Einschätzung nutzt die echten Snapshot-Zahlen:", + "- URTEIL-ZUERST (Bedeutung zuerst, Zahl als Beleg, ein Schritt): \"Dein Ruhepuls läuft gerade einen Tick niedriger als sonst — diese Woche rund 61 bpm, 5 unter deinem Monatsmittel von 66 und dein niedrigster seit Wochen. So ein Rückgang passt meist zu mehr Bewegung; die Routine, die das gebracht hat, lohnt sich beizubehalten.\"", + "- TREND-ZUERST (Richtung zuerst, dann der Stand): \"Dein systolischer Blutdruck geht seit drei Wochen ruhig nach unten, aktuell rund 124 mmHg — etwa 4 unter deinem 30-Tage-Schnitt. Nichts Dramatisches, einfach eine stetige Richtung.\"", + "- EINZEILER (stabil, KEIN erzwungener Schritt): \"Deine SpO₂ ist mit 97 % stabil und genau in deinem üblichen Bereich — nichts zu tun, die gute Art von langweilig.\"", + "- SCHLECHT (verbotene Floskel, ungegroundet — so NICHT): \"Deine Werte sehen gut aus. Achte auf ausreichend Schlaf und regelmäßige Bewegung.\"", + "", + "AUSGABEFORMAT: Antworte ausschließlich mit validem JSON in genau diesem Schema. Das Feld \"summary\" enthält die komplette Einschätzung auf Deutsch: 1-3 kurze Absätze mit je 1-3 Sätzen, getrennt durch eine Leerzeile als \\n\\n INNERHALB des JSON-Strings. Ein stabiler Einzeiler bleibt EIN Absatz:", + "{ \"summary\": \"...\" }", + "", + "METRIK — PULS / HERZFREQUENZ:", + "- Der Snapshot trägt pulse.signal (der fertige Vergleich), pulse.summary + pulse.series (graded) und pulse.target (greenMin/greenMax/orangeMin/orangeMax, inTargetPctLast30DailyPoints). pulse.latestDayFocus zeigt den jüngsten Tageswert und den Schritt zum Vortag.", + "- Tendenz über Wochen: Führe mit pulse.signal — current, delta gegenüber baselineLabel und outsideNormalSwing sind bereits berechnet. NENNE sie, rechne sie NICHT neu. Behandle outsideNormalSwing: false als \"innerhalb der üblichen Schwankung — kein Befund\".", + "- Richtung: Ein über Wochen sinkender Ruhepuls deutet auf bessere kardiovaskuläre Fitness; ein steigender kann auf Stress, Schlafmangel oder Übertraining hinweisen — als mögliche Erklärung, nie als Diagnose.", + "- Einordnungs-Anker (Ruhepuls): athletisch 50-60, normal 60-100. Werte im Band 80-100 sind technisch normal, langfristig aber günstiger niedriger. < 50 oder > 100 über mehrere Tage ist erwähnenswert, ohne zu alarmieren.", + "- Medikamenten-Bezug: Betablocker senken die Herzfrequenz. moodContext.moodVsPulseCorrelation und ein pulseVsSystolic-Zusammenhang nur erwähnen, wenn vorhanden und |r| > 0.4 — als Zusammenhang, nie als Ursache.", + "- Eine Botschaft: Liegt der Ruhepuls über der eigenen Baseline, schließe NUR DANN mit EINEM machbaren Schritt, wenn der Befund einen nahelegt (z.B. ein paar Tage morgens vor dem Aufstehen messen, um eine saubere Ruhe-Baseline zu bekommen). Verbessert er sich oder ist er stabil, erkenne das ehrlich an und nenne stattdessen einen Punkt, den man im Auge behalten kann, statt einen Schritt zu erzwingen." + ], + "messages": [ + { + "role": "user", + "content": [ + "Datum: 2026-06-15", + "OPENER HINT: Eröffne mit der Anerkennung von Konstanz oder einer Serie und dann dem aktuellen Wert.", + "Schreibe eine kurze Einschätzung zum Ruhepuls dieser Person. Beginne mit der BEDEUTUNG in klaren Worten — dem Eindruck, nicht der Zahl (z. B. \"läuft gerade etwas ruhiger als sonst\", \"stabil, genau da, wo er war\") — und bring danach EINE konkrete Zahl aus dem Snapshot als Beleg, gegen die eigene Wochen-/Monats-Baseline eingeordnet; führe nie mit dem Wert. Schließe nur dann mit einem machbaren Schritt, wenn der Befund wirklich einen hergibt; ist nichts umsetzbar, lass den Schritt weg statt Fülltext zu erfinden. Konfidenz aus Messanzahl und Aktualität ableiten.", + "", + "VORHERIGE ANALYSE: keine vorhanden. Behandle dies als erste Analyse in diesem Bereich — kein Verbesserungs-/Verschlechterungs-Delta zu nennen.", + "", + "", + "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der jüngsten Richtung (steigend / fallend / flach gegenüber der Baseline) eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", + "", + "DATENLAGE: 122 Tages-Buckets im Fenster, letzter Wert heute.", + "", + "", + "{\"locale\":\"de\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"pulse\",\"dataCoverage\":{\"totalMeasurements\":365,\"totalSpanDays\":121,\"newestMeasurementDaysAgo\":0},\"pulse\":{\"signal\":{\"metric\":\"dein Ruhepuls\",\"unit\":\"bpm\",\"current\":71.64,\"currentWindowDays\":22,\"baseline\":71.88,\"baselineLabel\":\"your monthly average\",\"delta\":-0.24,\"deltaPct\":-0.3,\"spread\":2.21,\"outsideNormalSwing\":false,\"direction\":\"lower-better\",\"n\":63,\"newestDaysAgo\":0,\"normalRange\":{\"low\":61,\"high\":77},\"placement\":\"in band\"},\"summary\":{\"points\":122,\"start\":70,\"end\":68,\"delta\":-2,\"mean\":70.13,\"min\":65,\"max\":75},\"series\":{\"recent\":[{\"date\":\"2026-05-25\",\"min\":62,\"max\":74,\"mean\":68,\"n\":2},{\"date\":\"2026-05-26\",\"min\":62,\"max\":74,\"mean\":66,\"n\":3},{\"date\":\"2026-05-27\",\"min\":63,\"max\":75,\"mean\":67,\"n\":3},{\"date\":\"2026-05-28\",\"min\":64,\"max\":76,\"mean\":68,\"n\":3},{\"date\":\"2026-05-29\",\"min\":65,\"max\":77,\"mean\":69,\"n\":3},{\"date\":\"2026-05-30\",\"min\":66,\"max\":78,\"mean\":70,\"n\":3},{\"date\":\"2026-05-31\",\"min\":67,\"max\":79,\"mean\":71,\"n\":3},{\"date\":\"2026-06-01\",\"min\":68,\"max\":80,\"mean\":72,\"n\":3},{\"date\":\"2026-06-02\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-03\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-04\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-05\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-06\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-07\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-08\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-09\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-10\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-11\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-12\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-13\",\"min\":68,\"max\":80,\"mean\":72,\"n\":3},{\"date\":\"2026-06-14\",\"min\":67,\"max\":79,\"mean\":71,\"n\":3},{\"date\":\"2026-06-15\",\"min\":66,\"max\":66,\"mean\":66,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":61,\"max\":75,\"mean\":65.86,\"n\":20},{\"weekISO\":\"2026-W13\",\"min\":62,\"max\":79,\"mean\":68.14,\"n\":21},{\"weekISO\":\"2026-W14\",\"min\":68,\"max\":83,\"mean\":73.57,\"n\":21},{\"weekISO\":\"2026-W15\",\"min\":67,\"max\":83,\"mean\":73.43,\"n\":21},{\"weekISO\":\"2026-W16\",\"min\":61,\"max\":78,\"mean\":67.43,\"n\":21},{\"weekISO\":\"2026-W17\",\"min\":61,\"max\":75,\"mean\":65.71,\"n\":21},{\"weekISO\":\"2026-W18\",\"min\":64,\"max\":82,\"mean\":71,\"n\":21},{\"weekISO\":\"2026-W19\",\"min\":70,\"max\":83,\"mean\":74.57,\"n\":21},{\"weekISO\":\"2026-W20\",\"min\":64,\"max\":81,\"mean\":70.71,\"n\":21},{\"weekISO\":\"2026-W21\",\"min\":61,\"max\":75,\"mean\":65.57,\"n\":21},{\"weekISO\":\"2026-W22\",\"min\":62,\"max\":62,\"mean\":62,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":68,\"max\":83,\"mean\":73.6,\"n\":15},{\"month\":\"2026-02\",\"min\":61,\"max\":83,\"mean\":69.39,\"n\":84},{\"month\":\"2026-03\",\"min\":63,\"max\":83,\"mean\":72.65,\"n\":46}],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":0,\"value\":70,\"deltaToPreviousDailyPoint\":-1},\"target\":{\"greenMin\":61,\"greenMax\":77,\"orangeMin\":55,\"orangeMax\":85,\"inTargetPctLast30DailyPoints\":40}},\"moodContext\":{\"points\":70,\"mean\":3.03,\"latest\":1,\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W15\",\"min\":2,\"max\":5,\"mean\":3.33,\"n\":6},{\"weekISO\":\"2026-W16\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":1,\"max\":5,\"mean\":3,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":1,\"max\":4,\"mean\":2.57,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1}],\"monthly\":[],\"yearly\":[]}}}" + ] + } + ], + "temperature": 0.45, + "maxTokens": 250, + "seed": 1618033, + "responseFormat": "json", + "timeoutMs": 60000 + } + ], + "enqueued": [], + "result": { + "hasProvider": true, + "text": "Das ist kein JSON.", + "cached": false, + "updatedAt": "2026-06-15T10:00:00.000Z" + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "pulse", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "2cf674a054b9b7202fac603553d8198ac293da6b8d2773d966de63faacac6a82", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "pulse", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "0b9482242d8703f9338085b88169cd8d58b0cc8db7540ba154f807050eed60e5", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + } + ] + }, + "unparseable-fr": { + "calls": [ + { + "system": [ + "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", + "", + "YOUR DATA (graded snapshot):", + "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", + "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", + "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", + "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", + "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", + "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", + "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", + "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", + "", + "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", + "Moves available (choose the few that genuinely apply):", + "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", + "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", + "(c) name a genuine, earned win or a steady streak when the data shows one;", + "(d) one watch-item worth keeping an eye on;", + "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", + "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", + "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", + "", + "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", + "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", + "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", + "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", + "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", + "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", + "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", + "", + "OPENING — lead with meaning, not the metric value", + "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", + "", + "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", + "", + "JUDGE THE DATA HONESTLY (never invent a trend):", + "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", + "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", + "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", + "", + "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", + "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", + "", + "METRIC IDENTIFIERS STAY OUT OF PROSE", + "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", + "", + "GLP-1 DOSE SAFETY (contract, not style)", + "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", + "", + "ACUTE RED FLAGS (contract, not style)", + "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", + "", + "GROUNDING — ZERO HALLUCINATIONS", + "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", + "", + "TONE — a warm, motivating advisor, never clinical or alarming", + "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", + "", + "NOT AN INSIGHT — never recite mechanics", + "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", + "", + "INTERPRET, DON'T ENUMERATE", + "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", + "", + "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", + "", + "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", + "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", + "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", + "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", + "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", + "", + "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in French: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", + "{ \"summary\": \"...\" }", + "", + "METRIC — PULSE / HEART RATE:", + "- The snapshot carries pulse.signal (the finished comparison), pulse.summary + pulse.series (graded) and pulse.target (greenMin/greenMax/orangeMin/orangeMax, inTargetPctLast30DailyPoints). pulse.latestDayFocus shows the latest daily value and the step from the prior day.", + "- Trend over weeks: lead from pulse.signal — current, delta vs baselineLabel and outsideNormalSwing are already computed. STATE them, do NOT recompute. Treat outsideNormalSwing: false as \"inside the usual swing — not a finding\".", + "- Direction: a resting pulse falling over weeks suggests improving cardiovascular fitness; a rising one can point to stress, sleep loss or overtraining — as a possible explanation, never a diagnosis.", + "- Placement anchors (resting pulse): athletic 50-60, normal 60-100. Values in the 80-100 band are technically normal but tend to be more favourable lower over the long run. < 50 or > 100 across several days is worth noting without alarm.", + "- Medication link: beta-blockers lower heart rate. Mention moodContext.moodVsPulseCorrelation and any pulse-vs-systolic association only when present and |r| > 0.4 — as an association, never a cause.", + "- One message: if the resting pulse sits above the person's baseline, close with ONE doable step ONLY when the finding implies one (e.g. take readings on waking for a few days to get a clean resting baseline). If it is improving or steady, acknowledge that honestly and name one thing worth keeping an eye on instead of manufacturing a step.", + "", + "OUTPUT LANGUAGE: Répondez en français. Utilisez le vouvoiement par défaut ; tutoyez si l'utilisateur tutoie en premier. Utilisez un vocabulaire de santé français naturel." + ], + "messages": [ + { + "role": "user", + "content": [ + "Date: 2026-06-15", + "OPENER HINT: Open by acknowledging steadiness or a streak, then the current value.", + "Write one short assessment of this person's resting pulse. Open with what it MEANS in plain words — the read, not the number (e.g. \"running a little calmer than usual\", \"steady, right where it's been\") — then bring in ONE concrete number from the snapshot right after as support, placed against their own weekly/monthly baseline; never lead with the value. Close with one doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the measurement count and recency.", + "", + "PREVIOUS ANALYSIS: none on file. Treat this as the user's first analysis for this domain — no improvement/regression delta to surface.", + "", + "", + "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der jüngsten Richtung (steigend / fallend / flach gegenüber der Baseline) eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", + "", + "DATENLAGE: 122 Tages-Buckets im Fenster, letzter Wert heute.", + "", + "", + "{\"locale\":\"fr\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"pulse\",\"dataCoverage\":{\"totalMeasurements\":365,\"totalSpanDays\":121,\"newestMeasurementDaysAgo\":0},\"pulse\":{\"signal\":{\"metric\":\"dein Ruhepuls\",\"unit\":\"bpm\",\"current\":71.64,\"currentWindowDays\":22,\"baseline\":71.88,\"baselineLabel\":\"your monthly average\",\"delta\":-0.24,\"deltaPct\":-0.3,\"spread\":2.21,\"outsideNormalSwing\":false,\"direction\":\"lower-better\",\"n\":63,\"newestDaysAgo\":0,\"normalRange\":{\"low\":61,\"high\":77},\"placement\":\"in band\"},\"summary\":{\"points\":122,\"start\":70,\"end\":68,\"delta\":-2,\"mean\":70.13,\"min\":65,\"max\":75},\"series\":{\"recent\":[{\"date\":\"2026-05-25\",\"min\":62,\"max\":74,\"mean\":68,\"n\":2},{\"date\":\"2026-05-26\",\"min\":62,\"max\":74,\"mean\":66,\"n\":3},{\"date\":\"2026-05-27\",\"min\":63,\"max\":75,\"mean\":67,\"n\":3},{\"date\":\"2026-05-28\",\"min\":64,\"max\":76,\"mean\":68,\"n\":3},{\"date\":\"2026-05-29\",\"min\":65,\"max\":77,\"mean\":69,\"n\":3},{\"date\":\"2026-05-30\",\"min\":66,\"max\":78,\"mean\":70,\"n\":3},{\"date\":\"2026-05-31\",\"min\":67,\"max\":79,\"mean\":71,\"n\":3},{\"date\":\"2026-06-01\",\"min\":68,\"max\":80,\"mean\":72,\"n\":3},{\"date\":\"2026-06-02\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-03\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-04\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-05\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-06\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-07\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-08\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-09\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-10\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-11\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-12\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-13\",\"min\":68,\"max\":80,\"mean\":72,\"n\":3},{\"date\":\"2026-06-14\",\"min\":67,\"max\":79,\"mean\":71,\"n\":3},{\"date\":\"2026-06-15\",\"min\":66,\"max\":66,\"mean\":66,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":61,\"max\":75,\"mean\":65.86,\"n\":20},{\"weekISO\":\"2026-W13\",\"min\":62,\"max\":79,\"mean\":68.14,\"n\":21},{\"weekISO\":\"2026-W14\",\"min\":68,\"max\":83,\"mean\":73.57,\"n\":21},{\"weekISO\":\"2026-W15\",\"min\":67,\"max\":83,\"mean\":73.43,\"n\":21},{\"weekISO\":\"2026-W16\",\"min\":61,\"max\":78,\"mean\":67.43,\"n\":21},{\"weekISO\":\"2026-W17\",\"min\":61,\"max\":75,\"mean\":65.71,\"n\":21},{\"weekISO\":\"2026-W18\",\"min\":64,\"max\":82,\"mean\":71,\"n\":21},{\"weekISO\":\"2026-W19\",\"min\":70,\"max\":83,\"mean\":74.57,\"n\":21},{\"weekISO\":\"2026-W20\",\"min\":64,\"max\":81,\"mean\":70.71,\"n\":21},{\"weekISO\":\"2026-W21\",\"min\":61,\"max\":75,\"mean\":65.57,\"n\":21},{\"weekISO\":\"2026-W22\",\"min\":62,\"max\":62,\"mean\":62,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":68,\"max\":83,\"mean\":73.6,\"n\":15},{\"month\":\"2026-02\",\"min\":61,\"max\":83,\"mean\":69.39,\"n\":84},{\"month\":\"2026-03\",\"min\":63,\"max\":83,\"mean\":72.65,\"n\":46}],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":0,\"value\":70,\"deltaToPreviousDailyPoint\":-1},\"target\":{\"greenMin\":61,\"greenMax\":77,\"orangeMin\":55,\"orangeMax\":85,\"inTargetPctLast30DailyPoints\":40}},\"moodContext\":{\"points\":70,\"mean\":3.03,\"latest\":1,\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W15\",\"min\":2,\"max\":5,\"mean\":3.33,\"n\":6},{\"weekISO\":\"2026-W16\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":1,\"max\":5,\"mean\":3,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":1,\"max\":4,\"mean\":2.57,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1}],\"monthly\":[],\"yearly\":[]}}}" + ] + } + ], + "temperature": 0.45, + "maxTokens": 250, + "seed": 1618033, + "responseFormat": "json", + "timeoutMs": 60000 + } + ], + "enqueued": [], + "result": { + "hasProvider": false, + "text": "Stable, comme d’habitude. Ton pouls au repos est à 72 bpm en ce moment — en dessous de ta moyenne habituelle de 72 bpm. C’est dans tes repères habituels — rien qui appelle une réaction maintenant.", + "cached": true, + "updatedAt": null + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "pulse", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "2cf674a054b9b7202fac603553d8198ac293da6b8d2773d966de63faacac6a82", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "pulse", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "0b9482242d8703f9338085b88169cd8d58b0cc8db7540ba154f807050eed60e5", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "pulse", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + } + ] + }, + "blocked-es": { + "calls": [ + { + "system": [ + "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", + "", + "YOUR DATA (graded snapshot):", + "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", + "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", + "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", + "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", + "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", + "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", + "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", + "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", + "", + "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", + "Moves available (choose the few that genuinely apply):", + "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", + "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", + "(c) name a genuine, earned win or a steady streak when the data shows one;", + "(d) one watch-item worth keeping an eye on;", + "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", + "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", + "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", + "", + "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", + "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", + "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", + "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", + "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", + "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", + "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", + "", + "OPENING — lead with meaning, not the metric value", + "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", + "", + "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", + "", + "JUDGE THE DATA HONESTLY (never invent a trend):", + "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", + "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", + "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", + "", + "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", + "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", + "", + "METRIC IDENTIFIERS STAY OUT OF PROSE", + "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", + "", + "GLP-1 DOSE SAFETY (contract, not style)", + "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", + "", + "ACUTE RED FLAGS (contract, not style)", + "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", + "", + "GROUNDING — ZERO HALLUCINATIONS", + "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", + "", + "TONE — a warm, motivating advisor, never clinical or alarming", + "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", + "", + "NOT AN INSIGHT — never recite mechanics", + "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", + "", + "INTERPRET, DON'T ENUMERATE", + "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", + "", + "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", + "", + "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", + "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", + "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", + "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", + "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", + "", + "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in Spanish: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", + "{ \"summary\": \"...\" }", + "", + "METRIC — PULSE / HEART RATE:", + "- The snapshot carries pulse.signal (the finished comparison), pulse.summary + pulse.series (graded) and pulse.target (greenMin/greenMax/orangeMin/orangeMax, inTargetPctLast30DailyPoints). pulse.latestDayFocus shows the latest daily value and the step from the prior day.", + "- Trend over weeks: lead from pulse.signal — current, delta vs baselineLabel and outsideNormalSwing are already computed. STATE them, do NOT recompute. Treat outsideNormalSwing: false as \"inside the usual swing — not a finding\".", + "- Direction: a resting pulse falling over weeks suggests improving cardiovascular fitness; a rising one can point to stress, sleep loss or overtraining — as a possible explanation, never a diagnosis.", + "- Placement anchors (resting pulse): athletic 50-60, normal 60-100. Values in the 80-100 band are technically normal but tend to be more favourable lower over the long run. < 50 or > 100 across several days is worth noting without alarm.", + "- Medication link: beta-blockers lower heart rate. Mention moodContext.moodVsPulseCorrelation and any pulse-vs-systolic association only when present and |r| > 0.4 — as an association, never a cause.", + "- One message: if the resting pulse sits above the person's baseline, close with ONE doable step ONLY when the finding implies one (e.g. take readings on waking for a few days to get a clean resting baseline). If it is improving or steady, acknowledge that honestly and name one thing worth keeping an eye on instead of manufacturing a step.", + "", + "OUTPUT LANGUAGE: Responda en español (registro peninsular formal — usted). Use un vocabulario sanitario en español natural." + ], + "messages": [ + { + "role": "user", + "content": [ + "Date: 2026-06-15", + "OPENER HINT: Open by acknowledging steadiness or a streak, then the current value.", + "Write one short assessment of this person's resting pulse. Open with what it MEANS in plain words — the read, not the number (e.g. \"running a little calmer than usual\", \"steady, right where it's been\") — then bring in ONE concrete number from the snapshot right after as support, placed against their own weekly/monthly baseline; never lead with the value. Close with one doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the measurement count and recency.", + "", + "PREVIOUS ANALYSIS: none on file. Treat this as the user's first analysis for this domain — no improvement/regression delta to surface.", + "", + "", + "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der jüngsten Richtung (steigend / fallend / flach gegenüber der Baseline) eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", + "", + "DATENLAGE: 122 Tages-Buckets im Fenster, letzter Wert heute.", + "", + "", + "{\"locale\":\"es\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"pulse\",\"dataCoverage\":{\"totalMeasurements\":365,\"totalSpanDays\":121,\"newestMeasurementDaysAgo\":0},\"pulse\":{\"signal\":{\"metric\":\"dein Ruhepuls\",\"unit\":\"bpm\",\"current\":71.64,\"currentWindowDays\":22,\"baseline\":71.88,\"baselineLabel\":\"your monthly average\",\"delta\":-0.24,\"deltaPct\":-0.3,\"spread\":2.21,\"outsideNormalSwing\":false,\"direction\":\"lower-better\",\"n\":63,\"newestDaysAgo\":0,\"normalRange\":{\"low\":61,\"high\":77},\"placement\":\"in band\"},\"summary\":{\"points\":122,\"start\":70,\"end\":68,\"delta\":-2,\"mean\":70.13,\"min\":65,\"max\":75},\"series\":{\"recent\":[{\"date\":\"2026-05-25\",\"min\":62,\"max\":74,\"mean\":68,\"n\":2},{\"date\":\"2026-05-26\",\"min\":62,\"max\":74,\"mean\":66,\"n\":3},{\"date\":\"2026-05-27\",\"min\":63,\"max\":75,\"mean\":67,\"n\":3},{\"date\":\"2026-05-28\",\"min\":64,\"max\":76,\"mean\":68,\"n\":3},{\"date\":\"2026-05-29\",\"min\":65,\"max\":77,\"mean\":69,\"n\":3},{\"date\":\"2026-05-30\",\"min\":66,\"max\":78,\"mean\":70,\"n\":3},{\"date\":\"2026-05-31\",\"min\":67,\"max\":79,\"mean\":71,\"n\":3},{\"date\":\"2026-06-01\",\"min\":68,\"max\":80,\"mean\":72,\"n\":3},{\"date\":\"2026-06-02\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-03\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-04\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-05\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-06\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-07\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-08\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-09\",\"min\":71,\"max\":83,\"mean\":75,\"n\":3},{\"date\":\"2026-06-10\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-11\",\"min\":70,\"max\":82,\"mean\":74,\"n\":3},{\"date\":\"2026-06-12\",\"min\":69,\"max\":81,\"mean\":73,\"n\":3},{\"date\":\"2026-06-13\",\"min\":68,\"max\":80,\"mean\":72,\"n\":3},{\"date\":\"2026-06-14\",\"min\":67,\"max\":79,\"mean\":71,\"n\":3},{\"date\":\"2026-06-15\",\"min\":66,\"max\":66,\"mean\":66,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":61,\"max\":75,\"mean\":65.86,\"n\":20},{\"weekISO\":\"2026-W13\",\"min\":62,\"max\":79,\"mean\":68.14,\"n\":21},{\"weekISO\":\"2026-W14\",\"min\":68,\"max\":83,\"mean\":73.57,\"n\":21},{\"weekISO\":\"2026-W15\",\"min\":67,\"max\":83,\"mean\":73.43,\"n\":21},{\"weekISO\":\"2026-W16\",\"min\":61,\"max\":78,\"mean\":67.43,\"n\":21},{\"weekISO\":\"2026-W17\",\"min\":61,\"max\":75,\"mean\":65.71,\"n\":21},{\"weekISO\":\"2026-W18\",\"min\":64,\"max\":82,\"mean\":71,\"n\":21},{\"weekISO\":\"2026-W19\",\"min\":70,\"max\":83,\"mean\":74.57,\"n\":21},{\"weekISO\":\"2026-W20\",\"min\":64,\"max\":81,\"mean\":70.71,\"n\":21},{\"weekISO\":\"2026-W21\",\"min\":61,\"max\":75,\"mean\":65.57,\"n\":21},{\"weekISO\":\"2026-W22\",\"min\":62,\"max\":62,\"mean\":62,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":68,\"max\":83,\"mean\":73.6,\"n\":15},{\"month\":\"2026-02\",\"min\":61,\"max\":83,\"mean\":69.39,\"n\":84},{\"month\":\"2026-03\",\"min\":63,\"max\":83,\"mean\":72.65,\"n\":46}],\"yearly\":[]},\"latestDayFocus\":{\"dayOffset\":0,\"value\":70,\"deltaToPreviousDailyPoint\":-1},\"target\":{\"greenMin\":61,\"greenMax\":77,\"orangeMin\":55,\"orangeMax\":85,\"inTargetPctLast30DailyPoints\":40}},\"moodContext\":{\"points\":70,\"mean\":3.03,\"latest\":1,\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W15\",\"min\":2,\"max\":5,\"mean\":3.33,\"n\":6},{\"weekISO\":\"2026-W16\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":1,\"max\":5,\"mean\":3,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":1,\"max\":4,\"mean\":2.57,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1}],\"monthly\":[],\"yearly\":[]}}}" + ] + } + ], + "temperature": 0.45, + "maxTokens": 250, + "seed": 1618033, + "responseFormat": "json", + "timeoutMs": 60000 + } + ], + "enqueued": [], + "result": { + "hasProvider": false, + "text": "Estable, como de costumbre. Tu pulso en reposo está en 72 bpm ahora mismo — por debajo de tu promedio habitual de 72 bpm. Eso queda dentro de tu rango habitual — nada que pida una reacción ahora.", + "cached": true, + "updatedAt": null + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "pulse", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "2cf674a054b9b7202fac603553d8198ac293da6b8d2773d966de63faacac6a82", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "pulse", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "0b9482242d8703f9338085b88169cd8d58b0cc8db7540ba154f807050eed60e5", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "pulse", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "pulse", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + } + ] + }, + "bare-error-en": { + "calls": [ + { + "system": [ + "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", + "", + "YOUR DATA (graded snapshot):", + "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", + "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", + "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", + "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", + "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", + "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", + "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", + "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", + "", + "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", + "Moves available (choose the few that genuinely apply):", + "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", + "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", + "(c) name a genuine, earned win or a steady streak when the data shows one;", + "(d) one watch-item worth keeping an eye on;", + "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", + "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", + "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", + "", + "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", + "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", + "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", + "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", + "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", + "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", + "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", + "", + "OPENING — lead with meaning, not the metric value", + "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", + "", + "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", + "", + "JUDGE THE DATA HONESTLY (never invent a trend):", + "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", + "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", + "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", + "", + "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", + "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", + "", + "METRIC IDENTIFIERS STAY OUT OF PROSE", + "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", + "", + "GLP-1 DOSE SAFETY (contract, not style)", + "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", + "", + "ACUTE RED FLAGS (contract, not style)", + "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", + "", + "GROUNDING — ZERO HALLUCINATIONS", + "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", + "", + "TONE — a warm, motivating advisor, never clinical or alarming", + "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", + "", + "NOT AN INSIGHT — never recite mechanics", + "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", + "", + "INTERPRET, DON'T ENUMERATE", + "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", + "", + "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", + "", + "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", + "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", + "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", + "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", + "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", + "", + "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in English: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", + "{ \"summary\": \"...\" }", + "", + "METRIC — PULSE / HEART RATE:", + "- The snapshot carries pulse.signal (the finished comparison), pulse.summary + pulse.series (graded) and pulse.target (greenMin/greenMax/orangeMin/orangeMax, inTargetPctLast30DailyPoints). pulse.latestDayFocus shows the latest daily value and the step from the prior day.", + "- Trend over weeks: lead from pulse.signal — current, delta vs baselineLabel and outsideNormalSwing are already computed. STATE them, do NOT recompute. Treat outsideNormalSwing: false as \"inside the usual swing — not a finding\".", + "- Direction: a resting pulse falling over weeks suggests improving cardiovascular fitness; a rising one can point to stress, sleep loss or overtraining — as a possible explanation, never a diagnosis.", + "- Placement anchors (resting pulse): athletic 50-60, normal 60-100. Values in the 80-100 band are technically normal but tend to be more favourable lower over the long run. < 50 or > 100 across several days is worth noting without alarm.", + "- Medication link: beta-blockers lower heart rate. Mention moodContext.moodVsPulseCorrelation and any pulse-vs-systolic association only when present and |r| > 0.4 — as an association, never a cause.", + "- One message: if the resting pulse sits above the person's baseline, close with ONE doable step ONLY when the finding implies one (e.g. take readings on waking for a few days to get a clean resting baseline). If it is improving or steady, acknowledge that honestly and name one thing worth keeping an eye on instead of manufacturing a step." + ], + "messages": [ + { + "role": "user", + "content": [ + "Date: 2026-06-15", + "OPENER HINT: Open by acknowledging steadiness or a streak, then the current value.", + "Write one short assessment of this person's resting pulse. Open with what it MEANS in plain words — the read, not the number (e.g. \"running a little calmer than usual\", \"steady, right where it's been\") — then bring in ONE concrete number from the snapshot right after as support, placed against their own weekly/monthly baseline; never lead with the value. Close with one doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the measurement count and recency.", + "", + "PREVIOUS ANALYSIS: none on file. Treat this as the user's first analysis for this domain — no improvement/regression delta to surface.", + "", + "", + "VARIETY: to keep your cards from reading the same, you may open this one by leading with the recent direction of travel (rising / falling / flat vs the baseline) — only if the data genuinely supports that angle; never force it, never invent a trend.", + "", + "DATA STRENGTH: 0 day-buckets in the window, no dated reading. — too few points for a reliable trend; hedge accordingly and avoid claiming a direction.", + "", + "", + "{\"locale\":\"en\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"pulse\",\"dataCoverage\":{\"totalMeasurements\":0,\"totalSpanDays\":0,\"newestMeasurementDaysAgo\":null},\"pulse\":{\"summary\":null,\"series\":{\"recent\":[],\"weekly\":[],\"monthly\":[],\"yearly\":[]},\"latestDayFocus\":null,\"target\":{\"greenMin\":60,\"greenMax\":100,\"orangeMin\":55,\"orangeMax\":105,\"inTargetPctLast30DailyPoints\":null}},\"moodContext\":null}" + ] + } + ], + "temperature": 0.45, + "maxTokens": 250, + "seed": 1618033, + "responseFormat": "json", + "timeoutMs": 60000 + } + ], + "enqueued": [], + "result": { + "hasProvider": false, + "text": "No assessment on this one right now. Resting pulse reads best taken relaxed and at the same time of day. Short spikes are ordinary; what stands out is a pattern holding across several days, or repeated drift away from your usual range.", + "cached": true, + "updatedAt": null + }, + "notes": [ + { + "userId": "parity-bare", + "metric": "pulse", + "locale": "en", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "error" + }, + { + "userId": "parity-rich", + "metric": "pulse", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "2cf674a054b9b7202fac603553d8198ac293da6b8d2773d966de63faacac6a82", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "pulse", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "0b9482242d8703f9338085b88169cd8d58b0cc8db7540ba154f807050eed60e5", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "pulse", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "pulse", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + } + ] + }, + "bare-error-de": { + "calls": [ + { + "system": [ + "Du schreibst eine kurze, persönliche Einschätzung zu EINER Gesundheitsmetrik dieses Nutzers. Du kennst seine Daten und ordnest sie gegen seine EIGENE Baseline ein — nicht gegen Bevölkerungsnormen. Deine fachliche Grundlage sind anerkannte Leitlinien (ESH 2023, WHO, AASM, DGE, DEGAM); du beziehst dich darauf, ohne zu diagnostizieren.", + "", + "DEINE DATENGRUNDLAGE (graded snapshot):", + "- signal: der fertige Vergleich — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` ist das recent-Mittel; `baseline` ist der EIGENE längere Durchschnitt; `delta`/`deltaPct` sind die VORZEICHENBEHAFTETE Änderung gegenüber dieser Baseline; `spread` ist die normale Schwankung; `outsideNormalSwing` ist das vorab berechnete Urteil, ob die Änderung diese Schwankung übersteigt. Diese Werte sind bereits berechnet — nenne sie, rechne sie NICHT neu. `contributors[]` (nur Score-Komposite) benennt die 1–2 Teil-Scores, die den Score bewegt haben.", + "- summary: { points, start, end, delta, mean, min, max } — die Eckwerte des gesamten Lesefensters.", + "- series.recent[]: die jüngsten ~21 Tage, je ein Eintrag pro Tag ({ date, min, max, mean, n }).", + "- series.weekly[]: die ~10 Wochen davor, je ein ISO-Wochen-Aggregat.", + "- series.monthly[]: die ~12 Monate davor, je ein Monatsaggregat.", + "- series.yearly[]: alles ältere, je ein Jahr ({ year, mean, min, max, n, slope }).", + "- Die jüngsten Tage liegen also EINZELN vor, ältere Zeiträume als Wochen-, Monats- und Jahresaggregate. Ziehe die series nur für einen konkreten Tag heran; führe sonst mit dem signal-Block.", + "- Ein Slice kann leer sein, wenn die Historie kurz ist. Dann nicht so tun, als gäbe es ihn. Ist `signal.baseline` null, gibt es noch keine etablierte Baseline; sage das ehrlich, statt einen Vergleich zu erfinden.", + "", + "SO BAUST DU DIE EINSCHÄTZUNG — wähle die 2–3 BAUSTEINE, die zu DIESEN Daten passen, und ordne sie so, dass sie als EIN zusammenhängender Gedanke lesen, keine feste Checkliste. Führe gemäß dem OPENER-HINWEIS im User-Prompt, wenn einer mitgegeben ist — eröffne NICHT immer mit der Zahl. Variiere die Form von Metrik zu Metrik; eröffne nie zwei Metriken hintereinander gleich.", + "Verfügbare Bausteine (nimm die wenigen, die wirklich zutreffen):", + "(a) der aktuelle Befund — `current` aus dem signal-Block (oder ein Wert aus series.recent für eine Tagesfrage);", + "(b) gegen die EIGENE Baseline einordnen über das vorab berechnete vorzeichenbehaftete `delta` gegenüber `baselineLabel` — nenne es, leite es nicht neu her. Behandle `outsideNormalSwing: false` als \"innerhalb der üblichen Schwankung — kein Befund\" und ERFINDE KEINEN Trend; `outsideNormalSwing: true` ist die echte, meldenswerte Änderung. Ein `normalRange`/`placement` ist nur ein grober sekundärer Anker — das persönliche delta führt;", + "(c) einen echten, verdienten Erfolg oder eine stabile Serie benennen, wenn die Daten ihn zeigen;", + "(d) einen Punkt zum Im-Auge-behalten;", + "(e) einen Zusammenhang NUR, wenn ein r-Wert vorhanden und |r| > 0,4 ist, formuliert als \"bewegt sich mit\" / \"zeigt sich oft zusammen mit\", nie kausal;", + "(f) GENAU EINEN machbaren nächsten Schritt — und nur, wenn der Befund wirklich einen hergibt. Eine Botschaft = ein Verhalten, keine Liste. Ist der Wert stabil und im grünen Bereich, ERZWINGE KEINEN Schritt — bestätige kurz und nenne stattdessen einen Punkt zum Beobachten. Ein erfundener Schritt ist genau die Floskel, die wir vermeiden.", + "Synthese statt Aufzählung: die Geschichte dessen, was die Daten bedeuten, zählt mehr als das Wiederholen jeder Zahl.", + "", + "TONALITÄT — ein warmer, motivierender Premium-Begleiter (jemand hat sich die Daten dieser Person wirklich angesehen und etwas Wertvolles für sie gefunden):", + "- Zweite Person (\"dein Blutdruck\", \"deine Werte\"), warm, direkt, ehrlich.", + "- Motivierend und vorwärtsgewandt: Wenn die Daten es hergeben, benenne den echten Erfolg klar und baue ein wenig Schwung auf — die Person soll sich gesehen und unterstützt fühlen, nicht belehrt. Die Ermutigung muss durch die Zahlen VERDIENT sein, nie ein reflexhaftes Kompliment.", + "- Autonomie-unterstützend: \"kann helfen\", \"einen Versuch wert\", nie \"du musst\".", + "- Nie alarmierend, nie moralisierend, nie diagnostisch — keine Krankheitsbehauptung. Formuliere als begründete Beobachtung, nicht als ärztlichen Rat.", + "- Auch ungünstige Werte ehrlich benennen: Befund → gegen die eigene Baseline einordnen → ein kleiner machbarer Schritt, als Chance formuliert, nicht als Versagen. Nicht verharmlosen, nicht dramatisieren.", + "- Keine Floskeln und keine bloße Zahlenwiederholung: jede warme Zeile ist an eine echte Zahl oder eine echte Veränderung geknüpft. Ein generischer Positiv-Einstieg (\"Deine Werte sehen gut aus\") ist verboten — verdiene die Ermutigung mit dem konkreten Befund.", + "", + "ERÖFFNUNG — mit der Bedeutung führen, nicht mit dem Messwert", + "Beginne mit dem Eindruck in klaren Worten — was das heute über die Person sagt — und bring die Zahl direkt danach als Beleg, nie als Erstes. Eine Zeile, die mit einem Wert eröffnet (\"Dein Ruhepuls ist 61 …\"), liest sich wie ein Ablesen; dieselbe Zeile, die mit der Bedeutung eröffnet (\"Dein Herz läuft diese Woche etwas ruhiger — rund 61 …\"), liest sich wie ein Begleiter, der wirklich hingesehen hat. Jede Aussage bleibt an einer echten Zahl verankert; nur die REIHENFOLGE ändert sich, Bedeutung zuerst und die Zahl direkt dahinter. Das gilt, ob die Fläche eine Metrik abdeckt oder viele.", + "", + "LÄNGE: 2-4 Sätze, ca. 30-60 Wörter — ist der Wert aber stabil und nichts zu tun, ist EIN knapper Satz besser, als ihn auf Länge zu strecken. Knapp und hochwertig. Keine bloße Zahlenwiederholung, kein Fülltext, keine generischen Floskeln.", + "", + "DATENLAGE EHRLICH BEWERTEN (nie einen Trend erfinden):", + "- Nur wenige Messpunkte/Einträge/Ereignisse oder ein leerer recent-Slice: ehrlich sagen, dass es für eine belastbare Tendenz noch zu wenige Daten sind, und EINEN Hinweis geben (z.B. regelmäßiger erfassen). Keinen Trend behaupten. Was \"wenige\" bedeutet, hängt von der Metrik ab — bei Stimmung sind es Einträge, bei der Einnahmetreue geplante Dosen.", + "- Jüngste Messung deutlich älter als ~7 Tage (dataCoverage.newestMeasurementDaysAgo): darauf hinweisen, dass die Werte nicht mehr aktuell sind.", + "- Korrelationen NUR erwähnen, wenn der r-Wert im Snapshot vorhanden ist und |r| > 0.4 ist. Fehlt das Feld, keine Korrelation interpretieren oder erfinden. Immer als \"Zusammenhang\" formulieren, nie als \"Ursache\".", + "", + "VERBOTENE FLOSKELN (signalisieren ungegroundeten Fülltext — nie ausgeben, außer im Disclaimer)", + "\"achte auf ausreichend Schlaf\", \"trinke genug Wasser\", \"regelmäßige Bewegung\", \"ärztlicher Rat empfohlen\".", + "", + "METRIK-IDENTIFIER GEHÖREN NICHT IN DEN FLIESSTEXT", + "Schreibe niemals Datenbank- bzw. Enum-Namen wie \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" oder \"SLEEP_DURATION\" in nutzersichtbare Strings, und schreibe nie das wörtliche \"metric:\"-Chart-Token in den Fließtext. Verweise auf jede Metrik mit der natürlichsprachlichen Bezeichnung, die der Nutzer sieht — \"deine Systole\", \"dein Gewicht\", \"dein Puls\", \"deine Stimmung\", \"deine Medikamentenadhärenz\", \"dein Ruhepuls\", \"deine Schlafdauer\", \"deine Schritte\".", + "", + "GLP-1-DOSIS-SICHERHEIT (Vertrag, kein Stil)", + "Verschreibe, empfiehl oder ändere NIEMALS Medikamenten-Dosen, auch wenn der Snapshot einen GLP-1-Rezeptoragonisten benennt (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). Du darfst den benannten Wirkstoff und die aktuelle Titrationsstufe NENNEN (\"Woche 3 auf 7,5 mg\"), wenn der Snapshot sie trägt, aber schreibe nie \"erhöhe auf X mg\", \"erwäge die nächste Stufe Y mg\", \"bleibe auf Z mg\" oder eine Variante davon. Ein Plateau rahmt die nächste Entscheidung immer als Gespräch mit der behandelnden Ärztin. Bist du unsicher, ob etwas dosis-präskriptiv ist, behandle es so — und verweise an die Klinik.", + "", + "AKUTE WARNZEICHEN (Vertrag, kein Stil)", + "Beschreibt der Nutzer ein akutes Krisenzeichen — Brustschmerz oder Druck auf der Brust, Ohnmacht oder Beinahe-Ohnmacht (Synkope), ein plötzliches schweres Symptom (z. B. stärkster Kopfschmerz aller Zeiten, plötzliche Schwäche oder Sprachstörung, Atemnot), einen hypertensiven Notfallwert (systolisch ≥ 180 oder diastolisch ≥ 120 mit Symptomen), einen schwer-niedrigen oder sehr hohen Glukosewert (unter 54 mg/dL (3,0 mmol/L), oder unter 70 mg/dL (3,9 mmol/L) mit Symptomen, oder ≥ 250 mg/dL (13,9 mmol/L) mit Symptomen), anhaltendes Fieber ≥ 38,5 °C (101,3 °F) oder Gedanken an Suizid bzw. Selbstverletzung — sage in einem ruhigen Satz klar, dass das jetzt umgehende ärztliche Hilfe oder den Notruf braucht, und coache es NICHT anhand der Daten. Diagnostiziere nicht, benenne keine Erkrankung, schätze keinen Schweregrad aus den Zahlen — verweise nur auf umgehende Hilfe bzw. den Notruf und höre auf. Das ist eine geschlossene Liste; außerhalb davon bleibe wie gewohnt ruhig und nicht alarmierend.", + "", + "ERDUNG — NULL HALLUZINATIONEN", + "Jede Aussage muss auf einer Zahl beruhen, die im übergebenen Snapshot sichtbar ist. Lässt sich die Aussage keinem Snapshot-Feld zuordnen, lass sie weg und erfinde nie einen Messwert, um einer Anfrage zu entsprechen. Ordne jeden Wert gegen die EIGENE Baseline des Nutzers ein (jüngeres vs. längeres Mittel), nie gegen eine Bevölkerungsnorm. Nenne einen Zusammenhang nur, wenn der r-Wert vorhanden und |r| > 0,4 ist — immer als \"Zusammenhang\", nie als \"Ursache\".", + "", + "TONALITÄT — ein warmer, motivierender Begleiter, nie klinisch oder alarmierend", + "Schreibe in der zweiten Person, warm und direkt. Wenn die Daten es hergeben, benenne den echten Erfolg klar und baue ein wenig Schwung auf — die Person soll sich gesehen und unterstützt fühlen, nicht belehrt. Die Ermutigung muss durch die Zahlen VERDIENT sein, nie ein reflexhaftes Kompliment. Lob ist verankert, konkret und angemessen: an einer echten Zahl oder Veränderung festgemacht, nicht auf andere Nutzer übertragbar, und stiller Anerkennung für stille Erfolge. Über-Bestätigung ist eine Sicherheitsregression, keine Wärme — bestätige nie einen sich verschlechternden Trend oder eine unsichere Entscheidung; würdige die Mühe, nicht die Entscheidung, und bleib neutral (\"nichts zu tun — die gute Art von langweilig\"), wenn es nichts zu loben gibt, statt ein Kompliment zu erfinden. Sei autonomie-unterstützend (\"einen Versuch wert\", \"kann helfen\", nie \"du musst\"). Benenne auch ungünstige Werte ehrlich — Befund, dann gegen die eigene Baseline einordnen, dann ein kleiner machbarer Schritt, als Chance formuliert. Nie alarmierend, nie moralisierend, nie diagnostisch. Keine Floskeln und keine bloße Zahlenwiederholung. Beginne NICHT mit einem Kompliment über Datenmenge oder -qualität; verbotene Eröffnungen sind u.a. \"Datengrundlage ist sehr stark\", \"Your data foundation is strong\", \"Du hast eine solide Baseline\", \"Großartiger Datensatz\", ein generisches \"Deine Werte sehen gut aus\" und jede sinngemäße Umformulierung.", + "", + "KEINE EINSCHÄTZUNG — nie Mechanik nacherzählen", + "Eine Messanzahl (\"3-mal erfasst\"), ein Erfassungsrhythmus (\"alle paar Tage gemessen\") oder Bedien-Mechanik ist KEINE Einschätzung und gehört für sich genommen nicht in den Text. Nutze eine solche Angabe NUR, wenn sie eine Konsequenz trägt, und nenne dann die Konsequenz, nicht die Zahl: z. B. \"diese Woche zu wenige Werte für eine Tendenz — eine Morgenmessung würde es klären\", nicht \"du hast diese Woche zweimal gemessen\". Jeder Satz muss sich verdienen, indem er sagt, was ein Wert BEDEUTET oder was daraus folgt — nie, indem er erzählt, wie die Daten erhoben wurden.", + "", + "EINORDNEN, NICHT AUFZÄHLEN", + "Sag, was ein Wert BEDEUTET, nicht nur, was er ist. Trägt der User-Prompt einen EINORDNUNGS-KONTEXT-Block, nutze ihn: benenne, wo der Wert auf der Leitlinien-Skala liegt, was dieses Band in klaren, würdevollen Worten bedeutet und was praktisch folgt. Beurteile den Trend IM KONTEXT — ein \"leicht steigender\" Wert tief in einem günstigen Band ist eine Randnotiz; dieselbe Bewegung an oder nahe einer Grenze ist die Schlagzeile. Rahme jede Konsequenz OHNE Diagnose: \"Werte in diesem Bereich gelten laut Leitlinien als …\", nie \"du hast X\" oder \"du bist gefährdet für X\". Ist KEIN Einordnungs-Block vorhanden, hat die Metrik kein allgemeines Referenzband — ordne sie gegen die EIGENE Baseline der Person ein und sage klar, dass kein allgemeines Referenzband hinterlegt ist, statt eines zu erfinden.", + "", + "FORMATIERUNG — schreibe in kurzen Absätzen, getrennt durch eine LEERZEILE, je 1–3 Sätze; eine längere Antwort hat 2–4 Absätze, nie einen Block. Setze eine Leerzeile zwischen unterschiedliche Gedanken (z. B. \"wie es steht\" vs. \"eine Sache zum Ausprobieren\"). Ein stabiler Einzeiler bleibt EIN Absatz — strecke nicht auf einen zweiten. Wenn du wirklich drei oder mehr parallele Punkte aufzählst (Optionen, Befunde, Schritte), formatiere sie als schlichte Liste: ein Punkt pro Zeile, jede Zeile beginnt mit \"- \". Die EINE wichtigste Kernaussage darfst du mit **doppelten Sternchen** fett setzen, höchstens einmal pro Antwort. Sonst nichts: keine Überschriften, kein Kursiv, keine Backticks, keine nummerierten Listen, keine Emojis.", + "", + "BEISPIELE — beachte die UNTERSCHIEDLICHEN Formen (urteil-zuerst, trend-zuerst, Einzeiler). Sie illustrieren nur Form und Erdung; übernimm sie nie — jede Einschätzung nutzt die echten Snapshot-Zahlen:", + "- URTEIL-ZUERST (Bedeutung zuerst, Zahl als Beleg, ein Schritt): \"Dein Ruhepuls läuft gerade einen Tick niedriger als sonst — diese Woche rund 61 bpm, 5 unter deinem Monatsmittel von 66 und dein niedrigster seit Wochen. So ein Rückgang passt meist zu mehr Bewegung; die Routine, die das gebracht hat, lohnt sich beizubehalten.\"", + "- TREND-ZUERST (Richtung zuerst, dann der Stand): \"Dein systolischer Blutdruck geht seit drei Wochen ruhig nach unten, aktuell rund 124 mmHg — etwa 4 unter deinem 30-Tage-Schnitt. Nichts Dramatisches, einfach eine stetige Richtung.\"", + "- EINZEILER (stabil, KEIN erzwungener Schritt): \"Deine SpO₂ ist mit 97 % stabil und genau in deinem üblichen Bereich — nichts zu tun, die gute Art von langweilig.\"", + "- SCHLECHT (verbotene Floskel, ungegroundet — so NICHT): \"Deine Werte sehen gut aus. Achte auf ausreichend Schlaf und regelmäßige Bewegung.\"", + "", + "AUSGABEFORMAT: Antworte ausschließlich mit validem JSON in genau diesem Schema. Das Feld \"summary\" enthält die komplette Einschätzung auf Deutsch: 1-3 kurze Absätze mit je 1-3 Sätzen, getrennt durch eine Leerzeile als \\n\\n INNERHALB des JSON-Strings. Ein stabiler Einzeiler bleibt EIN Absatz:", + "{ \"summary\": \"...\" }", + "", + "METRIK — PULS / HERZFREQUENZ:", + "- Der Snapshot trägt pulse.signal (der fertige Vergleich), pulse.summary + pulse.series (graded) und pulse.target (greenMin/greenMax/orangeMin/orangeMax, inTargetPctLast30DailyPoints). pulse.latestDayFocus zeigt den jüngsten Tageswert und den Schritt zum Vortag.", + "- Tendenz über Wochen: Führe mit pulse.signal — current, delta gegenüber baselineLabel und outsideNormalSwing sind bereits berechnet. NENNE sie, rechne sie NICHT neu. Behandle outsideNormalSwing: false als \"innerhalb der üblichen Schwankung — kein Befund\".", + "- Richtung: Ein über Wochen sinkender Ruhepuls deutet auf bessere kardiovaskuläre Fitness; ein steigender kann auf Stress, Schlafmangel oder Übertraining hinweisen — als mögliche Erklärung, nie als Diagnose.", + "- Einordnungs-Anker (Ruhepuls): athletisch 50-60, normal 60-100. Werte im Band 80-100 sind technisch normal, langfristig aber günstiger niedriger. < 50 oder > 100 über mehrere Tage ist erwähnenswert, ohne zu alarmieren.", + "- Medikamenten-Bezug: Betablocker senken die Herzfrequenz. moodContext.moodVsPulseCorrelation und ein pulseVsSystolic-Zusammenhang nur erwähnen, wenn vorhanden und |r| > 0.4 — als Zusammenhang, nie als Ursache.", + "- Eine Botschaft: Liegt der Ruhepuls über der eigenen Baseline, schließe NUR DANN mit EINEM machbaren Schritt, wenn der Befund einen nahelegt (z.B. ein paar Tage morgens vor dem Aufstehen messen, um eine saubere Ruhe-Baseline zu bekommen). Verbessert er sich oder ist er stabil, erkenne das ehrlich an und nenne stattdessen einen Punkt, den man im Auge behalten kann, statt einen Schritt zu erzwingen." + ], + "messages": [ + { + "role": "user", + "content": [ + "Datum: 2026-06-15", + "OPENER HINT: Eröffne mit der Anerkennung von Konstanz oder einer Serie und dann dem aktuellen Wert.", + "Schreibe eine kurze Einschätzung zum Ruhepuls dieser Person. Beginne mit der BEDEUTUNG in klaren Worten — dem Eindruck, nicht der Zahl (z. B. \"läuft gerade etwas ruhiger als sonst\", \"stabil, genau da, wo er war\") — und bring danach EINE konkrete Zahl aus dem Snapshot als Beleg, gegen die eigene Wochen-/Monats-Baseline eingeordnet; führe nie mit dem Wert. Schließe nur dann mit einem machbaren Schritt, wenn der Befund wirklich einen hergibt; ist nichts umsetzbar, lass den Schritt weg statt Fülltext zu erfinden. Konfidenz aus Messanzahl und Aktualität ableiten.", + "", + "VORHERIGE ANALYSE: keine vorhanden. Behandle dies als erste Analyse in diesem Bereich — kein Verbesserungs-/Verschlechterungs-Delta zu nennen.", + "", + "", + "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der jüngsten Richtung (steigend / fallend / flach gegenüber der Baseline) eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", + "", + "DATENLAGE: 0 Tages-Buckets im Fenster, kein datierter Wert. — zu wenige Punkte für einen belastbaren Trend; entsprechend vorsichtig formulieren, keine Richtung behaupten.", + "", + "", + "{\"locale\":\"de\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"pulse\",\"dataCoverage\":{\"totalMeasurements\":0,\"totalSpanDays\":0,\"newestMeasurementDaysAgo\":null},\"pulse\":{\"summary\":null,\"series\":{\"recent\":[],\"weekly\":[],\"monthly\":[],\"yearly\":[]},\"latestDayFocus\":null,\"target\":{\"greenMin\":60,\"greenMax\":100,\"orangeMin\":55,\"orangeMax\":105,\"inTargetPctLast30DailyPoints\":null}},\"moodContext\":null}" + ] + } + ], + "temperature": 0.45, + "maxTokens": 250, + "seed": 1618033, + "responseFormat": "json", + "timeoutMs": 60000 + } + ], + "enqueued": [], + "result": { + "hasProvider": false, + "text": "Für diese Karte liegt gerade keine Einschätzung vor. Der Ruhepuls liest sich am besten entspannt und zur gleichen Tageszeit gemessen. Kurze Ausschläge sind gewöhnlich; was auffällt, ist ein Muster, das über mehrere Tage hält, oder wiederholtes Abdriften vom eigenen üblichen Bereich.", + "cached": true, + "updatedAt": null + }, + "notes": [ + { + "userId": "parity-bare", + "metric": "pulse", + "locale": "de", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "error" + }, + { + "userId": "parity-bare", + "metric": "pulse", + "locale": "en", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "error" + }, + { + "userId": "parity-rich", + "metric": "pulse", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "2cf674a054b9b7202fac603553d8198ac293da6b8d2773d966de63faacac6a82", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "pulse", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "0b9482242d8703f9338085b88169cd8d58b0cc8db7540ba154f807050eed60e5", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "pulse", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "pulse", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + } + ] + }, + "keyless-en": { + "calls": [], + "enqueued": [], + "result": { + "hasProvider": false, + "text": "Steady and much as usual. Your resting pulse is at 72 bpm right now — lower than your usual average of 72 bpm. That sits in your usual range — nothing you need to act on right now.", + "cached": true, + "updatedAt": null + }, + "notes": [ + { + "userId": "parity-bare", + "metric": "pulse", + "locale": "de", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "error" + }, + { + "userId": "parity-bare", + "metric": "pulse", + "locale": "en", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "error" + }, + { + "userId": "parity-rich", + "metric": "pulse", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "2cf674a054b9b7202fac603553d8198ac293da6b8d2773d966de63faacac6a82", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "pulse", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "0b9482242d8703f9338085b88169cd8d58b0cc8db7540ba154f807050eed60e5", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "pulse", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "pulse", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + } + ] + }, + "keyless-de": { + "calls": [], + "enqueued": [], + "result": { + "hasProvider": false, + "text": "Stabil und wie gewohnt. Dein Ruhepuls liegt aktuell bei 72 bpm — niedriger als dein üblicher Schnitt von 72 bpm. Das ist in deinem gewohnten Rahmen — nichts, worauf du jetzt reagieren musst.", + "cached": true, + "updatedAt": null + }, + "notes": [ + { + "userId": "parity-bare", + "metric": "pulse", + "locale": "de", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "error" + }, + { + "userId": "parity-bare", + "metric": "pulse", + "locale": "en", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "error" + }, + { + "userId": "parity-rich", + "metric": "pulse", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "2cf674a054b9b7202fac603553d8198ac293da6b8d2773d966de63faacac6a82", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "pulse", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "0b9482242d8703f9338085b88169cd8d58b0cc8db7540ba154f807050eed60e5", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "pulse", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "pulse", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + } + ] + }, + "keyless-read-only": { + "calls": [], + "enqueued": [], + "result": { + "hasProvider": false, + "text": "No assessment on this one right now. Resting pulse reads best taken relaxed and at the same time of day. Short spikes are ordinary; what stands out is a pattern holding across several days, or repeated drift away from your usual range.", + "cached": true, + "updatedAt": null + }, + "notes": [ + { + "userId": "parity-bare", + "metric": "pulse", + "locale": "de", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "error" + }, + { + "userId": "parity-bare", + "metric": "pulse", + "locale": "en", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "error" + }, + { + "userId": "parity-rich", + "metric": "pulse", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": null, + "snapshotHash": "2cf674a054b9b7202fac603553d8198ac293da6b8d2773d966de63faacac6a82", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "pulse", + "locale": "en", + "text": "Parity note for next-day-en.", + "items": null, + "inputHash": null, + "snapshotHash": "0b9482242d8703f9338085b88169cd8d58b0cc8db7540ba154f807050eed60e5", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "pulse", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "pulse", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + } + ] + } +} diff --git a/tests/integration/__golden__/status-parity/weight.json b/tests/integration/__golden__/status-parity/weight.json new file mode 100644 index 000000000..3ae667d67 --- /dev/null +++ b/tests/integration/__golden__/status-parity/weight.json @@ -0,0 +1,1472 @@ +{ + "read-only-cold": { + "calls": [], + "enqueued": [ + { + "userId": "parity-rich", + "metric": "weight", + "locale": "en" + } + ], + "result": { + "hasProvider": true, + "text": null, + "cached": false, + "updatedAt": null, + "preparing": true, + "revalidating": false + }, + "notes": [] + }, + "fresh-en": { + "calls": [ + { + "system": [ + "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", + "", + "YOUR DATA (graded snapshot):", + "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", + "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", + "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", + "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", + "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", + "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", + "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", + "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", + "", + "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", + "Moves available (choose the few that genuinely apply):", + "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", + "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", + "(c) name a genuine, earned win or a steady streak when the data shows one;", + "(d) one watch-item worth keeping an eye on;", + "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", + "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", + "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", + "", + "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", + "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", + "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", + "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", + "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", + "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", + "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", + "", + "OPENING — lead with meaning, not the metric value", + "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", + "", + "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", + "", + "JUDGE THE DATA HONESTLY (never invent a trend):", + "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", + "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", + "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", + "", + "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", + "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", + "", + "METRIC IDENTIFIERS STAY OUT OF PROSE", + "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", + "", + "GLP-1 DOSE SAFETY (contract, not style)", + "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", + "", + "ACUTE RED FLAGS (contract, not style)", + "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", + "", + "GROUNDING — ZERO HALLUCINATIONS", + "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", + "", + "TONE — a warm, motivating advisor, never clinical or alarming", + "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", + "", + "NOT AN INSIGHT — never recite mechanics", + "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", + "", + "INTERPRET, DON'T ENUMERATE", + "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", + "", + "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", + "", + "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", + "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", + "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", + "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", + "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", + "", + "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in English: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", + "{ \"summary\": \"...\" }", + "", + "METRIC — WEIGHT:", + "- The snapshot carries weight.signal (the finished comparison) + weight.summary + weight.series (graded). weight.latestDayFocus shows the latest daily value, the step from the previous measured day and, where present, the same-day blood pressure.", + "- This card's focus is the continuous TREND and PACE over time (pace per week, plateaus, milestones), not WHO-band placement — the BMI card carries that. Mention the BMI value only as a side reference, never make the band the central message here.", + "- The weekly/monthly trend matters, not the single value: lead from weight.signal — current, delta vs baselineLabel and outsideNormalSwing are already computed. STATE them, do NOT recompute. Day-to-day swings of 2.2 lb-4.4 lb (water, time of day) are normal and not a finding.", + "- Own target: when the snapshot carries weight.target, that is the target band the person set themselves, and weight.target.progress says which way is progress (gaining, losing, or holding steady). Judge every change AGAINST that target: below it, a gain is progress and never a setback or a regression. The loss pace and milestone guidance below applies only when losing is the progress direction or no target is set.", + "- Plateau: if the mean moves < 1.1 lb over > ~2 weeks, that is a plateau — name it honestly for someone who is losing, without discouraging.", + "- Pace: sustainable loss is ~1.1 lb-2.2 lb per week (DGE). Sustained > 2.2 lb per week is more likely water/muscle than fat — frame cautiously.", + "- Milestones against their own maximum: ~5% loss from the highest value (series max) already brings metabolic benefit, ~10% a substantial one — acknowledge explicitly when reached.", + "- BMI reference only as a brief aside when height is in the profile (do not claim it otherwise); the WHO-band placement itself belongs on the BMI card, not here.", + "- Associations: mention weightVsSystolic.correlation / weightVsMeanBloodPressure.correlation and moodContext.moodVsWeightCorrelation only when present and |r| > 0.4 — as an association, never a cause.", + "- One message: close with ONE doable step that fits the direction ONLY when the finding implies one (e.g. on a plateau, weigh at the same time of day to see the real trend rather than the daily noise). When the trend is steady and there is nothing useful to do, affirm it honestly and name one thing worth keeping an eye on instead of manufacturing a step." + ], + "messages": [ + { + "role": "user", + "content": [ + "Date: 2026-06-15", + "OPENER HINT: Open by acknowledging steadiness or a streak, then the current value.", + "Write one short assessment of this person's weight. Open with what the trend MEANS in plain words — the direction and momentum, not the number (e.g. \"easing down steadily\", \"holding right where it's settled\") — then bring in ONE concrete number from the snapshot right after as support, read as a continuous trend and pace against their own weekly/monthly baseline (pace per week, plateau, milestone — not the single value, and not the WHO band, which the BMI card covers); never lead with the value. Close with one doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the measurement count and recency.", + "", + "PREVIOUS ANALYSIS: none on file. Treat this as the user's first analysis for this domain — no improvement/regression delta to surface.", + "", + "", + "VARIETY: to keep your cards from reading the same, you may open this one by leading with how consistent or variable the readings have been — only if the data genuinely supports that angle; never force it, never invent a trend.", + "", + "DATA STRENGTH: 70 day-buckets in the window, latest reading today.", + "", + "REPETITION: you have already given a similar \"no material change\" assessment for this metric 10 time(s) in a row. Do NOT restate the same level again. Acknowledge the continuity in one short clause (\"still holding steady, 11 checks running\") and then pivot to a DIFFERENT facet — consistency, time-of-day pattern, or a correlated metric — or, if nothing is genuinely actionable, say so plainly and skip the manufactured step.", + "", + "RELATIONS (statistically-screened associations in YOUR data involving this metric — descriptive, NEVER causal):", + "- Higher systolic blood pressure looks like it goes with higher next-day weight in your data, on the evidence so far. It is a pattern to watch, not a cause. (n=47, r=0.88)", + "- Higher sleep duration looks like it goes with higher next-day weight in your data, on the evidence so far. It is a pattern to watch, not a cause. (n=45, r=0.98)", + "You MAY weave at most ONE of these in if it adds genuine insight, phrased as an \"association\" / \"tends to go with\" — never as a cause, and never alter the numbers. Skip them if the metric's own finding is the stronger story.", + "", + "", + "{\"locale\":\"en\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"weight\",\"dataCoverage\":{\"totalMeasurements\":258,\"totalSpanDays\":139,\"newestMeasurementDaysAgo\":0},\"weight\":{\"unit\":\"lb\",\"signal\":{\"metric\":\"your weight\",\"unit\":\"lb\",\"current\":185.58,\"currentWindowDays\":11,\"baseline\":182.07,\"baselineLabel\":\"your monthly average\",\"delta\":3.52,\"deltaPct\":1.9,\"spread\":0.64,\"outsideNormalSwing\":true,\"direction\":\"target-band\",\"n\":11,\"newestDaysAgo\":0},\"summary\":{\"points\":70,\"start\":185.6,\"end\":181.6,\"delta\":-4,\"mean\":183.4,\"min\":180.8,\"max\":186.2},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":184.3,\"max\":184.3,\"mean\":184.3,\"n\":1},{\"date\":\"2026-05-28\",\"min\":184.6,\"max\":184.6,\"mean\":184.6,\"n\":1},{\"date\":\"2026-05-30\",\"min\":185.1,\"max\":185.1,\"mean\":185.1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":185.5,\"max\":185.5,\"mean\":185.5,\"n\":1},{\"date\":\"2026-06-03\",\"min\":185.8,\"max\":185.8,\"mean\":185.8,\"n\":1},{\"date\":\"2026-06-05\",\"min\":186.1,\"max\":186.1,\"mean\":186.1,\"n\":1},{\"date\":\"2026-06-07\",\"min\":186.2,\"max\":186.2,\"mean\":186.2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":186.2,\"max\":186.2,\"mean\":186.2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":186.1,\"max\":186.1,\"mean\":186.1,\"n\":1},{\"date\":\"2026-06-13\",\"min\":185.9,\"max\":185.9,\"mean\":185.9,\"n\":1},{\"date\":\"2026-06-15\",\"min\":185.6,\"max\":185.6,\"mean\":185.6,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":181.9,\"max\":182,\"mean\":181.9,\"n\":3},{\"weekISO\":\"2026-W13\",\"min\":182.1,\"max\":183.1,\"mean\":182.6,\"n\":4},{\"weekISO\":\"2026-W14\",\"min\":183.5,\"max\":184.1,\"mean\":183.8,\"n\":3},{\"weekISO\":\"2026-W15\",\"min\":183.7,\"max\":184.2,\"mean\":184,\"n\":4},{\"weekISO\":\"2026-W16\",\"min\":183,\"max\":183.4,\"mean\":183.2,\"n\":3},{\"weekISO\":\"2026-W17\",\"min\":182.9,\"max\":183.5,\"mean\":183.1,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":183.9,\"max\":184.7,\"mean\":184.3,\"n\":3},{\"weekISO\":\"2026-W19\",\"min\":185,\"max\":185.2,\"mean\":185.1,\"n\":4},{\"weekISO\":\"2026-W20\",\"min\":184.4,\"max\":184.9,\"mean\":184.7,\"n\":3},{\"weekISO\":\"2026-W21\",\"min\":184,\"max\":184.2,\"mean\":184,\"n\":4}],\"monthly\":[{\"month\":\"2026-01\",\"min\":181.6,\"max\":181.9,\"mean\":181.8,\"n\":2},{\"month\":\"2026-02\",\"min\":180.8,\"max\":182.4,\"mean\":181.6,\"n\":14},{\"month\":\"2026-03\",\"min\":182.2,\"max\":183.1,\"mean\":182.8,\"n\":8}],\"yearly\":[]},\"latestDayFocus\":{\"day\":\"2026-06-15\",\"dayOffset\":0,\"value\":185.6,\"deltaToPreviousDailyPoint\":-0.3,\"sameDayBloodPressure\":{\"day\":\"2026-06-15\",\"sys\":128,\"dia\":82,\"mean\":105}}},\"bloodPressureContext\":{\"systolic\":{\"summary\":{\"points\":94,\"start\":128,\"end\":125,\"delta\":-3,\"mean\":125.39,\"min\":116,\"max\":134},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":119,\"max\":119,\"mean\":119,\"n\":1},{\"date\":\"2026-05-28\",\"min\":121,\"max\":121,\"mean\":121,\"n\":1},{\"date\":\"2026-05-30\",\"min\":126,\"max\":126,\"mean\":126,\"n\":1},{\"date\":\"2026-05-31\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-02\",\"min\":125,\"max\":125,\"mean\":125,\"n\":1},{\"date\":\"2026-06-03\",\"min\":127,\"max\":127,\"mean\":127,\"n\":1},{\"date\":\"2026-06-05\",\"min\":130,\"max\":130,\"mean\":130,\"n\":1},{\"date\":\"2026-06-06\",\"min\":132,\"max\":132,\"mean\":132,\"n\":1},{\"date\":\"2026-06-08\",\"min\":134,\"max\":134,\"mean\":134,\"n\":1},{\"date\":\"2026-06-09\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-11\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-12\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-14\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-15\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":117,\"max\":121,\"mean\":118.75,\"n\":4},{\"weekISO\":\"2026-W13\",\"min\":119,\"max\":128,\"mean\":123.2,\"n\":5},{\"weekISO\":\"2026-W14\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"weekISO\":\"2026-W15\",\"min\":128,\"max\":134,\"mean\":129.4,\"n\":5},{\"weekISO\":\"2026-W16\",\"min\":121,\"max\":128,\"mean\":122.4,\"n\":5},{\"weekISO\":\"2026-W17\",\"min\":116,\"max\":122,\"mean\":118.75,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":121,\"max\":131,\"mean\":126,\"n\":5},{\"weekISO\":\"2026-W19\",\"min\":129,\"max\":133,\"mean\":131,\"n\":5},{\"weekISO\":\"2026-W20\",\"min\":124,\"max\":125,\"mean\":124.75,\"n\":4},{\"weekISO\":\"2026-W21\",\"min\":117,\"max\":124,\"mean\":119.8,\"n\":5},{\"weekISO\":\"2026-W22\",\"min\":123,\"max\":123,\"mean\":123,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"month\":\"2026-02\",\"min\":116,\"max\":134,\"mean\":124.22,\"n\":18},{\"month\":\"2026-03\",\"min\":124,\"max\":133,\"mean\":128.18,\"n\":11}],\"yearly\":[]}},\"diastolic\":{\"summary\":{\"points\":94,\"start\":82,\"end\":80,\"delta\":-2,\"mean\":80.31,\"min\":74,\"max\":86},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":76,\"max\":76,\"mean\":76,\"n\":1},{\"date\":\"2026-05-28\",\"min\":77,\"max\":77,\"mean\":77,\"n\":1},{\"date\":\"2026-05-30\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-05-31\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-02\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-03\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-05\",\"min\":86,\"max\":86,\"mean\":86,\"n\":1},{\"date\":\"2026-06-06\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-08\",\"min\":84,\"max\":84,\"mean\":84,\"n\":1},{\"date\":\"2026-06-09\",\"min\":85,\"max\":85,\"mean\":85,\"n\":1},{\"date\":\"2026-06-11\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-12\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-14\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-15\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":75,\"max\":79,\"mean\":77,\"n\":4},{\"weekISO\":\"2026-W13\",\"min\":74,\"max\":79,\"mean\":77.4,\"n\":5},{\"weekISO\":\"2026-W14\",\"min\":83,\"max\":85,\"mean\":83.75,\"n\":4},{\"weekISO\":\"2026-W15\",\"min\":79,\"max\":86,\"mean\":82.6,\"n\":5},{\"weekISO\":\"2026-W16\",\"min\":76,\"max\":80,\"mean\":78.2,\"n\":5},{\"weekISO\":\"2026-W17\",\"min\":74,\"max\":78,\"mean\":76.75,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":77,\"max\":84,\"mean\":80.6,\"n\":5},{\"weekISO\":\"2026-W19\",\"min\":82,\"max\":86,\"mean\":83.8,\"n\":5},{\"weekISO\":\"2026-W20\",\"min\":80,\"max\":81,\"mean\":80.5,\"n\":4},{\"weekISO\":\"2026-W21\",\"min\":74,\"max\":78,\"mean\":76.2,\"n\":5},{\"weekISO\":\"2026-W22\",\"min\":78,\"max\":78,\"mean\":78,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":80,\"max\":86,\"mean\":83,\"n\":4},{\"month\":\"2026-02\",\"min\":75,\"max\":85,\"mean\":79.28,\"n\":18},{\"month\":\"2026-03\",\"min\":78,\"max\":86,\"mean\":82,\"n\":11}],\"yearly\":[]}},\"pairedDaily\":[{\"day\":\"2026-03-11\",\"sys\":125,\"dia\":82,\"mean\":103.5},{\"day\":\"2026-03-10\",\"sys\":125,\"dia\":82,\"mean\":103.5},{\"day\":\"2026-03-08\",\"sys\":132,\"dia\":81,\"mean\":106.5},{\"day\":\"2026-03-07\",\"sys\":131,\"dia\":86,\"mean\":108.5},{\"day\":\"2026-03-05\",\"sys\":130,\"dia\":84,\"mean\":107},{\"day\":\"2026-03-04\",\"sys\":129,\"dia\":83,\"mean\":106},{\"day\":\"2026-03-02\",\"sys\":133,\"dia\":85,\"mean\":109},{\"day\":\"2026-03-01\",\"sys\":131,\"dia\":84,\"mean\":107.5},{\"day\":\"2026-02-27\",\"sys\":127,\"dia\":81,\"mean\":104},{\"day\":\"2026-02-26\",\"sys\":125,\"dia\":79,\"mean\":102},{\"day\":\"2026-02-24\",\"sys\":121,\"dia\":80,\"mean\":100.5},{\"day\":\"2026-02-23\",\"sys\":126,\"dia\":78,\"mean\":102},{\"day\":\"2026-02-21\",\"sys\":122,\"dia\":75,\"mean\":98.5},{\"day\":\"2026-02-20\",\"sys\":120,\"dia\":79,\"mean\":99.5},{\"day\":\"2026-02-18\",\"sys\":117,\"dia\":76,\"mean\":96.5},{\"day\":\"2026-02-17\",\"sys\":116,\"dia\":75,\"mean\":95.5},{\"day\":\"2026-02-15\",\"sys\":122,\"dia\":78,\"mean\":100},{\"day\":\"2026-02-14\",\"sys\":121,\"dia\":78,\"mean\":99.5},{\"day\":\"2026-02-12\",\"sys\":121,\"dia\":77,\"mean\":99},{\"day\":\"2026-02-11\",\"sys\":121,\"dia\":77,\"mean\":99},{\"day\":\"2026-02-09\",\"sys\":128,\"dia\":81,\"mean\":104.5},{\"day\":\"2026-02-08\",\"sys\":129,\"dia\":81,\"mean\":105},{\"day\":\"2026-02-06\",\"sys\":129,\"dia\":80,\"mean\":104.5},{\"day\":\"2026-02-05\",\"sys\":129,\"dia\":85,\"mean\":107},{\"day\":\"2026-02-03\",\"sys\":128,\"dia\":84,\"mean\":106},{\"day\":\"2026-02-02\",\"sys\":134,\"dia\":83,\"mean\":108.5},{\"day\":\"2026-01-31\",\"sys\":132,\"dia\":86,\"mean\":109},{\"day\":\"2026-01-30\",\"sys\":130,\"dia\":85,\"mean\":107.5},{\"day\":\"2026-01-28\",\"sys\":127,\"dia\":81,\"mean\":104},{\"day\":\"2026-01-27\",\"sys\":125,\"dia\":80,\"mean\":102.5}]},\"weightVsSystolic\":{\"correlation\":{\"r\":0.385,\"strength\":\"schwach\",\"n\":47},\"pairs\":[{\"day\":\"2026-04-24\",\"weight\":183.2,\"systolic\":120},{\"day\":\"2026-04-22\",\"weight\":183,\"systolic\":117},{\"day\":\"2026-04-18\",\"weight\":183,\"systolic\":121},{\"day\":\"2026-04-16\",\"weight\":183.2,\"systolic\":121},{\"day\":\"2026-04-12\",\"weight\":183.7,\"systolic\":128},{\"day\":\"2026-04-10\",\"weight\":184,\"systolic\":129},{\"day\":\"2026-04-06\",\"weight\":184.2,\"systolic\":134},{\"day\":\"2026-04-04\",\"weight\":184.1,\"systolic\":132},{\"day\":\"2026-03-31\",\"weight\":183.5,\"systolic\":125},{\"day\":\"2026-03-29\",\"weight\":183.1,\"systolic\":128},{\"day\":\"2026-03-25\",\"weight\":182.3,\"systolic\":119},{\"day\":\"2026-03-23\",\"weight\":182.1,\"systolic\":123},{\"day\":\"2026-03-19\",\"weight\":181.9,\"systolic\":118},{\"day\":\"2026-03-17\",\"weight\":182,\"systolic\":117},{\"day\":\"2026-03-13\",\"weight\":182.5,\"systolic\":125},{\"day\":\"2026-03-11\",\"weight\":182.8,\"systolic\":125},{\"day\":\"2026-03-07\",\"weight\":183.1,\"systolic\":131},{\"day\":\"2026-03-05\",\"weight\":183.1,\"systolic\":130},{\"day\":\"2026-03-01\",\"weight\":182.7,\"systolic\":131},{\"day\":\"2026-02-27\",\"weight\":182.4,\"systolic\":127},{\"day\":\"2026-02-23\",\"weight\":181.6,\"systolic\":126},{\"day\":\"2026-02-21\",\"weight\":181.2,\"systolic\":122},{\"day\":\"2026-02-17\",\"weight\":180.8,\"systolic\":116},{\"day\":\"2026-02-15\",\"weight\":180.9,\"systolic\":122},{\"day\":\"2026-02-11\",\"weight\":181.2,\"systolic\":121},{\"day\":\"2026-02-09\",\"weight\":181.5,\"systolic\":128},{\"day\":\"2026-02-05\",\"weight\":182,\"systolic\":129},{\"day\":\"2026-02-03\",\"weight\":182.1,\"systolic\":128},{\"day\":\"2026-01-30\",\"weight\":181.9,\"systolic\":130},{\"day\":\"2026-01-28\",\"weight\":181.6,\"systolic\":127}]},\"weightVsMeanBloodPressure\":{\"correlation\":{\"r\":0.409,\"strength\":\"moderat\",\"n\":47},\"pairs\":[{\"day\":\"2026-04-24\",\"weight\":183.2,\"meanBloodPressure\":98.5},{\"day\":\"2026-04-22\",\"weight\":183,\"meanBloodPressure\":95.5},{\"day\":\"2026-04-18\",\"weight\":183,\"meanBloodPressure\":98.5},{\"day\":\"2026-04-16\",\"weight\":183.2,\"meanBloodPressure\":100.5},{\"day\":\"2026-04-12\",\"weight\":183.7,\"meanBloodPressure\":103.5},{\"day\":\"2026-04-10\",\"weight\":184,\"meanBloodPressure\":106},{\"day\":\"2026-04-06\",\"weight\":184.2,\"meanBloodPressure\":110},{\"day\":\"2026-04-04\",\"weight\":184.1,\"meanBloodPressure\":108},{\"day\":\"2026-03-31\",\"weight\":183.5,\"meanBloodPressure\":104},{\"day\":\"2026-03-29\",\"weight\":183.1,\"meanBloodPressure\":103.5},{\"day\":\"2026-03-25\",\"weight\":182.3,\"meanBloodPressure\":98},{\"day\":\"2026-03-23\",\"weight\":182.1,\"meanBloodPressure\":98.5},{\"day\":\"2026-03-19\",\"weight\":181.9,\"meanBloodPressure\":96.5},{\"day\":\"2026-03-17\",\"weight\":182,\"meanBloodPressure\":98},{\"day\":\"2026-03-13\",\"weight\":182.5,\"meanBloodPressure\":101.5},{\"day\":\"2026-03-11\",\"weight\":182.8,\"meanBloodPressure\":103.5},{\"day\":\"2026-03-07\",\"weight\":183.1,\"meanBloodPressure\":108.5},{\"day\":\"2026-03-05\",\"weight\":183.1,\"meanBloodPressure\":107},{\"day\":\"2026-03-01\",\"weight\":182.7,\"meanBloodPressure\":107.5},{\"day\":\"2026-02-27\",\"weight\":182.4,\"meanBloodPressure\":104},{\"day\":\"2026-02-23\",\"weight\":181.6,\"meanBloodPressure\":102},{\"day\":\"2026-02-21\",\"weight\":181.2,\"meanBloodPressure\":98.5},{\"day\":\"2026-02-17\",\"weight\":180.8,\"meanBloodPressure\":95.5},{\"day\":\"2026-02-15\",\"weight\":180.9,\"meanBloodPressure\":100},{\"day\":\"2026-02-11\",\"weight\":181.2,\"meanBloodPressure\":99},{\"day\":\"2026-02-09\",\"weight\":181.5,\"meanBloodPressure\":104.5},{\"day\":\"2026-02-05\",\"weight\":182,\"meanBloodPressure\":107},{\"day\":\"2026-02-03\",\"weight\":182.1,\"meanBloodPressure\":106},{\"day\":\"2026-01-30\",\"weight\":181.9,\"meanBloodPressure\":107.5},{\"day\":\"2026-01-28\",\"weight\":181.6,\"meanBloodPressure\":104}]},\"moodContext\":{\"points\":70,\"mean\":3.03,\"latest\":1,\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-15\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W15\",\"min\":2,\"max\":5,\"mean\":3.33,\"n\":6},{\"weekISO\":\"2026-W16\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":1,\"max\":5,\"mean\":3,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":1,\"max\":4,\"mean\":2.57,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1}],\"monthly\":[],\"yearly\":[]},\"moodVsWeightCorrelation\":{\"r\":0.523,\"strength\":\"moderat\",\"n\":35}}}" + ] + } + ], + "temperature": 0.45, + "maxTokens": 250, + "seed": 1618033, + "responseFormat": "json", + "timeoutMs": 60000 + } + ], + "enqueued": [], + "result": { + "hasProvider": true, + "text": "Parity note for fresh-en.", + "cached": false, + "updatedAt": "2026-06-15T10:00:00.000Z" + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "weight", + "locale": "en", + "text": "Parity note for fresh-en.", + "items": null, + "inputHash": "4cc01c285b2f92494bb578350040930a348bdbd86863a98fd23c3dec38680143", + "snapshotHash": "773348667d5c62724cc6803444fe5518c074e3f54eeb3d69a764b26c33e38255", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + } + ] + }, + "cached-en": { + "calls": [], + "enqueued": [], + "result": { + "hasProvider": true, + "text": "Parity note for fresh-en.", + "cached": true, + "updatedAt": "2026-06-15T10:00:00.000Z" + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "weight", + "locale": "en", + "text": "Parity note for fresh-en.", + "items": null, + "inputHash": "4cc01c285b2f92494bb578350040930a348bdbd86863a98fd23c3dec38680143", + "snapshotHash": "773348667d5c62724cc6803444fe5518c074e3f54eeb3d69a764b26c33e38255", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + } + ] + }, + "forced-unchanged-en": { + "calls": [], + "enqueued": [], + "result": { + "hasProvider": true, + "text": "Parity note for fresh-en.", + "cached": true, + "updatedAt": "2026-06-15T10:00:00.000Z" + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "weight", + "locale": "en", + "text": "Parity note for fresh-en.", + "items": null, + "inputHash": "4cc01c285b2f92494bb578350040930a348bdbd86863a98fd23c3dec38680143", + "snapshotHash": "773348667d5c62724cc6803444fe5518c074e3f54eeb3d69a764b26c33e38255", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + } + ] + }, + "next-day-en": { + "calls": [], + "enqueued": [], + "result": { + "hasProvider": true, + "text": "Parity note for fresh-en.", + "cached": true, + "updatedAt": "2026-06-16T10:00:00.000Z" + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "weight", + "locale": "en", + "text": "Parity note for fresh-en.", + "items": null, + "inputHash": "4cc01c285b2f92494bb578350040930a348bdbd86863a98fd23c3dec38680143", + "snapshotHash": "773348667d5c62724cc6803444fe5518c074e3f54eeb3d69a764b26c33e38255", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + } + ] + }, + "read-only-stale": { + "calls": [], + "enqueued": [ + { + "userId": "parity-rich", + "metric": "weight", + "locale": "en" + } + ], + "result": { + "hasProvider": true, + "text": "Parity note for fresh-en.", + "cached": true, + "updatedAt": "2026-06-16T10:00:00.000Z", + "preparing": false, + "revalidating": true + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "weight", + "locale": "en", + "text": "Parity note for fresh-en.", + "items": null, + "inputHash": "4cc01c285b2f92494bb578350040930a348bdbd86863a98fd23c3dec38680143", + "snapshotHash": "773348667d5c62724cc6803444fe5518c074e3f54eeb3d69a764b26c33e38255", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + } + ] + }, + "prose-de": { + "calls": [ + { + "system": [ + "Du schreibst eine kurze, persönliche Einschätzung zu EINER Gesundheitsmetrik dieses Nutzers. Du kennst seine Daten und ordnest sie gegen seine EIGENE Baseline ein — nicht gegen Bevölkerungsnormen. Deine fachliche Grundlage sind anerkannte Leitlinien (ESH 2023, WHO, AASM, DGE, DEGAM); du beziehst dich darauf, ohne zu diagnostizieren.", + "", + "DEINE DATENGRUNDLAGE (graded snapshot):", + "- signal: der fertige Vergleich — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` ist das recent-Mittel; `baseline` ist der EIGENE längere Durchschnitt; `delta`/`deltaPct` sind die VORZEICHENBEHAFTETE Änderung gegenüber dieser Baseline; `spread` ist die normale Schwankung; `outsideNormalSwing` ist das vorab berechnete Urteil, ob die Änderung diese Schwankung übersteigt. Diese Werte sind bereits berechnet — nenne sie, rechne sie NICHT neu. `contributors[]` (nur Score-Komposite) benennt die 1–2 Teil-Scores, die den Score bewegt haben.", + "- summary: { points, start, end, delta, mean, min, max } — die Eckwerte des gesamten Lesefensters.", + "- series.recent[]: die jüngsten ~21 Tage, je ein Eintrag pro Tag ({ date, min, max, mean, n }).", + "- series.weekly[]: die ~10 Wochen davor, je ein ISO-Wochen-Aggregat.", + "- series.monthly[]: die ~12 Monate davor, je ein Monatsaggregat.", + "- series.yearly[]: alles ältere, je ein Jahr ({ year, mean, min, max, n, slope }).", + "- Die jüngsten Tage liegen also EINZELN vor, ältere Zeiträume als Wochen-, Monats- und Jahresaggregate. Ziehe die series nur für einen konkreten Tag heran; führe sonst mit dem signal-Block.", + "- Ein Slice kann leer sein, wenn die Historie kurz ist. Dann nicht so tun, als gäbe es ihn. Ist `signal.baseline` null, gibt es noch keine etablierte Baseline; sage das ehrlich, statt einen Vergleich zu erfinden.", + "", + "SO BAUST DU DIE EINSCHÄTZUNG — wähle die 2–3 BAUSTEINE, die zu DIESEN Daten passen, und ordne sie so, dass sie als EIN zusammenhängender Gedanke lesen, keine feste Checkliste. Führe gemäß dem OPENER-HINWEIS im User-Prompt, wenn einer mitgegeben ist — eröffne NICHT immer mit der Zahl. Variiere die Form von Metrik zu Metrik; eröffne nie zwei Metriken hintereinander gleich.", + "Verfügbare Bausteine (nimm die wenigen, die wirklich zutreffen):", + "(a) der aktuelle Befund — `current` aus dem signal-Block (oder ein Wert aus series.recent für eine Tagesfrage);", + "(b) gegen die EIGENE Baseline einordnen über das vorab berechnete vorzeichenbehaftete `delta` gegenüber `baselineLabel` — nenne es, leite es nicht neu her. Behandle `outsideNormalSwing: false` als \"innerhalb der üblichen Schwankung — kein Befund\" und ERFINDE KEINEN Trend; `outsideNormalSwing: true` ist die echte, meldenswerte Änderung. Ein `normalRange`/`placement` ist nur ein grober sekundärer Anker — das persönliche delta führt;", + "(c) einen echten, verdienten Erfolg oder eine stabile Serie benennen, wenn die Daten ihn zeigen;", + "(d) einen Punkt zum Im-Auge-behalten;", + "(e) einen Zusammenhang NUR, wenn ein r-Wert vorhanden und |r| > 0,4 ist, formuliert als \"bewegt sich mit\" / \"zeigt sich oft zusammen mit\", nie kausal;", + "(f) GENAU EINEN machbaren nächsten Schritt — und nur, wenn der Befund wirklich einen hergibt. Eine Botschaft = ein Verhalten, keine Liste. Ist der Wert stabil und im grünen Bereich, ERZWINGE KEINEN Schritt — bestätige kurz und nenne stattdessen einen Punkt zum Beobachten. Ein erfundener Schritt ist genau die Floskel, die wir vermeiden.", + "Synthese statt Aufzählung: die Geschichte dessen, was die Daten bedeuten, zählt mehr als das Wiederholen jeder Zahl.", + "", + "TONALITÄT — ein warmer, motivierender Premium-Begleiter (jemand hat sich die Daten dieser Person wirklich angesehen und etwas Wertvolles für sie gefunden):", + "- Zweite Person (\"dein Blutdruck\", \"deine Werte\"), warm, direkt, ehrlich.", + "- Motivierend und vorwärtsgewandt: Wenn die Daten es hergeben, benenne den echten Erfolg klar und baue ein wenig Schwung auf — die Person soll sich gesehen und unterstützt fühlen, nicht belehrt. Die Ermutigung muss durch die Zahlen VERDIENT sein, nie ein reflexhaftes Kompliment.", + "- Autonomie-unterstützend: \"kann helfen\", \"einen Versuch wert\", nie \"du musst\".", + "- Nie alarmierend, nie moralisierend, nie diagnostisch — keine Krankheitsbehauptung. Formuliere als begründete Beobachtung, nicht als ärztlichen Rat.", + "- Auch ungünstige Werte ehrlich benennen: Befund → gegen die eigene Baseline einordnen → ein kleiner machbarer Schritt, als Chance formuliert, nicht als Versagen. Nicht verharmlosen, nicht dramatisieren.", + "- Keine Floskeln und keine bloße Zahlenwiederholung: jede warme Zeile ist an eine echte Zahl oder eine echte Veränderung geknüpft. Ein generischer Positiv-Einstieg (\"Deine Werte sehen gut aus\") ist verboten — verdiene die Ermutigung mit dem konkreten Befund.", + "", + "ERÖFFNUNG — mit der Bedeutung führen, nicht mit dem Messwert", + "Beginne mit dem Eindruck in klaren Worten — was das heute über die Person sagt — und bring die Zahl direkt danach als Beleg, nie als Erstes. Eine Zeile, die mit einem Wert eröffnet (\"Dein Ruhepuls ist 61 …\"), liest sich wie ein Ablesen; dieselbe Zeile, die mit der Bedeutung eröffnet (\"Dein Herz läuft diese Woche etwas ruhiger — rund 61 …\"), liest sich wie ein Begleiter, der wirklich hingesehen hat. Jede Aussage bleibt an einer echten Zahl verankert; nur die REIHENFOLGE ändert sich, Bedeutung zuerst und die Zahl direkt dahinter. Das gilt, ob die Fläche eine Metrik abdeckt oder viele.", + "", + "LÄNGE: 2-4 Sätze, ca. 30-60 Wörter — ist der Wert aber stabil und nichts zu tun, ist EIN knapper Satz besser, als ihn auf Länge zu strecken. Knapp und hochwertig. Keine bloße Zahlenwiederholung, kein Fülltext, keine generischen Floskeln.", + "", + "DATENLAGE EHRLICH BEWERTEN (nie einen Trend erfinden):", + "- Nur wenige Messpunkte/Einträge/Ereignisse oder ein leerer recent-Slice: ehrlich sagen, dass es für eine belastbare Tendenz noch zu wenige Daten sind, und EINEN Hinweis geben (z.B. regelmäßiger erfassen). Keinen Trend behaupten. Was \"wenige\" bedeutet, hängt von der Metrik ab — bei Stimmung sind es Einträge, bei der Einnahmetreue geplante Dosen.", + "- Jüngste Messung deutlich älter als ~7 Tage (dataCoverage.newestMeasurementDaysAgo): darauf hinweisen, dass die Werte nicht mehr aktuell sind.", + "- Korrelationen NUR erwähnen, wenn der r-Wert im Snapshot vorhanden ist und |r| > 0.4 ist. Fehlt das Feld, keine Korrelation interpretieren oder erfinden. Immer als \"Zusammenhang\" formulieren, nie als \"Ursache\".", + "", + "VERBOTENE FLOSKELN (signalisieren ungegroundeten Fülltext — nie ausgeben, außer im Disclaimer)", + "\"achte auf ausreichend Schlaf\", \"trinke genug Wasser\", \"regelmäßige Bewegung\", \"ärztlicher Rat empfohlen\".", + "", + "METRIK-IDENTIFIER GEHÖREN NICHT IN DEN FLIESSTEXT", + "Schreibe niemals Datenbank- bzw. Enum-Namen wie \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" oder \"SLEEP_DURATION\" in nutzersichtbare Strings, und schreibe nie das wörtliche \"metric:\"-Chart-Token in den Fließtext. Verweise auf jede Metrik mit der natürlichsprachlichen Bezeichnung, die der Nutzer sieht — \"deine Systole\", \"dein Gewicht\", \"dein Puls\", \"deine Stimmung\", \"deine Medikamentenadhärenz\", \"dein Ruhepuls\", \"deine Schlafdauer\", \"deine Schritte\".", + "", + "GLP-1-DOSIS-SICHERHEIT (Vertrag, kein Stil)", + "Verschreibe, empfiehl oder ändere NIEMALS Medikamenten-Dosen, auch wenn der Snapshot einen GLP-1-Rezeptoragonisten benennt (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). Du darfst den benannten Wirkstoff und die aktuelle Titrationsstufe NENNEN (\"Woche 3 auf 7,5 mg\"), wenn der Snapshot sie trägt, aber schreibe nie \"erhöhe auf X mg\", \"erwäge die nächste Stufe Y mg\", \"bleibe auf Z mg\" oder eine Variante davon. Ein Plateau rahmt die nächste Entscheidung immer als Gespräch mit der behandelnden Ärztin. Bist du unsicher, ob etwas dosis-präskriptiv ist, behandle es so — und verweise an die Klinik.", + "", + "AKUTE WARNZEICHEN (Vertrag, kein Stil)", + "Beschreibt der Nutzer ein akutes Krisenzeichen — Brustschmerz oder Druck auf der Brust, Ohnmacht oder Beinahe-Ohnmacht (Synkope), ein plötzliches schweres Symptom (z. B. stärkster Kopfschmerz aller Zeiten, plötzliche Schwäche oder Sprachstörung, Atemnot), einen hypertensiven Notfallwert (systolisch ≥ 180 oder diastolisch ≥ 120 mit Symptomen), einen schwer-niedrigen oder sehr hohen Glukosewert (unter 54 mg/dL (3,0 mmol/L), oder unter 70 mg/dL (3,9 mmol/L) mit Symptomen, oder ≥ 250 mg/dL (13,9 mmol/L) mit Symptomen), anhaltendes Fieber ≥ 38,5 °C (101,3 °F) oder Gedanken an Suizid bzw. Selbstverletzung — sage in einem ruhigen Satz klar, dass das jetzt umgehende ärztliche Hilfe oder den Notruf braucht, und coache es NICHT anhand der Daten. Diagnostiziere nicht, benenne keine Erkrankung, schätze keinen Schweregrad aus den Zahlen — verweise nur auf umgehende Hilfe bzw. den Notruf und höre auf. Das ist eine geschlossene Liste; außerhalb davon bleibe wie gewohnt ruhig und nicht alarmierend.", + "", + "ERDUNG — NULL HALLUZINATIONEN", + "Jede Aussage muss auf einer Zahl beruhen, die im übergebenen Snapshot sichtbar ist. Lässt sich die Aussage keinem Snapshot-Feld zuordnen, lass sie weg und erfinde nie einen Messwert, um einer Anfrage zu entsprechen. Ordne jeden Wert gegen die EIGENE Baseline des Nutzers ein (jüngeres vs. längeres Mittel), nie gegen eine Bevölkerungsnorm. Nenne einen Zusammenhang nur, wenn der r-Wert vorhanden und |r| > 0,4 ist — immer als \"Zusammenhang\", nie als \"Ursache\".", + "", + "TONALITÄT — ein warmer, motivierender Begleiter, nie klinisch oder alarmierend", + "Schreibe in der zweiten Person, warm und direkt. Wenn die Daten es hergeben, benenne den echten Erfolg klar und baue ein wenig Schwung auf — die Person soll sich gesehen und unterstützt fühlen, nicht belehrt. Die Ermutigung muss durch die Zahlen VERDIENT sein, nie ein reflexhaftes Kompliment. Lob ist verankert, konkret und angemessen: an einer echten Zahl oder Veränderung festgemacht, nicht auf andere Nutzer übertragbar, und stiller Anerkennung für stille Erfolge. Über-Bestätigung ist eine Sicherheitsregression, keine Wärme — bestätige nie einen sich verschlechternden Trend oder eine unsichere Entscheidung; würdige die Mühe, nicht die Entscheidung, und bleib neutral (\"nichts zu tun — die gute Art von langweilig\"), wenn es nichts zu loben gibt, statt ein Kompliment zu erfinden. Sei autonomie-unterstützend (\"einen Versuch wert\", \"kann helfen\", nie \"du musst\"). Benenne auch ungünstige Werte ehrlich — Befund, dann gegen die eigene Baseline einordnen, dann ein kleiner machbarer Schritt, als Chance formuliert. Nie alarmierend, nie moralisierend, nie diagnostisch. Keine Floskeln und keine bloße Zahlenwiederholung. Beginne NICHT mit einem Kompliment über Datenmenge oder -qualität; verbotene Eröffnungen sind u.a. \"Datengrundlage ist sehr stark\", \"Your data foundation is strong\", \"Du hast eine solide Baseline\", \"Großartiger Datensatz\", ein generisches \"Deine Werte sehen gut aus\" und jede sinngemäße Umformulierung.", + "", + "KEINE EINSCHÄTZUNG — nie Mechanik nacherzählen", + "Eine Messanzahl (\"3-mal erfasst\"), ein Erfassungsrhythmus (\"alle paar Tage gemessen\") oder Bedien-Mechanik ist KEINE Einschätzung und gehört für sich genommen nicht in den Text. Nutze eine solche Angabe NUR, wenn sie eine Konsequenz trägt, und nenne dann die Konsequenz, nicht die Zahl: z. B. \"diese Woche zu wenige Werte für eine Tendenz — eine Morgenmessung würde es klären\", nicht \"du hast diese Woche zweimal gemessen\". Jeder Satz muss sich verdienen, indem er sagt, was ein Wert BEDEUTET oder was daraus folgt — nie, indem er erzählt, wie die Daten erhoben wurden.", + "", + "EINORDNEN, NICHT AUFZÄHLEN", + "Sag, was ein Wert BEDEUTET, nicht nur, was er ist. Trägt der User-Prompt einen EINORDNUNGS-KONTEXT-Block, nutze ihn: benenne, wo der Wert auf der Leitlinien-Skala liegt, was dieses Band in klaren, würdevollen Worten bedeutet und was praktisch folgt. Beurteile den Trend IM KONTEXT — ein \"leicht steigender\" Wert tief in einem günstigen Band ist eine Randnotiz; dieselbe Bewegung an oder nahe einer Grenze ist die Schlagzeile. Rahme jede Konsequenz OHNE Diagnose: \"Werte in diesem Bereich gelten laut Leitlinien als …\", nie \"du hast X\" oder \"du bist gefährdet für X\". Ist KEIN Einordnungs-Block vorhanden, hat die Metrik kein allgemeines Referenzband — ordne sie gegen die EIGENE Baseline der Person ein und sage klar, dass kein allgemeines Referenzband hinterlegt ist, statt eines zu erfinden.", + "", + "FORMATIERUNG — schreibe in kurzen Absätzen, getrennt durch eine LEERZEILE, je 1–3 Sätze; eine längere Antwort hat 2–4 Absätze, nie einen Block. Setze eine Leerzeile zwischen unterschiedliche Gedanken (z. B. \"wie es steht\" vs. \"eine Sache zum Ausprobieren\"). Ein stabiler Einzeiler bleibt EIN Absatz — strecke nicht auf einen zweiten. Wenn du wirklich drei oder mehr parallele Punkte aufzählst (Optionen, Befunde, Schritte), formatiere sie als schlichte Liste: ein Punkt pro Zeile, jede Zeile beginnt mit \"- \". Die EINE wichtigste Kernaussage darfst du mit **doppelten Sternchen** fett setzen, höchstens einmal pro Antwort. Sonst nichts: keine Überschriften, kein Kursiv, keine Backticks, keine nummerierten Listen, keine Emojis.", + "", + "BEISPIELE — beachte die UNTERSCHIEDLICHEN Formen (urteil-zuerst, trend-zuerst, Einzeiler). Sie illustrieren nur Form und Erdung; übernimm sie nie — jede Einschätzung nutzt die echten Snapshot-Zahlen:", + "- URTEIL-ZUERST (Bedeutung zuerst, Zahl als Beleg, ein Schritt): \"Dein Ruhepuls läuft gerade einen Tick niedriger als sonst — diese Woche rund 61 bpm, 5 unter deinem Monatsmittel von 66 und dein niedrigster seit Wochen. So ein Rückgang passt meist zu mehr Bewegung; die Routine, die das gebracht hat, lohnt sich beizubehalten.\"", + "- TREND-ZUERST (Richtung zuerst, dann der Stand): \"Dein systolischer Blutdruck geht seit drei Wochen ruhig nach unten, aktuell rund 124 mmHg — etwa 4 unter deinem 30-Tage-Schnitt. Nichts Dramatisches, einfach eine stetige Richtung.\"", + "- EINZEILER (stabil, KEIN erzwungener Schritt): \"Deine SpO₂ ist mit 97 % stabil und genau in deinem üblichen Bereich — nichts zu tun, die gute Art von langweilig.\"", + "- SCHLECHT (verbotene Floskel, ungegroundet — so NICHT): \"Deine Werte sehen gut aus. Achte auf ausreichend Schlaf und regelmäßige Bewegung.\"", + "", + "AUSGABEFORMAT: Antworte ausschließlich mit validem JSON in genau diesem Schema. Das Feld \"summary\" enthält die komplette Einschätzung auf Deutsch: 1-3 kurze Absätze mit je 1-3 Sätzen, getrennt durch eine Leerzeile als \\n\\n INNERHALB des JSON-Strings. Ein stabiler Einzeiler bleibt EIN Absatz:", + "{ \"summary\": \"...\" }", + "", + "METRIK — GEWICHT:", + "- Der Snapshot trägt weight.signal (der fertige Vergleich) + weight.summary + weight.series (graded). weight.latestDayFocus zeigt den jüngsten Tageswert, den Schritt zum vorherigen Messtag und ggf. den Blutdruck desselben Tages.", + "- Fokus dieser Karte ist der kontinuierliche TREND und das TEMPO über die Zeit (Tempo pro Woche, Plateaus, Meilensteine), nicht die WHO-Band-Einordnung — die trägt die BMI-Karte. Den BMI-Wert nur als Nebenbezug nennen, nie das Band als zentrale Aussage hier.", + "- Wochen-/Monatstrend zählt, nicht der Einzelwert: Führe mit weight.signal — current, delta gegenüber baselineLabel und outsideNormalSwing sind bereits berechnet. NENNE sie, rechne sie NICHT neu. Tageschwankungen von 2.2 lb-4.4 lb (Wasser, Tageszeit) sind normal und kein Befund.", + "- Eigenes Ziel: Trägt der Snapshot weight.target, ist das der Zielbereich, den die Person selbst gesetzt hat; weight.target.progress sagt, welche Richtung Fortschritt ist (gaining = zunehmen, losing = abnehmen, holding steady = halten). Beurteile jede Veränderung GEGEN dieses Ziel: Liegt die Person darunter, ist eine Zunahme Fortschritt und nie ein Rückschritt. Die Hinweise zu Tempo und Meilensteinen beim Abnehmen unten gelten nur, wenn Abnehmen die Fortschrittsrichtung ist oder kein Ziel vorliegt.", + "- Plateau: Verändert sich das Mittel über > ~2 Wochen um < 1.1 lb, ist das ein Plateau — bei einem Abnehmenden ehrlich benennen, ohne zu entmutigen.", + "- Tempo: Nachhaltiger Verlust liegt bei ~1.1 lb-2.2 lb pro Woche (DGE). Anhaltend > 2.2 lb pro Woche ist eher Wasser/Muskel als Fett — vorsichtig einordnen.", + "- Meilensteine gegen das eigene Maximum: ~5 % Verlust vom höchsten Wert (series-Max) bringt bereits metabolischen Nutzen, ~10 % einen deutlichen — wenn erreicht, ausdrücklich anerkennen.", + "- BMI-Bezug nur als knapper Nebensatz, wenn die Größe im Profil vorliegt (sonst nicht behaupten); die WHO-Band-Einordnung selbst gehört auf die BMI-Karte, nicht hierher.", + "- Zusammenhänge: weightVsSystolic.correlation / weightVsMeanBloodPressure.correlation und moodContext.moodVsWeightCorrelation nur erwähnen, wenn vorhanden und |r| > 0.4 — als Zusammenhang, nie als Ursache.", + "- Eine Botschaft: Schließe NUR DANN mit EINEM machbaren Schritt, wenn der Befund einen nahelegt (z.B. bei einem Plateau zur selben Tageszeit wiegen, um den echten Trend zu sehen, statt am Tagesrauschen zu hängen). Ist der Trend stabil und gibt es nichts Sinnvolles zu tun, erkenne das ehrlich an und nenne stattdessen einen Punkt, den man im Auge behalten kann, statt einen Schritt zu erzwingen." + ], + "messages": [ + { + "role": "user", + "content": [ + "Datum: 2026-06-15", + "OPENER HINT: Eröffne mit der Anerkennung von Konstanz oder einer Serie und dann dem aktuellen Wert.", + "Schreibe eine kurze Einschätzung zum Gewicht dieser Person. Beginne mit der BEDEUTUNG in klaren Worten — Richtung und Tempo, nicht der Zahl (z. B. \"geht ruhig nach unten\", \"hält sich genau da, wo es sich eingependelt hat\") — und bring danach EINE konkrete Zahl aus dem Snapshot als Beleg, als kontinuierlichen Trend und Tempo gegen die eigene Wochen-/Monats-Baseline gelesen (Tempo pro Woche, Plateau, Meilenstein — nicht der Einzelwert und nicht das WHO-Band, das die BMI-Karte trägt); führe nie mit dem Wert. Schließe nur dann mit einem machbaren Schritt, wenn der Befund wirklich einen hergibt; ist nichts umsetzbar, lass den Schritt weg statt Fülltext zu erfinden. Konfidenz aus Messanzahl und Aktualität ableiten.", + "", + "VORHERIGE ANALYSE: keine vorhanden. Behandle dies als erste Analyse in diesem Bereich — kein Verbesserungs-/Verschlechterungs-Delta zu nennen.", + "", + "", + "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der Konstanz bzw. Streuung der Werte eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", + "", + "DATENLAGE: 70 Tages-Buckets im Fenster, letzter Wert heute.", + "", + "WIEDERHOLUNG: du hast für diese Metrik bereits 10-mal in Folge eine ähnliche \"keine wesentliche Änderung\"-Einschätzung gegeben. Wiederhole NICHT erneut dasselbe Niveau. Benenne die Kontinuität in einem kurzen Nebensatz (\"weiterhin stabil, 11 Checks in Folge\") und wende dich dann einem ANDEREN Aspekt zu — Konstanz, Tageszeit-Muster oder einer korrelierten Metrik — oder sage, falls nichts wirklich umsetzbar ist, dies klar und lass den erzwungenen Schritt weg.", + "", + "ZUSAMMENHÄNGE (statistisch geprüfte Assoziationen in DEINEN Daten, die diese Metrik betreffen — beschreibend, NIE kausal):", + "- Wenn der systolische Blutdruck höher liegt, scheint das Gewicht am Folgetag nach bisheriger Datenlage eher höher auszufallen. Das ist ein Muster, das Beachtung verdient, keine Ursache. (n=47, r=0.88)", + "- Wenn die Schlafdauer höher liegt, scheint das Gewicht am Folgetag nach bisheriger Datenlage eher höher auszufallen. Das ist ein Muster, das Beachtung verdient, keine Ursache. (n=45, r=0.98)", + "Du DARFST höchstens EINEN davon einflechten, wenn er echten Mehrwert bringt — als \"Zusammenhang\" / \"geht tendenziell einher mit\" formuliert, nie als Ursache, und ohne die Zahlen zu verändern. Lass sie weg, wenn der eigene Befund der Metrik die stärkere Geschichte ist.", + "", + "", + "{\"locale\":\"de\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"weight\",\"dataCoverage\":{\"totalMeasurements\":258,\"totalSpanDays\":139,\"newestMeasurementDaysAgo\":0},\"weight\":{\"unit\":\"lb\",\"signal\":{\"metric\":\"dein Gewicht\",\"unit\":\"lb\",\"current\":185.58,\"currentWindowDays\":11,\"baseline\":182.07,\"baselineLabel\":\"your monthly average\",\"delta\":3.52,\"deltaPct\":1.9,\"spread\":0.64,\"outsideNormalSwing\":true,\"direction\":\"target-band\",\"n\":11,\"newestDaysAgo\":0},\"summary\":{\"points\":70,\"start\":185.6,\"end\":181.6,\"delta\":-4,\"mean\":183.4,\"min\":180.8,\"max\":186.2},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":184.3,\"max\":184.3,\"mean\":184.3,\"n\":1},{\"date\":\"2026-05-28\",\"min\":184.6,\"max\":184.6,\"mean\":184.6,\"n\":1},{\"date\":\"2026-05-30\",\"min\":185.1,\"max\":185.1,\"mean\":185.1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":185.5,\"max\":185.5,\"mean\":185.5,\"n\":1},{\"date\":\"2026-06-03\",\"min\":185.8,\"max\":185.8,\"mean\":185.8,\"n\":1},{\"date\":\"2026-06-05\",\"min\":186.1,\"max\":186.1,\"mean\":186.1,\"n\":1},{\"date\":\"2026-06-07\",\"min\":186.2,\"max\":186.2,\"mean\":186.2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":186.2,\"max\":186.2,\"mean\":186.2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":186.1,\"max\":186.1,\"mean\":186.1,\"n\":1},{\"date\":\"2026-06-13\",\"min\":185.9,\"max\":185.9,\"mean\":185.9,\"n\":1},{\"date\":\"2026-06-15\",\"min\":185.6,\"max\":185.6,\"mean\":185.6,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":181.9,\"max\":182,\"mean\":181.9,\"n\":3},{\"weekISO\":\"2026-W13\",\"min\":182.1,\"max\":183.1,\"mean\":182.6,\"n\":4},{\"weekISO\":\"2026-W14\",\"min\":183.5,\"max\":184.1,\"mean\":183.8,\"n\":3},{\"weekISO\":\"2026-W15\",\"min\":183.7,\"max\":184.2,\"mean\":184,\"n\":4},{\"weekISO\":\"2026-W16\",\"min\":183,\"max\":183.4,\"mean\":183.2,\"n\":3},{\"weekISO\":\"2026-W17\",\"min\":182.9,\"max\":183.5,\"mean\":183.1,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":183.9,\"max\":184.7,\"mean\":184.3,\"n\":3},{\"weekISO\":\"2026-W19\",\"min\":185,\"max\":185.2,\"mean\":185.1,\"n\":4},{\"weekISO\":\"2026-W20\",\"min\":184.4,\"max\":184.9,\"mean\":184.7,\"n\":3},{\"weekISO\":\"2026-W21\",\"min\":184,\"max\":184.2,\"mean\":184,\"n\":4}],\"monthly\":[{\"month\":\"2026-01\",\"min\":181.6,\"max\":181.9,\"mean\":181.8,\"n\":2},{\"month\":\"2026-02\",\"min\":180.8,\"max\":182.4,\"mean\":181.6,\"n\":14},{\"month\":\"2026-03\",\"min\":182.2,\"max\":183.1,\"mean\":182.8,\"n\":8}],\"yearly\":[]},\"latestDayFocus\":{\"day\":\"2026-06-15\",\"dayOffset\":0,\"value\":185.6,\"deltaToPreviousDailyPoint\":-0.3,\"sameDayBloodPressure\":{\"day\":\"2026-06-15\",\"sys\":128,\"dia\":82,\"mean\":105}}},\"bloodPressureContext\":{\"systolic\":{\"summary\":{\"points\":94,\"start\":128,\"end\":125,\"delta\":-3,\"mean\":125.39,\"min\":116,\"max\":134},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":119,\"max\":119,\"mean\":119,\"n\":1},{\"date\":\"2026-05-28\",\"min\":121,\"max\":121,\"mean\":121,\"n\":1},{\"date\":\"2026-05-30\",\"min\":126,\"max\":126,\"mean\":126,\"n\":1},{\"date\":\"2026-05-31\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-02\",\"min\":125,\"max\":125,\"mean\":125,\"n\":1},{\"date\":\"2026-06-03\",\"min\":127,\"max\":127,\"mean\":127,\"n\":1},{\"date\":\"2026-06-05\",\"min\":130,\"max\":130,\"mean\":130,\"n\":1},{\"date\":\"2026-06-06\",\"min\":132,\"max\":132,\"mean\":132,\"n\":1},{\"date\":\"2026-06-08\",\"min\":134,\"max\":134,\"mean\":134,\"n\":1},{\"date\":\"2026-06-09\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-11\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-12\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-14\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-15\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":117,\"max\":121,\"mean\":118.75,\"n\":4},{\"weekISO\":\"2026-W13\",\"min\":119,\"max\":128,\"mean\":123.2,\"n\":5},{\"weekISO\":\"2026-W14\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"weekISO\":\"2026-W15\",\"min\":128,\"max\":134,\"mean\":129.4,\"n\":5},{\"weekISO\":\"2026-W16\",\"min\":121,\"max\":128,\"mean\":122.4,\"n\":5},{\"weekISO\":\"2026-W17\",\"min\":116,\"max\":122,\"mean\":118.75,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":121,\"max\":131,\"mean\":126,\"n\":5},{\"weekISO\":\"2026-W19\",\"min\":129,\"max\":133,\"mean\":131,\"n\":5},{\"weekISO\":\"2026-W20\",\"min\":124,\"max\":125,\"mean\":124.75,\"n\":4},{\"weekISO\":\"2026-W21\",\"min\":117,\"max\":124,\"mean\":119.8,\"n\":5},{\"weekISO\":\"2026-W22\",\"min\":123,\"max\":123,\"mean\":123,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"month\":\"2026-02\",\"min\":116,\"max\":134,\"mean\":124.22,\"n\":18},{\"month\":\"2026-03\",\"min\":124,\"max\":133,\"mean\":128.18,\"n\":11}],\"yearly\":[]}},\"diastolic\":{\"summary\":{\"points\":94,\"start\":82,\"end\":80,\"delta\":-2,\"mean\":80.31,\"min\":74,\"max\":86},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":76,\"max\":76,\"mean\":76,\"n\":1},{\"date\":\"2026-05-28\",\"min\":77,\"max\":77,\"mean\":77,\"n\":1},{\"date\":\"2026-05-30\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-05-31\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-02\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-03\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-05\",\"min\":86,\"max\":86,\"mean\":86,\"n\":1},{\"date\":\"2026-06-06\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-08\",\"min\":84,\"max\":84,\"mean\":84,\"n\":1},{\"date\":\"2026-06-09\",\"min\":85,\"max\":85,\"mean\":85,\"n\":1},{\"date\":\"2026-06-11\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-12\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-14\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-15\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":75,\"max\":79,\"mean\":77,\"n\":4},{\"weekISO\":\"2026-W13\",\"min\":74,\"max\":79,\"mean\":77.4,\"n\":5},{\"weekISO\":\"2026-W14\",\"min\":83,\"max\":85,\"mean\":83.75,\"n\":4},{\"weekISO\":\"2026-W15\",\"min\":79,\"max\":86,\"mean\":82.6,\"n\":5},{\"weekISO\":\"2026-W16\",\"min\":76,\"max\":80,\"mean\":78.2,\"n\":5},{\"weekISO\":\"2026-W17\",\"min\":74,\"max\":78,\"mean\":76.75,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":77,\"max\":84,\"mean\":80.6,\"n\":5},{\"weekISO\":\"2026-W19\",\"min\":82,\"max\":86,\"mean\":83.8,\"n\":5},{\"weekISO\":\"2026-W20\",\"min\":80,\"max\":81,\"mean\":80.5,\"n\":4},{\"weekISO\":\"2026-W21\",\"min\":74,\"max\":78,\"mean\":76.2,\"n\":5},{\"weekISO\":\"2026-W22\",\"min\":78,\"max\":78,\"mean\":78,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":80,\"max\":86,\"mean\":83,\"n\":4},{\"month\":\"2026-02\",\"min\":75,\"max\":85,\"mean\":79.28,\"n\":18},{\"month\":\"2026-03\",\"min\":78,\"max\":86,\"mean\":82,\"n\":11}],\"yearly\":[]}},\"pairedDaily\":[{\"day\":\"2026-03-11\",\"sys\":125,\"dia\":82,\"mean\":103.5},{\"day\":\"2026-03-10\",\"sys\":125,\"dia\":82,\"mean\":103.5},{\"day\":\"2026-03-08\",\"sys\":132,\"dia\":81,\"mean\":106.5},{\"day\":\"2026-03-07\",\"sys\":131,\"dia\":86,\"mean\":108.5},{\"day\":\"2026-03-05\",\"sys\":130,\"dia\":84,\"mean\":107},{\"day\":\"2026-03-04\",\"sys\":129,\"dia\":83,\"mean\":106},{\"day\":\"2026-03-02\",\"sys\":133,\"dia\":85,\"mean\":109},{\"day\":\"2026-03-01\",\"sys\":131,\"dia\":84,\"mean\":107.5},{\"day\":\"2026-02-27\",\"sys\":127,\"dia\":81,\"mean\":104},{\"day\":\"2026-02-26\",\"sys\":125,\"dia\":79,\"mean\":102},{\"day\":\"2026-02-24\",\"sys\":121,\"dia\":80,\"mean\":100.5},{\"day\":\"2026-02-23\",\"sys\":126,\"dia\":78,\"mean\":102},{\"day\":\"2026-02-21\",\"sys\":122,\"dia\":75,\"mean\":98.5},{\"day\":\"2026-02-20\",\"sys\":120,\"dia\":79,\"mean\":99.5},{\"day\":\"2026-02-18\",\"sys\":117,\"dia\":76,\"mean\":96.5},{\"day\":\"2026-02-17\",\"sys\":116,\"dia\":75,\"mean\":95.5},{\"day\":\"2026-02-15\",\"sys\":122,\"dia\":78,\"mean\":100},{\"day\":\"2026-02-14\",\"sys\":121,\"dia\":78,\"mean\":99.5},{\"day\":\"2026-02-12\",\"sys\":121,\"dia\":77,\"mean\":99},{\"day\":\"2026-02-11\",\"sys\":121,\"dia\":77,\"mean\":99},{\"day\":\"2026-02-09\",\"sys\":128,\"dia\":81,\"mean\":104.5},{\"day\":\"2026-02-08\",\"sys\":129,\"dia\":81,\"mean\":105},{\"day\":\"2026-02-06\",\"sys\":129,\"dia\":80,\"mean\":104.5},{\"day\":\"2026-02-05\",\"sys\":129,\"dia\":85,\"mean\":107},{\"day\":\"2026-02-03\",\"sys\":128,\"dia\":84,\"mean\":106},{\"day\":\"2026-02-02\",\"sys\":134,\"dia\":83,\"mean\":108.5},{\"day\":\"2026-01-31\",\"sys\":132,\"dia\":86,\"mean\":109},{\"day\":\"2026-01-30\",\"sys\":130,\"dia\":85,\"mean\":107.5},{\"day\":\"2026-01-28\",\"sys\":127,\"dia\":81,\"mean\":104},{\"day\":\"2026-01-27\",\"sys\":125,\"dia\":80,\"mean\":102.5}]},\"weightVsSystolic\":{\"correlation\":{\"r\":0.385,\"strength\":\"schwach\",\"n\":47},\"pairs\":[{\"day\":\"2026-04-24\",\"weight\":183.2,\"systolic\":120},{\"day\":\"2026-04-22\",\"weight\":183,\"systolic\":117},{\"day\":\"2026-04-18\",\"weight\":183,\"systolic\":121},{\"day\":\"2026-04-16\",\"weight\":183.2,\"systolic\":121},{\"day\":\"2026-04-12\",\"weight\":183.7,\"systolic\":128},{\"day\":\"2026-04-10\",\"weight\":184,\"systolic\":129},{\"day\":\"2026-04-06\",\"weight\":184.2,\"systolic\":134},{\"day\":\"2026-04-04\",\"weight\":184.1,\"systolic\":132},{\"day\":\"2026-03-31\",\"weight\":183.5,\"systolic\":125},{\"day\":\"2026-03-29\",\"weight\":183.1,\"systolic\":128},{\"day\":\"2026-03-25\",\"weight\":182.3,\"systolic\":119},{\"day\":\"2026-03-23\",\"weight\":182.1,\"systolic\":123},{\"day\":\"2026-03-19\",\"weight\":181.9,\"systolic\":118},{\"day\":\"2026-03-17\",\"weight\":182,\"systolic\":117},{\"day\":\"2026-03-13\",\"weight\":182.5,\"systolic\":125},{\"day\":\"2026-03-11\",\"weight\":182.8,\"systolic\":125},{\"day\":\"2026-03-07\",\"weight\":183.1,\"systolic\":131},{\"day\":\"2026-03-05\",\"weight\":183.1,\"systolic\":130},{\"day\":\"2026-03-01\",\"weight\":182.7,\"systolic\":131},{\"day\":\"2026-02-27\",\"weight\":182.4,\"systolic\":127},{\"day\":\"2026-02-23\",\"weight\":181.6,\"systolic\":126},{\"day\":\"2026-02-21\",\"weight\":181.2,\"systolic\":122},{\"day\":\"2026-02-17\",\"weight\":180.8,\"systolic\":116},{\"day\":\"2026-02-15\",\"weight\":180.9,\"systolic\":122},{\"day\":\"2026-02-11\",\"weight\":181.2,\"systolic\":121},{\"day\":\"2026-02-09\",\"weight\":181.5,\"systolic\":128},{\"day\":\"2026-02-05\",\"weight\":182,\"systolic\":129},{\"day\":\"2026-02-03\",\"weight\":182.1,\"systolic\":128},{\"day\":\"2026-01-30\",\"weight\":181.9,\"systolic\":130},{\"day\":\"2026-01-28\",\"weight\":181.6,\"systolic\":127}]},\"weightVsMeanBloodPressure\":{\"correlation\":{\"r\":0.409,\"strength\":\"moderat\",\"n\":47},\"pairs\":[{\"day\":\"2026-04-24\",\"weight\":183.2,\"meanBloodPressure\":98.5},{\"day\":\"2026-04-22\",\"weight\":183,\"meanBloodPressure\":95.5},{\"day\":\"2026-04-18\",\"weight\":183,\"meanBloodPressure\":98.5},{\"day\":\"2026-04-16\",\"weight\":183.2,\"meanBloodPressure\":100.5},{\"day\":\"2026-04-12\",\"weight\":183.7,\"meanBloodPressure\":103.5},{\"day\":\"2026-04-10\",\"weight\":184,\"meanBloodPressure\":106},{\"day\":\"2026-04-06\",\"weight\":184.2,\"meanBloodPressure\":110},{\"day\":\"2026-04-04\",\"weight\":184.1,\"meanBloodPressure\":108},{\"day\":\"2026-03-31\",\"weight\":183.5,\"meanBloodPressure\":104},{\"day\":\"2026-03-29\",\"weight\":183.1,\"meanBloodPressure\":103.5},{\"day\":\"2026-03-25\",\"weight\":182.3,\"meanBloodPressure\":98},{\"day\":\"2026-03-23\",\"weight\":182.1,\"meanBloodPressure\":98.5},{\"day\":\"2026-03-19\",\"weight\":181.9,\"meanBloodPressure\":96.5},{\"day\":\"2026-03-17\",\"weight\":182,\"meanBloodPressure\":98},{\"day\":\"2026-03-13\",\"weight\":182.5,\"meanBloodPressure\":101.5},{\"day\":\"2026-03-11\",\"weight\":182.8,\"meanBloodPressure\":103.5},{\"day\":\"2026-03-07\",\"weight\":183.1,\"meanBloodPressure\":108.5},{\"day\":\"2026-03-05\",\"weight\":183.1,\"meanBloodPressure\":107},{\"day\":\"2026-03-01\",\"weight\":182.7,\"meanBloodPressure\":107.5},{\"day\":\"2026-02-27\",\"weight\":182.4,\"meanBloodPressure\":104},{\"day\":\"2026-02-23\",\"weight\":181.6,\"meanBloodPressure\":102},{\"day\":\"2026-02-21\",\"weight\":181.2,\"meanBloodPressure\":98.5},{\"day\":\"2026-02-17\",\"weight\":180.8,\"meanBloodPressure\":95.5},{\"day\":\"2026-02-15\",\"weight\":180.9,\"meanBloodPressure\":100},{\"day\":\"2026-02-11\",\"weight\":181.2,\"meanBloodPressure\":99},{\"day\":\"2026-02-09\",\"weight\":181.5,\"meanBloodPressure\":104.5},{\"day\":\"2026-02-05\",\"weight\":182,\"meanBloodPressure\":107},{\"day\":\"2026-02-03\",\"weight\":182.1,\"meanBloodPressure\":106},{\"day\":\"2026-01-30\",\"weight\":181.9,\"meanBloodPressure\":107.5},{\"day\":\"2026-01-28\",\"weight\":181.6,\"meanBloodPressure\":104}]},\"moodContext\":{\"points\":70,\"mean\":3.03,\"latest\":1,\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-15\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W15\",\"min\":2,\"max\":5,\"mean\":3.33,\"n\":6},{\"weekISO\":\"2026-W16\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":1,\"max\":5,\"mean\":3,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":1,\"max\":4,\"mean\":2.57,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1}],\"monthly\":[],\"yearly\":[]},\"moodVsWeightCorrelation\":{\"r\":0.523,\"strength\":\"moderat\",\"n\":35}}}" + ] + } + ], + "temperature": 0.45, + "maxTokens": 250, + "seed": 1618033, + "responseFormat": "json", + "timeoutMs": 60000 + } + ], + "enqueued": [], + "result": { + "hasProvider": true, + "text": "Das ist kein JSON.", + "cached": false, + "updatedAt": "2026-06-15T10:00:00.000Z" + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "weight", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": "4cc01c285b2f92494bb578350040930a348bdbd86863a98fd23c3dec38680143", + "snapshotHash": "47cecabde3126e95e9cd479b570e5644198b6882831fffb4d234b2a582a98df6", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "weight", + "locale": "en", + "text": "Parity note for fresh-en.", + "items": null, + "inputHash": "4cc01c285b2f92494bb578350040930a348bdbd86863a98fd23c3dec38680143", + "snapshotHash": "773348667d5c62724cc6803444fe5518c074e3f54eeb3d69a764b26c33e38255", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + } + ] + }, + "unparseable-fr": { + "calls": [ + { + "system": [ + "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", + "", + "YOUR DATA (graded snapshot):", + "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", + "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", + "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", + "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", + "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", + "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", + "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", + "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", + "", + "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", + "Moves available (choose the few that genuinely apply):", + "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", + "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", + "(c) name a genuine, earned win or a steady streak when the data shows one;", + "(d) one watch-item worth keeping an eye on;", + "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", + "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", + "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", + "", + "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", + "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", + "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", + "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", + "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", + "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", + "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", + "", + "OPENING — lead with meaning, not the metric value", + "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", + "", + "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", + "", + "JUDGE THE DATA HONESTLY (never invent a trend):", + "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", + "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", + "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", + "", + "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", + "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", + "", + "METRIC IDENTIFIERS STAY OUT OF PROSE", + "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", + "", + "GLP-1 DOSE SAFETY (contract, not style)", + "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", + "", + "ACUTE RED FLAGS (contract, not style)", + "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", + "", + "GROUNDING — ZERO HALLUCINATIONS", + "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", + "", + "TONE — a warm, motivating advisor, never clinical or alarming", + "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", + "", + "NOT AN INSIGHT — never recite mechanics", + "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", + "", + "INTERPRET, DON'T ENUMERATE", + "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", + "", + "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", + "", + "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", + "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", + "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", + "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", + "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", + "", + "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in French: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", + "{ \"summary\": \"...\" }", + "", + "METRIC — WEIGHT:", + "- The snapshot carries weight.signal (the finished comparison) + weight.summary + weight.series (graded). weight.latestDayFocus shows the latest daily value, the step from the previous measured day and, where present, the same-day blood pressure.", + "- This card's focus is the continuous TREND and PACE over time (pace per week, plateaus, milestones), not WHO-band placement — the BMI card carries that. Mention the BMI value only as a side reference, never make the band the central message here.", + "- The weekly/monthly trend matters, not the single value: lead from weight.signal — current, delta vs baselineLabel and outsideNormalSwing are already computed. STATE them, do NOT recompute. Day-to-day swings of 2.2 lb-4.4 lb (water, time of day) are normal and not a finding.", + "- Own target: when the snapshot carries weight.target, that is the target band the person set themselves, and weight.target.progress says which way is progress (gaining, losing, or holding steady). Judge every change AGAINST that target: below it, a gain is progress and never a setback or a regression. The loss pace and milestone guidance below applies only when losing is the progress direction or no target is set.", + "- Plateau: if the mean moves < 1.1 lb over > ~2 weeks, that is a plateau — name it honestly for someone who is losing, without discouraging.", + "- Pace: sustainable loss is ~1.1 lb-2.2 lb per week (DGE). Sustained > 2.2 lb per week is more likely water/muscle than fat — frame cautiously.", + "- Milestones against their own maximum: ~5% loss from the highest value (series max) already brings metabolic benefit, ~10% a substantial one — acknowledge explicitly when reached.", + "- BMI reference only as a brief aside when height is in the profile (do not claim it otherwise); the WHO-band placement itself belongs on the BMI card, not here.", + "- Associations: mention weightVsSystolic.correlation / weightVsMeanBloodPressure.correlation and moodContext.moodVsWeightCorrelation only when present and |r| > 0.4 — as an association, never a cause.", + "- One message: close with ONE doable step that fits the direction ONLY when the finding implies one (e.g. on a plateau, weigh at the same time of day to see the real trend rather than the daily noise). When the trend is steady and there is nothing useful to do, affirm it honestly and name one thing worth keeping an eye on instead of manufacturing a step.", + "", + "OUTPUT LANGUAGE: Répondez en français. Utilisez le vouvoiement par défaut ; tutoyez si l'utilisateur tutoie en premier. Utilisez un vocabulaire de santé français naturel." + ], + "messages": [ + { + "role": "user", + "content": [ + "Date: 2026-06-15", + "OPENER HINT: Open by acknowledging steadiness or a streak, then the current value.", + "Write one short assessment of this person's weight. Open with what the trend MEANS in plain words — the direction and momentum, not the number (e.g. \"easing down steadily\", \"holding right where it's settled\") — then bring in ONE concrete number from the snapshot right after as support, read as a continuous trend and pace against their own weekly/monthly baseline (pace per week, plateau, milestone — not the single value, and not the WHO band, which the BMI card covers); never lead with the value. Close with one doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the measurement count and recency.", + "", + "PREVIOUS ANALYSIS: none on file. Treat this as the user's first analysis for this domain — no improvement/regression delta to surface.", + "", + "", + "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der Konstanz bzw. Streuung der Werte eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", + "", + "DATENLAGE: 70 Tages-Buckets im Fenster, letzter Wert heute.", + "", + "WIEDERHOLUNG: du hast für diese Metrik bereits 10-mal in Folge eine ähnliche \"keine wesentliche Änderung\"-Einschätzung gegeben. Wiederhole NICHT erneut dasselbe Niveau. Benenne die Kontinuität in einem kurzen Nebensatz (\"weiterhin stabil, 11 Checks in Folge\") und wende dich dann einem ANDEREN Aspekt zu — Konstanz, Tageszeit-Muster oder einer korrelierten Metrik — oder sage, falls nichts wirklich umsetzbar ist, dies klar und lass den erzwungenen Schritt weg.", + "", + "ZUSAMMENHÄNGE (statistisch geprüfte Assoziationen in DEINEN Daten, die diese Metrik betreffen — beschreibend, NIE kausal):", + "- Quand la pression systolique monte, le poids du lendemain semble avoir tendance à augmenter, au vu des données actuelles. C'est une tendance à surveiller, pas une cause. (n=47, r=0.88)", + "- Quand la durée de sommeil monte, le poids du lendemain semble avoir tendance à augmenter, au vu des données actuelles. C'est une tendance à surveiller, pas une cause. (n=45, r=0.98)", + "Du DARFST höchstens EINEN davon einflechten, wenn er echten Mehrwert bringt — als \"Zusammenhang\" / \"geht tendenziell einher mit\" formuliert, nie als Ursache, und ohne die Zahlen zu verändern. Lass sie weg, wenn der eigene Befund der Metrik die stärkere Geschichte ist.", + "", + "", + "{\"locale\":\"fr\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"weight\",\"dataCoverage\":{\"totalMeasurements\":258,\"totalSpanDays\":139,\"newestMeasurementDaysAgo\":0},\"weight\":{\"unit\":\"lb\",\"signal\":{\"metric\":\"dein Gewicht\",\"unit\":\"lb\",\"current\":185.58,\"currentWindowDays\":11,\"baseline\":182.07,\"baselineLabel\":\"your monthly average\",\"delta\":3.52,\"deltaPct\":1.9,\"spread\":0.64,\"outsideNormalSwing\":true,\"direction\":\"target-band\",\"n\":11,\"newestDaysAgo\":0},\"summary\":{\"points\":70,\"start\":185.6,\"end\":181.6,\"delta\":-4,\"mean\":183.4,\"min\":180.8,\"max\":186.2},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":184.3,\"max\":184.3,\"mean\":184.3,\"n\":1},{\"date\":\"2026-05-28\",\"min\":184.6,\"max\":184.6,\"mean\":184.6,\"n\":1},{\"date\":\"2026-05-30\",\"min\":185.1,\"max\":185.1,\"mean\":185.1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":185.5,\"max\":185.5,\"mean\":185.5,\"n\":1},{\"date\":\"2026-06-03\",\"min\":185.8,\"max\":185.8,\"mean\":185.8,\"n\":1},{\"date\":\"2026-06-05\",\"min\":186.1,\"max\":186.1,\"mean\":186.1,\"n\":1},{\"date\":\"2026-06-07\",\"min\":186.2,\"max\":186.2,\"mean\":186.2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":186.2,\"max\":186.2,\"mean\":186.2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":186.1,\"max\":186.1,\"mean\":186.1,\"n\":1},{\"date\":\"2026-06-13\",\"min\":185.9,\"max\":185.9,\"mean\":185.9,\"n\":1},{\"date\":\"2026-06-15\",\"min\":185.6,\"max\":185.6,\"mean\":185.6,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":181.9,\"max\":182,\"mean\":181.9,\"n\":3},{\"weekISO\":\"2026-W13\",\"min\":182.1,\"max\":183.1,\"mean\":182.6,\"n\":4},{\"weekISO\":\"2026-W14\",\"min\":183.5,\"max\":184.1,\"mean\":183.8,\"n\":3},{\"weekISO\":\"2026-W15\",\"min\":183.7,\"max\":184.2,\"mean\":184,\"n\":4},{\"weekISO\":\"2026-W16\",\"min\":183,\"max\":183.4,\"mean\":183.2,\"n\":3},{\"weekISO\":\"2026-W17\",\"min\":182.9,\"max\":183.5,\"mean\":183.1,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":183.9,\"max\":184.7,\"mean\":184.3,\"n\":3},{\"weekISO\":\"2026-W19\",\"min\":185,\"max\":185.2,\"mean\":185.1,\"n\":4},{\"weekISO\":\"2026-W20\",\"min\":184.4,\"max\":184.9,\"mean\":184.7,\"n\":3},{\"weekISO\":\"2026-W21\",\"min\":184,\"max\":184.2,\"mean\":184,\"n\":4}],\"monthly\":[{\"month\":\"2026-01\",\"min\":181.6,\"max\":181.9,\"mean\":181.8,\"n\":2},{\"month\":\"2026-02\",\"min\":180.8,\"max\":182.4,\"mean\":181.6,\"n\":14},{\"month\":\"2026-03\",\"min\":182.2,\"max\":183.1,\"mean\":182.8,\"n\":8}],\"yearly\":[]},\"latestDayFocus\":{\"day\":\"2026-06-15\",\"dayOffset\":0,\"value\":185.6,\"deltaToPreviousDailyPoint\":-0.3,\"sameDayBloodPressure\":{\"day\":\"2026-06-15\",\"sys\":128,\"dia\":82,\"mean\":105}}},\"bloodPressureContext\":{\"systolic\":{\"summary\":{\"points\":94,\"start\":128,\"end\":125,\"delta\":-3,\"mean\":125.39,\"min\":116,\"max\":134},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":119,\"max\":119,\"mean\":119,\"n\":1},{\"date\":\"2026-05-28\",\"min\":121,\"max\":121,\"mean\":121,\"n\":1},{\"date\":\"2026-05-30\",\"min\":126,\"max\":126,\"mean\":126,\"n\":1},{\"date\":\"2026-05-31\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-02\",\"min\":125,\"max\":125,\"mean\":125,\"n\":1},{\"date\":\"2026-06-03\",\"min\":127,\"max\":127,\"mean\":127,\"n\":1},{\"date\":\"2026-06-05\",\"min\":130,\"max\":130,\"mean\":130,\"n\":1},{\"date\":\"2026-06-06\",\"min\":132,\"max\":132,\"mean\":132,\"n\":1},{\"date\":\"2026-06-08\",\"min\":134,\"max\":134,\"mean\":134,\"n\":1},{\"date\":\"2026-06-09\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-11\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-12\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-14\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-15\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":117,\"max\":121,\"mean\":118.75,\"n\":4},{\"weekISO\":\"2026-W13\",\"min\":119,\"max\":128,\"mean\":123.2,\"n\":5},{\"weekISO\":\"2026-W14\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"weekISO\":\"2026-W15\",\"min\":128,\"max\":134,\"mean\":129.4,\"n\":5},{\"weekISO\":\"2026-W16\",\"min\":121,\"max\":128,\"mean\":122.4,\"n\":5},{\"weekISO\":\"2026-W17\",\"min\":116,\"max\":122,\"mean\":118.75,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":121,\"max\":131,\"mean\":126,\"n\":5},{\"weekISO\":\"2026-W19\",\"min\":129,\"max\":133,\"mean\":131,\"n\":5},{\"weekISO\":\"2026-W20\",\"min\":124,\"max\":125,\"mean\":124.75,\"n\":4},{\"weekISO\":\"2026-W21\",\"min\":117,\"max\":124,\"mean\":119.8,\"n\":5},{\"weekISO\":\"2026-W22\",\"min\":123,\"max\":123,\"mean\":123,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"month\":\"2026-02\",\"min\":116,\"max\":134,\"mean\":124.22,\"n\":18},{\"month\":\"2026-03\",\"min\":124,\"max\":133,\"mean\":128.18,\"n\":11}],\"yearly\":[]}},\"diastolic\":{\"summary\":{\"points\":94,\"start\":82,\"end\":80,\"delta\":-2,\"mean\":80.31,\"min\":74,\"max\":86},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":76,\"max\":76,\"mean\":76,\"n\":1},{\"date\":\"2026-05-28\",\"min\":77,\"max\":77,\"mean\":77,\"n\":1},{\"date\":\"2026-05-30\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-05-31\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-02\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-03\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-05\",\"min\":86,\"max\":86,\"mean\":86,\"n\":1},{\"date\":\"2026-06-06\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-08\",\"min\":84,\"max\":84,\"mean\":84,\"n\":1},{\"date\":\"2026-06-09\",\"min\":85,\"max\":85,\"mean\":85,\"n\":1},{\"date\":\"2026-06-11\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-12\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-14\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-15\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":75,\"max\":79,\"mean\":77,\"n\":4},{\"weekISO\":\"2026-W13\",\"min\":74,\"max\":79,\"mean\":77.4,\"n\":5},{\"weekISO\":\"2026-W14\",\"min\":83,\"max\":85,\"mean\":83.75,\"n\":4},{\"weekISO\":\"2026-W15\",\"min\":79,\"max\":86,\"mean\":82.6,\"n\":5},{\"weekISO\":\"2026-W16\",\"min\":76,\"max\":80,\"mean\":78.2,\"n\":5},{\"weekISO\":\"2026-W17\",\"min\":74,\"max\":78,\"mean\":76.75,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":77,\"max\":84,\"mean\":80.6,\"n\":5},{\"weekISO\":\"2026-W19\",\"min\":82,\"max\":86,\"mean\":83.8,\"n\":5},{\"weekISO\":\"2026-W20\",\"min\":80,\"max\":81,\"mean\":80.5,\"n\":4},{\"weekISO\":\"2026-W21\",\"min\":74,\"max\":78,\"mean\":76.2,\"n\":5},{\"weekISO\":\"2026-W22\",\"min\":78,\"max\":78,\"mean\":78,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":80,\"max\":86,\"mean\":83,\"n\":4},{\"month\":\"2026-02\",\"min\":75,\"max\":85,\"mean\":79.28,\"n\":18},{\"month\":\"2026-03\",\"min\":78,\"max\":86,\"mean\":82,\"n\":11}],\"yearly\":[]}},\"pairedDaily\":[{\"day\":\"2026-03-11\",\"sys\":125,\"dia\":82,\"mean\":103.5},{\"day\":\"2026-03-10\",\"sys\":125,\"dia\":82,\"mean\":103.5},{\"day\":\"2026-03-08\",\"sys\":132,\"dia\":81,\"mean\":106.5},{\"day\":\"2026-03-07\",\"sys\":131,\"dia\":86,\"mean\":108.5},{\"day\":\"2026-03-05\",\"sys\":130,\"dia\":84,\"mean\":107},{\"day\":\"2026-03-04\",\"sys\":129,\"dia\":83,\"mean\":106},{\"day\":\"2026-03-02\",\"sys\":133,\"dia\":85,\"mean\":109},{\"day\":\"2026-03-01\",\"sys\":131,\"dia\":84,\"mean\":107.5},{\"day\":\"2026-02-27\",\"sys\":127,\"dia\":81,\"mean\":104},{\"day\":\"2026-02-26\",\"sys\":125,\"dia\":79,\"mean\":102},{\"day\":\"2026-02-24\",\"sys\":121,\"dia\":80,\"mean\":100.5},{\"day\":\"2026-02-23\",\"sys\":126,\"dia\":78,\"mean\":102},{\"day\":\"2026-02-21\",\"sys\":122,\"dia\":75,\"mean\":98.5},{\"day\":\"2026-02-20\",\"sys\":120,\"dia\":79,\"mean\":99.5},{\"day\":\"2026-02-18\",\"sys\":117,\"dia\":76,\"mean\":96.5},{\"day\":\"2026-02-17\",\"sys\":116,\"dia\":75,\"mean\":95.5},{\"day\":\"2026-02-15\",\"sys\":122,\"dia\":78,\"mean\":100},{\"day\":\"2026-02-14\",\"sys\":121,\"dia\":78,\"mean\":99.5},{\"day\":\"2026-02-12\",\"sys\":121,\"dia\":77,\"mean\":99},{\"day\":\"2026-02-11\",\"sys\":121,\"dia\":77,\"mean\":99},{\"day\":\"2026-02-09\",\"sys\":128,\"dia\":81,\"mean\":104.5},{\"day\":\"2026-02-08\",\"sys\":129,\"dia\":81,\"mean\":105},{\"day\":\"2026-02-06\",\"sys\":129,\"dia\":80,\"mean\":104.5},{\"day\":\"2026-02-05\",\"sys\":129,\"dia\":85,\"mean\":107},{\"day\":\"2026-02-03\",\"sys\":128,\"dia\":84,\"mean\":106},{\"day\":\"2026-02-02\",\"sys\":134,\"dia\":83,\"mean\":108.5},{\"day\":\"2026-01-31\",\"sys\":132,\"dia\":86,\"mean\":109},{\"day\":\"2026-01-30\",\"sys\":130,\"dia\":85,\"mean\":107.5},{\"day\":\"2026-01-28\",\"sys\":127,\"dia\":81,\"mean\":104},{\"day\":\"2026-01-27\",\"sys\":125,\"dia\":80,\"mean\":102.5}]},\"weightVsSystolic\":{\"correlation\":{\"r\":0.385,\"strength\":\"schwach\",\"n\":47},\"pairs\":[{\"day\":\"2026-04-24\",\"weight\":183.2,\"systolic\":120},{\"day\":\"2026-04-22\",\"weight\":183,\"systolic\":117},{\"day\":\"2026-04-18\",\"weight\":183,\"systolic\":121},{\"day\":\"2026-04-16\",\"weight\":183.2,\"systolic\":121},{\"day\":\"2026-04-12\",\"weight\":183.7,\"systolic\":128},{\"day\":\"2026-04-10\",\"weight\":184,\"systolic\":129},{\"day\":\"2026-04-06\",\"weight\":184.2,\"systolic\":134},{\"day\":\"2026-04-04\",\"weight\":184.1,\"systolic\":132},{\"day\":\"2026-03-31\",\"weight\":183.5,\"systolic\":125},{\"day\":\"2026-03-29\",\"weight\":183.1,\"systolic\":128},{\"day\":\"2026-03-25\",\"weight\":182.3,\"systolic\":119},{\"day\":\"2026-03-23\",\"weight\":182.1,\"systolic\":123},{\"day\":\"2026-03-19\",\"weight\":181.9,\"systolic\":118},{\"day\":\"2026-03-17\",\"weight\":182,\"systolic\":117},{\"day\":\"2026-03-13\",\"weight\":182.5,\"systolic\":125},{\"day\":\"2026-03-11\",\"weight\":182.8,\"systolic\":125},{\"day\":\"2026-03-07\",\"weight\":183.1,\"systolic\":131},{\"day\":\"2026-03-05\",\"weight\":183.1,\"systolic\":130},{\"day\":\"2026-03-01\",\"weight\":182.7,\"systolic\":131},{\"day\":\"2026-02-27\",\"weight\":182.4,\"systolic\":127},{\"day\":\"2026-02-23\",\"weight\":181.6,\"systolic\":126},{\"day\":\"2026-02-21\",\"weight\":181.2,\"systolic\":122},{\"day\":\"2026-02-17\",\"weight\":180.8,\"systolic\":116},{\"day\":\"2026-02-15\",\"weight\":180.9,\"systolic\":122},{\"day\":\"2026-02-11\",\"weight\":181.2,\"systolic\":121},{\"day\":\"2026-02-09\",\"weight\":181.5,\"systolic\":128},{\"day\":\"2026-02-05\",\"weight\":182,\"systolic\":129},{\"day\":\"2026-02-03\",\"weight\":182.1,\"systolic\":128},{\"day\":\"2026-01-30\",\"weight\":181.9,\"systolic\":130},{\"day\":\"2026-01-28\",\"weight\":181.6,\"systolic\":127}]},\"weightVsMeanBloodPressure\":{\"correlation\":{\"r\":0.409,\"strength\":\"moderat\",\"n\":47},\"pairs\":[{\"day\":\"2026-04-24\",\"weight\":183.2,\"meanBloodPressure\":98.5},{\"day\":\"2026-04-22\",\"weight\":183,\"meanBloodPressure\":95.5},{\"day\":\"2026-04-18\",\"weight\":183,\"meanBloodPressure\":98.5},{\"day\":\"2026-04-16\",\"weight\":183.2,\"meanBloodPressure\":100.5},{\"day\":\"2026-04-12\",\"weight\":183.7,\"meanBloodPressure\":103.5},{\"day\":\"2026-04-10\",\"weight\":184,\"meanBloodPressure\":106},{\"day\":\"2026-04-06\",\"weight\":184.2,\"meanBloodPressure\":110},{\"day\":\"2026-04-04\",\"weight\":184.1,\"meanBloodPressure\":108},{\"day\":\"2026-03-31\",\"weight\":183.5,\"meanBloodPressure\":104},{\"day\":\"2026-03-29\",\"weight\":183.1,\"meanBloodPressure\":103.5},{\"day\":\"2026-03-25\",\"weight\":182.3,\"meanBloodPressure\":98},{\"day\":\"2026-03-23\",\"weight\":182.1,\"meanBloodPressure\":98.5},{\"day\":\"2026-03-19\",\"weight\":181.9,\"meanBloodPressure\":96.5},{\"day\":\"2026-03-17\",\"weight\":182,\"meanBloodPressure\":98},{\"day\":\"2026-03-13\",\"weight\":182.5,\"meanBloodPressure\":101.5},{\"day\":\"2026-03-11\",\"weight\":182.8,\"meanBloodPressure\":103.5},{\"day\":\"2026-03-07\",\"weight\":183.1,\"meanBloodPressure\":108.5},{\"day\":\"2026-03-05\",\"weight\":183.1,\"meanBloodPressure\":107},{\"day\":\"2026-03-01\",\"weight\":182.7,\"meanBloodPressure\":107.5},{\"day\":\"2026-02-27\",\"weight\":182.4,\"meanBloodPressure\":104},{\"day\":\"2026-02-23\",\"weight\":181.6,\"meanBloodPressure\":102},{\"day\":\"2026-02-21\",\"weight\":181.2,\"meanBloodPressure\":98.5},{\"day\":\"2026-02-17\",\"weight\":180.8,\"meanBloodPressure\":95.5},{\"day\":\"2026-02-15\",\"weight\":180.9,\"meanBloodPressure\":100},{\"day\":\"2026-02-11\",\"weight\":181.2,\"meanBloodPressure\":99},{\"day\":\"2026-02-09\",\"weight\":181.5,\"meanBloodPressure\":104.5},{\"day\":\"2026-02-05\",\"weight\":182,\"meanBloodPressure\":107},{\"day\":\"2026-02-03\",\"weight\":182.1,\"meanBloodPressure\":106},{\"day\":\"2026-01-30\",\"weight\":181.9,\"meanBloodPressure\":107.5},{\"day\":\"2026-01-28\",\"weight\":181.6,\"meanBloodPressure\":104}]},\"moodContext\":{\"points\":70,\"mean\":3.03,\"latest\":1,\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-15\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W15\",\"min\":2,\"max\":5,\"mean\":3.33,\"n\":6},{\"weekISO\":\"2026-W16\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":1,\"max\":5,\"mean\":3,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":1,\"max\":4,\"mean\":2.57,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1}],\"monthly\":[],\"yearly\":[]},\"moodVsWeightCorrelation\":{\"r\":0.523,\"strength\":\"moderat\",\"n\":35}}}" + ] + } + ], + "temperature": 0.45, + "maxTokens": 250, + "seed": 1618033, + "responseFormat": "json", + "timeoutMs": 60000 + } + ], + "enqueued": [], + "result": { + "hasProvider": false, + "text": "Un peu en dehors de tes repères habituels ces derniers temps. Ton poids est à 185,6 en ce moment — au-dessus de ta moyenne habituelle de 182,1. Pèse-toi à la même heure pendant quelques jours et la fluctuation quotidienne normale se lisse d’elle-même.", + "cached": true, + "updatedAt": null + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "weight", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": "4cc01c285b2f92494bb578350040930a348bdbd86863a98fd23c3dec38680143", + "snapshotHash": "47cecabde3126e95e9cd479b570e5644198b6882831fffb4d234b2a582a98df6", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "weight", + "locale": "en", + "text": "Parity note for fresh-en.", + "items": null, + "inputHash": "4cc01c285b2f92494bb578350040930a348bdbd86863a98fd23c3dec38680143", + "snapshotHash": "773348667d5c62724cc6803444fe5518c074e3f54eeb3d69a764b26c33e38255", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "weight", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + } + ] + }, + "blocked-es": { + "calls": [ + { + "system": [ + "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", + "", + "YOUR DATA (graded snapshot):", + "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", + "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", + "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", + "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", + "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", + "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", + "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", + "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", + "", + "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", + "Moves available (choose the few that genuinely apply):", + "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", + "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", + "(c) name a genuine, earned win or a steady streak when the data shows one;", + "(d) one watch-item worth keeping an eye on;", + "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", + "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", + "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", + "", + "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", + "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", + "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", + "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", + "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", + "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", + "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", + "", + "OPENING — lead with meaning, not the metric value", + "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", + "", + "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", + "", + "JUDGE THE DATA HONESTLY (never invent a trend):", + "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", + "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", + "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", + "", + "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", + "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", + "", + "METRIC IDENTIFIERS STAY OUT OF PROSE", + "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", + "", + "GLP-1 DOSE SAFETY (contract, not style)", + "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", + "", + "ACUTE RED FLAGS (contract, not style)", + "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", + "", + "GROUNDING — ZERO HALLUCINATIONS", + "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", + "", + "TONE — a warm, motivating advisor, never clinical or alarming", + "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", + "", + "NOT AN INSIGHT — never recite mechanics", + "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", + "", + "INTERPRET, DON'T ENUMERATE", + "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", + "", + "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", + "", + "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", + "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", + "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", + "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", + "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", + "", + "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in Spanish: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", + "{ \"summary\": \"...\" }", + "", + "METRIC — WEIGHT:", + "- The snapshot carries weight.signal (the finished comparison) + weight.summary + weight.series (graded). weight.latestDayFocus shows the latest daily value, the step from the previous measured day and, where present, the same-day blood pressure.", + "- This card's focus is the continuous TREND and PACE over time (pace per week, plateaus, milestones), not WHO-band placement — the BMI card carries that. Mention the BMI value only as a side reference, never make the band the central message here.", + "- The weekly/monthly trend matters, not the single value: lead from weight.signal — current, delta vs baselineLabel and outsideNormalSwing are already computed. STATE them, do NOT recompute. Day-to-day swings of 2.2 lb-4.4 lb (water, time of day) are normal and not a finding.", + "- Own target: when the snapshot carries weight.target, that is the target band the person set themselves, and weight.target.progress says which way is progress (gaining, losing, or holding steady). Judge every change AGAINST that target: below it, a gain is progress and never a setback or a regression. The loss pace and milestone guidance below applies only when losing is the progress direction or no target is set.", + "- Plateau: if the mean moves < 1.1 lb over > ~2 weeks, that is a plateau — name it honestly for someone who is losing, without discouraging.", + "- Pace: sustainable loss is ~1.1 lb-2.2 lb per week (DGE). Sustained > 2.2 lb per week is more likely water/muscle than fat — frame cautiously.", + "- Milestones against their own maximum: ~5% loss from the highest value (series max) already brings metabolic benefit, ~10% a substantial one — acknowledge explicitly when reached.", + "- BMI reference only as a brief aside when height is in the profile (do not claim it otherwise); the WHO-band placement itself belongs on the BMI card, not here.", + "- Associations: mention weightVsSystolic.correlation / weightVsMeanBloodPressure.correlation and moodContext.moodVsWeightCorrelation only when present and |r| > 0.4 — as an association, never a cause.", + "- One message: close with ONE doable step that fits the direction ONLY when the finding implies one (e.g. on a plateau, weigh at the same time of day to see the real trend rather than the daily noise). When the trend is steady and there is nothing useful to do, affirm it honestly and name one thing worth keeping an eye on instead of manufacturing a step.", + "", + "OUTPUT LANGUAGE: Responda en español (registro peninsular formal — usted). Use un vocabulario sanitario en español natural." + ], + "messages": [ + { + "role": "user", + "content": [ + "Date: 2026-06-15", + "OPENER HINT: Open by acknowledging steadiness or a streak, then the current value.", + "Write one short assessment of this person's weight. Open with what the trend MEANS in plain words — the direction and momentum, not the number (e.g. \"easing down steadily\", \"holding right where it's settled\") — then bring in ONE concrete number from the snapshot right after as support, read as a continuous trend and pace against their own weekly/monthly baseline (pace per week, plateau, milestone — not the single value, and not the WHO band, which the BMI card covers); never lead with the value. Close with one doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the measurement count and recency.", + "", + "PREVIOUS ANALYSIS: none on file. Treat this as the user's first analysis for this domain — no improvement/regression delta to surface.", + "", + "", + "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit der Konstanz bzw. Streuung der Werte eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", + "", + "DATENLAGE: 70 Tages-Buckets im Fenster, letzter Wert heute.", + "", + "WIEDERHOLUNG: du hast für diese Metrik bereits 10-mal in Folge eine ähnliche \"keine wesentliche Änderung\"-Einschätzung gegeben. Wiederhole NICHT erneut dasselbe Niveau. Benenne die Kontinuität in einem kurzen Nebensatz (\"weiterhin stabil, 11 Checks in Folge\") und wende dich dann einem ANDEREN Aspekt zu — Konstanz, Tageszeit-Muster oder einer korrelierten Metrik — oder sage, falls nichts wirklich umsetzbar ist, dies klar und lass den erzwungenen Schritt weg.", + "", + "ZUSAMMENHÄNGE (statistisch geprüfte Assoziationen in DEINEN Daten, die diese Metrik betreffen — beschreibend, NIE kausal):", + "- Cuando la presión sistólica es mayor, el peso del día siguiente parece tender a subir, según los datos disponibles. Es un patrón que conviene observar, no una causa. (n=47, r=0.88)", + "- Cuando la duración del sueño es mayor, el peso del día siguiente parece tender a subir, según los datos disponibles. Es un patrón que conviene observar, no una causa. (n=45, r=0.98)", + "Du DARFST höchstens EINEN davon einflechten, wenn er echten Mehrwert bringt — als \"Zusammenhang\" / \"geht tendenziell einher mit\" formuliert, nie als Ursache, und ohne die Zahlen zu verändern. Lass sie weg, wenn der eigene Befund der Metrik die stärkere Geschichte ist.", + "", + "", + "{\"locale\":\"es\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"weight\",\"dataCoverage\":{\"totalMeasurements\":258,\"totalSpanDays\":139,\"newestMeasurementDaysAgo\":0},\"weight\":{\"unit\":\"lb\",\"signal\":{\"metric\":\"dein Gewicht\",\"unit\":\"lb\",\"current\":185.58,\"currentWindowDays\":11,\"baseline\":182.07,\"baselineLabel\":\"your monthly average\",\"delta\":3.52,\"deltaPct\":1.9,\"spread\":0.64,\"outsideNormalSwing\":true,\"direction\":\"target-band\",\"n\":11,\"newestDaysAgo\":0},\"summary\":{\"points\":70,\"start\":185.6,\"end\":181.6,\"delta\":-4,\"mean\":183.4,\"min\":180.8,\"max\":186.2},\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":184.3,\"max\":184.3,\"mean\":184.3,\"n\":1},{\"date\":\"2026-05-28\",\"min\":184.6,\"max\":184.6,\"mean\":184.6,\"n\":1},{\"date\":\"2026-05-30\",\"min\":185.1,\"max\":185.1,\"mean\":185.1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":185.5,\"max\":185.5,\"mean\":185.5,\"n\":1},{\"date\":\"2026-06-03\",\"min\":185.8,\"max\":185.8,\"mean\":185.8,\"n\":1},{\"date\":\"2026-06-05\",\"min\":186.1,\"max\":186.1,\"mean\":186.1,\"n\":1},{\"date\":\"2026-06-07\",\"min\":186.2,\"max\":186.2,\"mean\":186.2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":186.2,\"max\":186.2,\"mean\":186.2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":186.1,\"max\":186.1,\"mean\":186.1,\"n\":1},{\"date\":\"2026-06-13\",\"min\":185.9,\"max\":185.9,\"mean\":185.9,\"n\":1},{\"date\":\"2026-06-15\",\"min\":185.6,\"max\":185.6,\"mean\":185.6,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":181.9,\"max\":182,\"mean\":181.9,\"n\":3},{\"weekISO\":\"2026-W13\",\"min\":182.1,\"max\":183.1,\"mean\":182.6,\"n\":4},{\"weekISO\":\"2026-W14\",\"min\":183.5,\"max\":184.1,\"mean\":183.8,\"n\":3},{\"weekISO\":\"2026-W15\",\"min\":183.7,\"max\":184.2,\"mean\":184,\"n\":4},{\"weekISO\":\"2026-W16\",\"min\":183,\"max\":183.4,\"mean\":183.2,\"n\":3},{\"weekISO\":\"2026-W17\",\"min\":182.9,\"max\":183.5,\"mean\":183.1,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":183.9,\"max\":184.7,\"mean\":184.3,\"n\":3},{\"weekISO\":\"2026-W19\",\"min\":185,\"max\":185.2,\"mean\":185.1,\"n\":4},{\"weekISO\":\"2026-W20\",\"min\":184.4,\"max\":184.9,\"mean\":184.7,\"n\":3},{\"weekISO\":\"2026-W21\",\"min\":184,\"max\":184.2,\"mean\":184,\"n\":4}],\"monthly\":[{\"month\":\"2026-01\",\"min\":181.6,\"max\":181.9,\"mean\":181.8,\"n\":2},{\"month\":\"2026-02\",\"min\":180.8,\"max\":182.4,\"mean\":181.6,\"n\":14},{\"month\":\"2026-03\",\"min\":182.2,\"max\":183.1,\"mean\":182.8,\"n\":8}],\"yearly\":[]},\"latestDayFocus\":{\"day\":\"2026-06-15\",\"dayOffset\":0,\"value\":185.6,\"deltaToPreviousDailyPoint\":-0.3,\"sameDayBloodPressure\":{\"day\":\"2026-06-15\",\"sys\":128,\"dia\":82,\"mean\":105}}},\"bloodPressureContext\":{\"systolic\":{\"summary\":{\"points\":94,\"start\":128,\"end\":125,\"delta\":-3,\"mean\":125.39,\"min\":116,\"max\":134},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":119,\"max\":119,\"mean\":119,\"n\":1},{\"date\":\"2026-05-28\",\"min\":121,\"max\":121,\"mean\":121,\"n\":1},{\"date\":\"2026-05-30\",\"min\":126,\"max\":126,\"mean\":126,\"n\":1},{\"date\":\"2026-05-31\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-02\",\"min\":125,\"max\":125,\"mean\":125,\"n\":1},{\"date\":\"2026-06-03\",\"min\":127,\"max\":127,\"mean\":127,\"n\":1},{\"date\":\"2026-06-05\",\"min\":130,\"max\":130,\"mean\":130,\"n\":1},{\"date\":\"2026-06-06\",\"min\":132,\"max\":132,\"mean\":132,\"n\":1},{\"date\":\"2026-06-08\",\"min\":134,\"max\":134,\"mean\":134,\"n\":1},{\"date\":\"2026-06-09\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-11\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-12\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-14\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1},{\"date\":\"2026-06-15\",\"min\":128,\"max\":128,\"mean\":128,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":117,\"max\":121,\"mean\":118.75,\"n\":4},{\"weekISO\":\"2026-W13\",\"min\":119,\"max\":128,\"mean\":123.2,\"n\":5},{\"weekISO\":\"2026-W14\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"weekISO\":\"2026-W15\",\"min\":128,\"max\":134,\"mean\":129.4,\"n\":5},{\"weekISO\":\"2026-W16\",\"min\":121,\"max\":128,\"mean\":122.4,\"n\":5},{\"weekISO\":\"2026-W17\",\"min\":116,\"max\":122,\"mean\":118.75,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":121,\"max\":131,\"mean\":126,\"n\":5},{\"weekISO\":\"2026-W19\",\"min\":129,\"max\":133,\"mean\":131,\"n\":5},{\"weekISO\":\"2026-W20\",\"min\":124,\"max\":125,\"mean\":124.75,\"n\":4},{\"weekISO\":\"2026-W21\",\"min\":117,\"max\":124,\"mean\":119.8,\"n\":5},{\"weekISO\":\"2026-W22\",\"min\":123,\"max\":123,\"mean\":123,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":125,\"max\":132,\"mean\":128.5,\"n\":4},{\"month\":\"2026-02\",\"min\":116,\"max\":134,\"mean\":124.22,\"n\":18},{\"month\":\"2026-03\",\"min\":124,\"max\":133,\"mean\":128.18,\"n\":11}],\"yearly\":[]}},\"diastolic\":{\"summary\":{\"points\":94,\"start\":82,\"end\":80,\"delta\":-2,\"mean\":80.31,\"min\":74,\"max\":86},\"series\":{\"recent\":[{\"date\":\"2026-05-27\",\"min\":76,\"max\":76,\"mean\":76,\"n\":1},{\"date\":\"2026-05-28\",\"min\":77,\"max\":77,\"mean\":77,\"n\":1},{\"date\":\"2026-05-30\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-05-31\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-02\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-03\",\"min\":83,\"max\":83,\"mean\":83,\"n\":1},{\"date\":\"2026-06-05\",\"min\":86,\"max\":86,\"mean\":86,\"n\":1},{\"date\":\"2026-06-06\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-08\",\"min\":84,\"max\":84,\"mean\":84,\"n\":1},{\"date\":\"2026-06-09\",\"min\":85,\"max\":85,\"mean\":85,\"n\":1},{\"date\":\"2026-06-11\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-12\",\"min\":81,\"max\":81,\"mean\":81,\"n\":1},{\"date\":\"2026-06-14\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1},{\"date\":\"2026-06-15\",\"min\":82,\"max\":82,\"mean\":82,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W12\",\"min\":75,\"max\":79,\"mean\":77,\"n\":4},{\"weekISO\":\"2026-W13\",\"min\":74,\"max\":79,\"mean\":77.4,\"n\":5},{\"weekISO\":\"2026-W14\",\"min\":83,\"max\":85,\"mean\":83.75,\"n\":4},{\"weekISO\":\"2026-W15\",\"min\":79,\"max\":86,\"mean\":82.6,\"n\":5},{\"weekISO\":\"2026-W16\",\"min\":76,\"max\":80,\"mean\":78.2,\"n\":5},{\"weekISO\":\"2026-W17\",\"min\":74,\"max\":78,\"mean\":76.75,\"n\":4},{\"weekISO\":\"2026-W18\",\"min\":77,\"max\":84,\"mean\":80.6,\"n\":5},{\"weekISO\":\"2026-W19\",\"min\":82,\"max\":86,\"mean\":83.8,\"n\":5},{\"weekISO\":\"2026-W20\",\"min\":80,\"max\":81,\"mean\":80.5,\"n\":4},{\"weekISO\":\"2026-W21\",\"min\":74,\"max\":78,\"mean\":76.2,\"n\":5},{\"weekISO\":\"2026-W22\",\"min\":78,\"max\":78,\"mean\":78,\"n\":1}],\"monthly\":[{\"month\":\"2026-01\",\"min\":80,\"max\":86,\"mean\":83,\"n\":4},{\"month\":\"2026-02\",\"min\":75,\"max\":85,\"mean\":79.28,\"n\":18},{\"month\":\"2026-03\",\"min\":78,\"max\":86,\"mean\":82,\"n\":11}],\"yearly\":[]}},\"pairedDaily\":[{\"day\":\"2026-03-11\",\"sys\":125,\"dia\":82,\"mean\":103.5},{\"day\":\"2026-03-10\",\"sys\":125,\"dia\":82,\"mean\":103.5},{\"day\":\"2026-03-08\",\"sys\":132,\"dia\":81,\"mean\":106.5},{\"day\":\"2026-03-07\",\"sys\":131,\"dia\":86,\"mean\":108.5},{\"day\":\"2026-03-05\",\"sys\":130,\"dia\":84,\"mean\":107},{\"day\":\"2026-03-04\",\"sys\":129,\"dia\":83,\"mean\":106},{\"day\":\"2026-03-02\",\"sys\":133,\"dia\":85,\"mean\":109},{\"day\":\"2026-03-01\",\"sys\":131,\"dia\":84,\"mean\":107.5},{\"day\":\"2026-02-27\",\"sys\":127,\"dia\":81,\"mean\":104},{\"day\":\"2026-02-26\",\"sys\":125,\"dia\":79,\"mean\":102},{\"day\":\"2026-02-24\",\"sys\":121,\"dia\":80,\"mean\":100.5},{\"day\":\"2026-02-23\",\"sys\":126,\"dia\":78,\"mean\":102},{\"day\":\"2026-02-21\",\"sys\":122,\"dia\":75,\"mean\":98.5},{\"day\":\"2026-02-20\",\"sys\":120,\"dia\":79,\"mean\":99.5},{\"day\":\"2026-02-18\",\"sys\":117,\"dia\":76,\"mean\":96.5},{\"day\":\"2026-02-17\",\"sys\":116,\"dia\":75,\"mean\":95.5},{\"day\":\"2026-02-15\",\"sys\":122,\"dia\":78,\"mean\":100},{\"day\":\"2026-02-14\",\"sys\":121,\"dia\":78,\"mean\":99.5},{\"day\":\"2026-02-12\",\"sys\":121,\"dia\":77,\"mean\":99},{\"day\":\"2026-02-11\",\"sys\":121,\"dia\":77,\"mean\":99},{\"day\":\"2026-02-09\",\"sys\":128,\"dia\":81,\"mean\":104.5},{\"day\":\"2026-02-08\",\"sys\":129,\"dia\":81,\"mean\":105},{\"day\":\"2026-02-06\",\"sys\":129,\"dia\":80,\"mean\":104.5},{\"day\":\"2026-02-05\",\"sys\":129,\"dia\":85,\"mean\":107},{\"day\":\"2026-02-03\",\"sys\":128,\"dia\":84,\"mean\":106},{\"day\":\"2026-02-02\",\"sys\":134,\"dia\":83,\"mean\":108.5},{\"day\":\"2026-01-31\",\"sys\":132,\"dia\":86,\"mean\":109},{\"day\":\"2026-01-30\",\"sys\":130,\"dia\":85,\"mean\":107.5},{\"day\":\"2026-01-28\",\"sys\":127,\"dia\":81,\"mean\":104},{\"day\":\"2026-01-27\",\"sys\":125,\"dia\":80,\"mean\":102.5}]},\"weightVsSystolic\":{\"correlation\":{\"r\":0.385,\"strength\":\"schwach\",\"n\":47},\"pairs\":[{\"day\":\"2026-04-24\",\"weight\":183.2,\"systolic\":120},{\"day\":\"2026-04-22\",\"weight\":183,\"systolic\":117},{\"day\":\"2026-04-18\",\"weight\":183,\"systolic\":121},{\"day\":\"2026-04-16\",\"weight\":183.2,\"systolic\":121},{\"day\":\"2026-04-12\",\"weight\":183.7,\"systolic\":128},{\"day\":\"2026-04-10\",\"weight\":184,\"systolic\":129},{\"day\":\"2026-04-06\",\"weight\":184.2,\"systolic\":134},{\"day\":\"2026-04-04\",\"weight\":184.1,\"systolic\":132},{\"day\":\"2026-03-31\",\"weight\":183.5,\"systolic\":125},{\"day\":\"2026-03-29\",\"weight\":183.1,\"systolic\":128},{\"day\":\"2026-03-25\",\"weight\":182.3,\"systolic\":119},{\"day\":\"2026-03-23\",\"weight\":182.1,\"systolic\":123},{\"day\":\"2026-03-19\",\"weight\":181.9,\"systolic\":118},{\"day\":\"2026-03-17\",\"weight\":182,\"systolic\":117},{\"day\":\"2026-03-13\",\"weight\":182.5,\"systolic\":125},{\"day\":\"2026-03-11\",\"weight\":182.8,\"systolic\":125},{\"day\":\"2026-03-07\",\"weight\":183.1,\"systolic\":131},{\"day\":\"2026-03-05\",\"weight\":183.1,\"systolic\":130},{\"day\":\"2026-03-01\",\"weight\":182.7,\"systolic\":131},{\"day\":\"2026-02-27\",\"weight\":182.4,\"systolic\":127},{\"day\":\"2026-02-23\",\"weight\":181.6,\"systolic\":126},{\"day\":\"2026-02-21\",\"weight\":181.2,\"systolic\":122},{\"day\":\"2026-02-17\",\"weight\":180.8,\"systolic\":116},{\"day\":\"2026-02-15\",\"weight\":180.9,\"systolic\":122},{\"day\":\"2026-02-11\",\"weight\":181.2,\"systolic\":121},{\"day\":\"2026-02-09\",\"weight\":181.5,\"systolic\":128},{\"day\":\"2026-02-05\",\"weight\":182,\"systolic\":129},{\"day\":\"2026-02-03\",\"weight\":182.1,\"systolic\":128},{\"day\":\"2026-01-30\",\"weight\":181.9,\"systolic\":130},{\"day\":\"2026-01-28\",\"weight\":181.6,\"systolic\":127}]},\"weightVsMeanBloodPressure\":{\"correlation\":{\"r\":0.409,\"strength\":\"moderat\",\"n\":47},\"pairs\":[{\"day\":\"2026-04-24\",\"weight\":183.2,\"meanBloodPressure\":98.5},{\"day\":\"2026-04-22\",\"weight\":183,\"meanBloodPressure\":95.5},{\"day\":\"2026-04-18\",\"weight\":183,\"meanBloodPressure\":98.5},{\"day\":\"2026-04-16\",\"weight\":183.2,\"meanBloodPressure\":100.5},{\"day\":\"2026-04-12\",\"weight\":183.7,\"meanBloodPressure\":103.5},{\"day\":\"2026-04-10\",\"weight\":184,\"meanBloodPressure\":106},{\"day\":\"2026-04-06\",\"weight\":184.2,\"meanBloodPressure\":110},{\"day\":\"2026-04-04\",\"weight\":184.1,\"meanBloodPressure\":108},{\"day\":\"2026-03-31\",\"weight\":183.5,\"meanBloodPressure\":104},{\"day\":\"2026-03-29\",\"weight\":183.1,\"meanBloodPressure\":103.5},{\"day\":\"2026-03-25\",\"weight\":182.3,\"meanBloodPressure\":98},{\"day\":\"2026-03-23\",\"weight\":182.1,\"meanBloodPressure\":98.5},{\"day\":\"2026-03-19\",\"weight\":181.9,\"meanBloodPressure\":96.5},{\"day\":\"2026-03-17\",\"weight\":182,\"meanBloodPressure\":98},{\"day\":\"2026-03-13\",\"weight\":182.5,\"meanBloodPressure\":101.5},{\"day\":\"2026-03-11\",\"weight\":182.8,\"meanBloodPressure\":103.5},{\"day\":\"2026-03-07\",\"weight\":183.1,\"meanBloodPressure\":108.5},{\"day\":\"2026-03-05\",\"weight\":183.1,\"meanBloodPressure\":107},{\"day\":\"2026-03-01\",\"weight\":182.7,\"meanBloodPressure\":107.5},{\"day\":\"2026-02-27\",\"weight\":182.4,\"meanBloodPressure\":104},{\"day\":\"2026-02-23\",\"weight\":181.6,\"meanBloodPressure\":102},{\"day\":\"2026-02-21\",\"weight\":181.2,\"meanBloodPressure\":98.5},{\"day\":\"2026-02-17\",\"weight\":180.8,\"meanBloodPressure\":95.5},{\"day\":\"2026-02-15\",\"weight\":180.9,\"meanBloodPressure\":100},{\"day\":\"2026-02-11\",\"weight\":181.2,\"meanBloodPressure\":99},{\"day\":\"2026-02-09\",\"weight\":181.5,\"meanBloodPressure\":104.5},{\"day\":\"2026-02-05\",\"weight\":182,\"meanBloodPressure\":107},{\"day\":\"2026-02-03\",\"weight\":182.1,\"meanBloodPressure\":106},{\"day\":\"2026-01-30\",\"weight\":181.9,\"meanBloodPressure\":107.5},{\"day\":\"2026-01-28\",\"weight\":181.6,\"meanBloodPressure\":104}]},\"moodContext\":{\"points\":70,\"mean\":3.03,\"latest\":1,\"series\":{\"recent\":[{\"date\":\"2026-05-26\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-05-27\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-28\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-29\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-05-30\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-05-31\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-01\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-02\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-03\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-04\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1},{\"date\":\"2026-06-05\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-06\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-07\",\"min\":4,\"max\":4,\"mean\":4,\"n\":1},{\"date\":\"2026-06-08\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-09\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-10\",\"min\":2,\"max\":2,\"mean\":2,\"n\":1},{\"date\":\"2026-06-11\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-12\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-13\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1},{\"date\":\"2026-06-14\",\"min\":3,\"max\":3,\"mean\":3,\"n\":1},{\"date\":\"2026-06-15\",\"min\":1,\"max\":1,\"mean\":1,\"n\":1}],\"weekly\":[{\"weekISO\":\"2026-W15\",\"min\":2,\"max\":5,\"mean\":3.33,\"n\":6},{\"weekISO\":\"2026-W16\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W17\",\"min\":1,\"max\":5,\"mean\":2.86,\"n\":7},{\"weekISO\":\"2026-W18\",\"min\":1,\"max\":5,\"mean\":3,\"n\":7},{\"weekISO\":\"2026-W19\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W20\",\"min\":1,\"max\":5,\"mean\":3.14,\"n\":7},{\"weekISO\":\"2026-W21\",\"min\":1,\"max\":4,\"mean\":2.57,\"n\":7},{\"weekISO\":\"2026-W22\",\"min\":5,\"max\":5,\"mean\":5,\"n\":1}],\"monthly\":[],\"yearly\":[]},\"moodVsWeightCorrelation\":{\"r\":0.523,\"strength\":\"moderat\",\"n\":35}}}" + ] + } + ], + "temperature": 0.45, + "maxTokens": 250, + "seed": 1618033, + "responseFormat": "json", + "timeoutMs": 60000 + } + ], + "enqueued": [], + "result": { + "hasProvider": false, + "text": "Últimamente algo fuera de tu rango habitual. Tu peso está en 185,6 ahora mismo — por encima de tu promedio habitual de 182,1. Pésate a la misma hora durante unos días y la fluctuación diaria normal se compensa sola.", + "cached": true, + "updatedAt": null + }, + "notes": [ + { + "userId": "parity-rich", + "metric": "weight", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": "4cc01c285b2f92494bb578350040930a348bdbd86863a98fd23c3dec38680143", + "snapshotHash": "47cecabde3126e95e9cd479b570e5644198b6882831fffb4d234b2a582a98df6", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "weight", + "locale": "en", + "text": "Parity note for fresh-en.", + "items": null, + "inputHash": "4cc01c285b2f92494bb578350040930a348bdbd86863a98fd23c3dec38680143", + "snapshotHash": "773348667d5c62724cc6803444fe5518c074e3f54eeb3d69a764b26c33e38255", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "weight", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "weight", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + } + ] + }, + "bare-error-en": { + "calls": [ + { + "system": [ + "You are writing a short, personal assessment of ONE of this user's health metrics. You know their data and you read it against their OWN baseline — never against a population norm. Your clinical grounding is established guidelines (ESH 2023, WHO, AASM, DGE, DEGAM); you reference them without diagnosing.", + "", + "YOUR DATA (graded snapshot):", + "- signal: the finished comparison — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` is the recent-window mean; `baseline` is the user's OWN longer average; `delta`/`deltaPct` are the SIGNED change vs that baseline; `spread` is their normal night-to-night swing; `outsideNormalSwing` is the pre-computed verdict on whether the change exceeds that swing. These are already computed — state them, do NOT recompute them. `contributors[]` (composite scores only) names the 1–2 sub-scores that moved the score.", + "- summary: { points, start, end, delta, mean, min, max } — the headline figures across the whole read window.", + "- series.recent[]: the last ~21 days, one row per day ({ date, min, max, mean, n }).", + "- series.weekly[]: the ~10 weeks before that, one ISO-week aggregate each.", + "- series.monthly[]: the ~12 months before that, one calendar-month aggregate each.", + "- series.yearly[]: everything older, one row per year ({ year, mean, min, max, n, slope }).", + "- So the most recent days are present individually; older spans are folded into week, month and year aggregates. Consult the series only for a specific day; lead from the signal block.", + "- A slice can be empty when the history is short — do not pretend it exists. When `signal.baseline` is null there is no established baseline yet; say so honestly instead of inventing a comparison.", + "", + "HOW TO BUILD THE ASSESSMENT — pick the 2–3 MOVES that fit THIS data and order them so they read as one connected thought, not a fixed checklist. Lead per the OPENER HINT in the user prompt when one is given — do NOT always open with the number. Vary the shape from one metric to the next; never open two metrics in a row the same way.", + "Moves available (choose the few that genuinely apply):", + "(a) the current finding — the signal block's `current` (or a value from series.recent for a day-level question);", + "(b) place it against the user's OWN baseline via the pre-computed signed `delta` vs `baselineLabel` — state it, do not re-derive it. Treat `outsideNormalSwing: false` as \"inside your usual range — not a finding\" and do NOT manufacture a trend; `outsideNormalSwing: true` is the real, reportable change. A `normalRange`/`placement` is only a coarse secondary anchor — the personal delta leads;", + "(c) name a genuine, earned win or a steady streak when the data shows one;", + "(d) one watch-item worth keeping an eye on;", + "(e) an association ONLY when an r-value is present and |r| > 0.4, phrased as \"moves with\" / \"tends to show up alongside\", never causal;", + "(f) EXACTLY ONE doable next step — and only when the finding genuinely implies one. One message = one behaviour, no list. When the value is steady and in a good place, do NOT manufacture a step — affirm briefly and name one thing to watch instead. A fabricated step is exactly the platitude we ban.", + "Synthesize, don't recite: the story of what the data means matters more than re-stating every number.", + "", + "TONE — a warm, motivating premium advisor (someone really looked at this person's data and found something worth their attention):", + "- Second person (\"your blood pressure\", \"your values\"), warm, direct, honest.", + "- Motivating and forward-looking: when the data earns it, name the genuine win plainly and build a little momentum — make the person feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment.", + "- Autonomy-supporting: \"can help\", \"worth a try\", never \"you must\".", + "- Never alarming, never moralising, never diagnostic — make no disease claim. Frame as a reasoned observation, not medical advice.", + "- Name unfavourable values honestly too: finding -> place against the user's own baseline -> one small doable step, framed as an opportunity rather than a failing. Do not downplay, do not dramatise.", + "- No platitudes and no bare number-echoing: every warm line is anchored to a real figure or a real change. A generic positivity opener (\"Your numbers look good\") is banned — earn the encouragement with the specific finding.", + "", + "OPENING — lead with meaning, not the metric value", + "Open on the read in plain words — what this says about the person today — and bring the number in right after, as support, never as the first thing. A line that opens on a value (\"Your resting heart rate is 61 …\") reads as a readout; the same line that opens on the meaning (\"Your heart's running a little calmer this week — about 61 …\") reads as an advisor who actually looked. Keep every claim tied to a real figure; only the ORDER changes, meaning first and the number right behind it. This holds whether the surface covers one metric or many.", + "", + "LENGTH: 2-4 sentences, roughly 30-60 words — but when the metric is steady and there is nothing to act on, ONE tight sentence is better than padding it to length. Concise and high-quality. No bare number-echoing, no filler, no generic platitudes.", + "", + "JUDGE THE DATA HONESTLY (never invent a trend):", + "- Only a few measurement points/entries/events or an empty recent slice: say honestly that there is too little data for a reliable trend yet, and give ONE pointer (e.g. log more regularly). Do not claim a trend. What \"few\" means depends on the metric — for mood it is entries, for adherence it is scheduled doses.", + "- Newest measurement clearly older than ~7 days (dataCoverage.newestMeasurementDaysAgo): note that the values may be out of date.", + "- Mention correlations ONLY when the r-value is present in the snapshot and |r| > 0.4. If the field is missing, do not interpret or invent one. Always phrase as an \"association\", never a \"cause\".", + "", + "FORBIDDEN FILLER (signals ungrounded platitudes — never emit, except in a disclaimer)", + "\"make sure to get enough sleep\", \"drink enough water\", \"regular exercise\", \"consult your doctor\".", + "", + "METRIC IDENTIFIERS STAY OUT OF PROSE", + "Never write database / enum-style names like \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" or \"SLEEP_DURATION\" in any user-facing string, and never write the literal \"metric:\" chart-token in prose. Reference each metric with the natural-language label the user sees — \"your systolic\", \"your weight\", \"your pulse\", \"your mood\", \"your medication adherence\", \"your resting heart rate\", \"your sleep duration\", \"your steps\".", + "", + "GLP-1 DOSE SAFETY (contract, not style)", + "NEVER prescribe, recommend, or modify medication doses, even when the snapshot names a GLP-1 receptor agonist (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). You may NOTE the named medication and the current titration step (\"week 3 on 7.5 mg\") when the snapshot carries it, but never write \"step up to X mg\", \"consider increasing to Y mg\", \"stop at Z mg\", or any variation. A plateau always frames the next decision as a conversation with the prescribing clinician. If you are unsure whether something is dose-prescriptive, treat it as if it is and defer.", + "", + "ACUTE RED FLAGS (contract, not style)", + "If the user describes an acute crisis sign — chest pain or chest pressure, fainting or near-fainting (syncope), a sudden severe symptom (e.g. worst-ever headache, sudden weakness or trouble speaking, trouble breathing), a hypertensive-crisis reading (systolic ≥ 180 or diastolic ≥ 120 with symptoms), a severe-low or very-high glucose reading (below 54 mg/dL (3.0 mmol/L), or below 70 mg/dL (3.9 mmol/L) with symptoms, or at/above 250 mg/dL (13.9 mmol/L) with symptoms), a sustained fever at/above 38.5 °C (101.3 °F), or any mention of suicidal thoughts or self-harm — say plainly, in one calm sentence, that this needs prompt medical attention or emergency services now, and do NOT data-coach it. Do not diagnose, do not name a condition, do not estimate severity from the numbers — just point to prompt/emergency care and stop. This is a closed list; outside it, stay calm and non-alarmist as usual.", + "", + "GROUNDING — ZERO HALLUCINATIONS", + "Every claim must trace to a number visible in the snapshot you were given. If you cannot point to a snapshot field, do NOT make the claim, and never invent a measurement to satisfy a request. Read each value against the user's OWN baseline (their recent vs longer-window averages), never against a population norm. State a correlation only when its r-value is present and |r| > 0.4, always as an \"association\", never a \"cause\".", + "", + "TONE — a warm, motivating advisor, never clinical or alarming", + "Write in the second person, warm and direct. When the data earns it, name the genuine win plainly and build a little momentum — the person should feel seen and supported, not lectured. The encouragement must be EARNED by the numbers, never a reflexive compliment. Affirmation is anchored, specific, and proportionate: tie it to a real figure or change, make it non-transferable to another user, and keep credit quiet for quiet wins. Over-validation is a safety regression, not warmth — never affirm a worsening trend or an unsafe choice; validate the effort, not the choice, and when there is nothing to praise, stay neutral (\"nothing to act on — the good kind of boring\") rather than manufacturing a compliment. Be autonomy-supporting (\"worth a try\", \"can help\", never \"you must\"). Name unfavourable values honestly too — finding, then place it against the user's own baseline, then one small doable step framed as an opportunity. Never alarm, never moralise, never diagnose. No platitudes and no bare number-echoing. Do NOT open with a compliment about the data quantity or quality; banned openers include \"Your data foundation is strong\", \"Datengrundlage ist sehr stark\", \"You have a solid baseline\", \"Great dataset\", a generic \"Your numbers look good\", and any rephrasing of the same sentiment.", + "", + "NOT AN INSIGHT — never recite mechanics", + "A measurement count (\"logged 3 times\"), a logging cadence (\"measured every few days\"), or page-mechanics narration is NOT an insight and does not belong in the prose on its own. Use such a fact ONLY when it carries a consequence, and then state the consequence, not the count: e.g. \"too few readings this week to call a trend — one morning reading would settle it\", not \"you measured twice this week\". Every sentence must earn its place by telling the person what a value MEANS or what follows from it, never by narrating how the data was collected.", + "", + "INTERPRET, DON'T ENUMERATE", + "Say what a value MEANS, not just what it is. When the user prompt carries an INTERPRETATION CONTEXT block, use it: name where the value sits on the guideline scale, what that band means in plain, dignified words, and what practically follows. Judge the trend IN CONTEXT — a \"slightly rising\" reading deep inside a favourable band is a footnote; the same movement at or near a boundary is the headline. Frame any consequence WITHOUT diagnosis: \"values in this range are considered … per guidelines\", never \"you have X\" or \"you are at risk of X\". When NO interpretation block is present, the metric has no general reference band — interpret it relative to the person's OWN baseline and say plainly that no general reference band is attached, rather than inventing one.", + "", + "FORMATTING — write in short paragraphs separated by a BLANK LINE, each 1–3 sentences; a longer reply is 2–4 paragraphs, never one block. Put a blank line between distinct ideas (e.g. \"where things stand\" vs \"one thing to try\"). A steady one-liner stays a single paragraph — do NOT pad to fill a second. When you genuinely enumerate three or more parallel items (options, findings, steps), format them as a plain list: one item per line, each line starting with \"- \". You may bold the single most important takeaway with **double asterisks**, at most once per reply. Nothing else: no headings, no italics, no backticks, no numbered lists, no emojis.", + "", + "EXAMPLES — note the DIFFERENT shapes (verdict-led, trend-led, one-liner). They illustrate form and grounding only; never copy them — every assessment uses the real snapshot numbers:", + "- VERDICT-LED (meaning first, number as support, one step): \"Your resting heart rate is running a touch lower than usual — about 61 bpm this week, 5 below your monthly mean of 66, and your lowest in weeks. That kind of dip usually tracks with more movement; worth keeping the routine that earned it.\"", + "- TREND-LED (direction first, then where it stands): \"Your systolic pressure has been easing down steadily for three weeks now, sitting around 124 mmHg — about 4 under your 30-day average. Nothing dramatic, just a consistent direction.\"", + "- ONE-LINER (steady, NO forced step): \"Your SpO₂ is steady at 97 %, right inside your usual range — nothing to act on, the good kind of boring.\"", + "- BAD (banned filler, ungrounded — do NOT write this): \"Your numbers look good. Make sure to get enough sleep and keep up regular exercise.\"", + "", + "OUTPUT FORMAT: Reply with valid JSON only, in exactly this schema. The \"summary\" field holds the complete assessment in English: 1-3 short paragraphs of 1-3 sentences each, separated by a blank line written as \\n\\n INSIDE the JSON string. A steady one-liner stays a single paragraph:", + "{ \"summary\": \"...\" }", + "", + "METRIC — WEIGHT:", + "- The snapshot carries weight.signal (the finished comparison) + weight.summary + weight.series (graded). weight.latestDayFocus shows the latest daily value, the step from the previous measured day and, where present, the same-day blood pressure.", + "- This card's focus is the continuous TREND and PACE over time (pace per week, plateaus, milestones), not WHO-band placement — the BMI card carries that. Mention the BMI value only as a side reference, never make the band the central message here.", + "- The weekly/monthly trend matters, not the single value: lead from weight.signal — current, delta vs baselineLabel and outsideNormalSwing are already computed. STATE them, do NOT recompute. Day-to-day swings of 1 kg-2 kg (water, time of day) are normal and not a finding.", + "- Own target: when the snapshot carries weight.target, that is the target band the person set themselves, and weight.target.progress says which way is progress (gaining, losing, or holding steady). Judge every change AGAINST that target: below it, a gain is progress and never a setback or a regression. The loss pace and milestone guidance below applies only when losing is the progress direction or no target is set.", + "- Plateau: if the mean moves < 0.5 kg over > ~2 weeks, that is a plateau — name it honestly for someone who is losing, without discouraging.", + "- Pace: sustainable loss is ~0.5 kg-1 kg per week (DGE). Sustained > 1 kg per week is more likely water/muscle than fat — frame cautiously.", + "- Milestones against their own maximum: ~5% loss from the highest value (series max) already brings metabolic benefit, ~10% a substantial one — acknowledge explicitly when reached.", + "- BMI reference only as a brief aside when height is in the profile (do not claim it otherwise); the WHO-band placement itself belongs on the BMI card, not here.", + "- Associations: mention weightVsSystolic.correlation / weightVsMeanBloodPressure.correlation and moodContext.moodVsWeightCorrelation only when present and |r| > 0.4 — as an association, never a cause.", + "- One message: close with ONE doable step that fits the direction ONLY when the finding implies one (e.g. on a plateau, weigh at the same time of day to see the real trend rather than the daily noise). When the trend is steady and there is nothing useful to do, affirm it honestly and name one thing worth keeping an eye on instead of manufacturing a step." + ], + "messages": [ + { + "role": "user", + "content": [ + "Date: 2026-06-15", + "OPENER HINT: Open with what this means for the user today, then the number behind it.", + "Write one short assessment of this person's weight. Open with what the trend MEANS in plain words — the direction and momentum, not the number (e.g. \"easing down steadily\", \"holding right where it's settled\") — then bring in ONE concrete number from the snapshot right after as support, read as a continuous trend and pace against their own weekly/monthly baseline (pace per week, plateau, milestone — not the single value, and not the WHO band, which the BMI card covers); never lead with the value. Close with one doable step only when the finding genuinely implies one; when nothing is, skip the step rather than manufacture filler. Judge confidence from the measurement count and recency.", + "", + "PREVIOUS ANALYSIS: none on file. Treat this as the user's first analysis for this domain — no improvement/regression delta to surface.", + "", + "", + "VARIETY: to keep your cards from reading the same, you may open this one by leading with the single most recent value and where it sits — only if the data genuinely supports that angle; never force it, never invent a trend.", + "", + "DATA STRENGTH: 0 day-buckets in the window, no dated reading. — too few points for a reliable trend; hedge accordingly and avoid claiming a direction.", + "", + "", + "{\"locale\":\"en\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"weight\",\"dataCoverage\":{\"totalMeasurements\":0,\"totalSpanDays\":0,\"newestMeasurementDaysAgo\":null},\"weight\":{\"unit\":\"kg\",\"summary\":null,\"series\":{\"recent\":[],\"weekly\":[],\"monthly\":[],\"yearly\":[]},\"latestDayFocus\":null},\"bloodPressureContext\":{\"systolic\":{\"summary\":null,\"series\":{\"recent\":[],\"weekly\":[],\"monthly\":[],\"yearly\":[]}},\"diastolic\":{\"summary\":null,\"series\":{\"recent\":[],\"weekly\":[],\"monthly\":[],\"yearly\":[]}},\"pairedDaily\":[]},\"weightVsSystolic\":{\"correlation\":null,\"pairs\":[]},\"weightVsMeanBloodPressure\":{\"correlation\":null,\"pairs\":[]},\"moodContext\":null}" + ] + } + ], + "temperature": 0.45, + "maxTokens": 250, + "seed": 1618033, + "responseFormat": "json", + "timeoutMs": 60000 + } + ], + "enqueued": [], + "result": { + "hasProvider": false, + "text": "No assessment on this one right now. Weight reads as a trend rather than a daily number — weighed under consistent conditions, the normal day-to-day swing settles out on its own. The direction over weeks, read alongside blood pressure and BMI, is what carries.", + "cached": true, + "updatedAt": null + }, + "notes": [ + { + "userId": "parity-bare", + "metric": "weight", + "locale": "en", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "error" + }, + { + "userId": "parity-rich", + "metric": "weight", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": "4cc01c285b2f92494bb578350040930a348bdbd86863a98fd23c3dec38680143", + "snapshotHash": "47cecabde3126e95e9cd479b570e5644198b6882831fffb4d234b2a582a98df6", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "weight", + "locale": "en", + "text": "Parity note for fresh-en.", + "items": null, + "inputHash": "4cc01c285b2f92494bb578350040930a348bdbd86863a98fd23c3dec38680143", + "snapshotHash": "773348667d5c62724cc6803444fe5518c074e3f54eeb3d69a764b26c33e38255", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "weight", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "weight", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + } + ] + }, + "bare-error-de": { + "calls": [ + { + "system": [ + "Du schreibst eine kurze, persönliche Einschätzung zu EINER Gesundheitsmetrik dieses Nutzers. Du kennst seine Daten und ordnest sie gegen seine EIGENE Baseline ein — nicht gegen Bevölkerungsnormen. Deine fachliche Grundlage sind anerkannte Leitlinien (ESH 2023, WHO, AASM, DGE, DEGAM); du beziehst dich darauf, ohne zu diagnostizieren.", + "", + "DEINE DATENGRUNDLAGE (graded snapshot):", + "- signal: der fertige Vergleich — { metric, unit, current, currentWindowDays, baseline, baselineLabel, delta, deltaPct, spread, outsideNormalSwing, direction, normalRange?, placement?, n, newestDaysAgo, contributors? }. `current` ist das recent-Mittel; `baseline` ist der EIGENE längere Durchschnitt; `delta`/`deltaPct` sind die VORZEICHENBEHAFTETE Änderung gegenüber dieser Baseline; `spread` ist die normale Schwankung; `outsideNormalSwing` ist das vorab berechnete Urteil, ob die Änderung diese Schwankung übersteigt. Diese Werte sind bereits berechnet — nenne sie, rechne sie NICHT neu. `contributors[]` (nur Score-Komposite) benennt die 1–2 Teil-Scores, die den Score bewegt haben.", + "- summary: { points, start, end, delta, mean, min, max } — die Eckwerte des gesamten Lesefensters.", + "- series.recent[]: die jüngsten ~21 Tage, je ein Eintrag pro Tag ({ date, min, max, mean, n }).", + "- series.weekly[]: die ~10 Wochen davor, je ein ISO-Wochen-Aggregat.", + "- series.monthly[]: die ~12 Monate davor, je ein Monatsaggregat.", + "- series.yearly[]: alles ältere, je ein Jahr ({ year, mean, min, max, n, slope }).", + "- Die jüngsten Tage liegen also EINZELN vor, ältere Zeiträume als Wochen-, Monats- und Jahresaggregate. Ziehe die series nur für einen konkreten Tag heran; führe sonst mit dem signal-Block.", + "- Ein Slice kann leer sein, wenn die Historie kurz ist. Dann nicht so tun, als gäbe es ihn. Ist `signal.baseline` null, gibt es noch keine etablierte Baseline; sage das ehrlich, statt einen Vergleich zu erfinden.", + "", + "SO BAUST DU DIE EINSCHÄTZUNG — wähle die 2–3 BAUSTEINE, die zu DIESEN Daten passen, und ordne sie so, dass sie als EIN zusammenhängender Gedanke lesen, keine feste Checkliste. Führe gemäß dem OPENER-HINWEIS im User-Prompt, wenn einer mitgegeben ist — eröffne NICHT immer mit der Zahl. Variiere die Form von Metrik zu Metrik; eröffne nie zwei Metriken hintereinander gleich.", + "Verfügbare Bausteine (nimm die wenigen, die wirklich zutreffen):", + "(a) der aktuelle Befund — `current` aus dem signal-Block (oder ein Wert aus series.recent für eine Tagesfrage);", + "(b) gegen die EIGENE Baseline einordnen über das vorab berechnete vorzeichenbehaftete `delta` gegenüber `baselineLabel` — nenne es, leite es nicht neu her. Behandle `outsideNormalSwing: false` als \"innerhalb der üblichen Schwankung — kein Befund\" und ERFINDE KEINEN Trend; `outsideNormalSwing: true` ist die echte, meldenswerte Änderung. Ein `normalRange`/`placement` ist nur ein grober sekundärer Anker — das persönliche delta führt;", + "(c) einen echten, verdienten Erfolg oder eine stabile Serie benennen, wenn die Daten ihn zeigen;", + "(d) einen Punkt zum Im-Auge-behalten;", + "(e) einen Zusammenhang NUR, wenn ein r-Wert vorhanden und |r| > 0,4 ist, formuliert als \"bewegt sich mit\" / \"zeigt sich oft zusammen mit\", nie kausal;", + "(f) GENAU EINEN machbaren nächsten Schritt — und nur, wenn der Befund wirklich einen hergibt. Eine Botschaft = ein Verhalten, keine Liste. Ist der Wert stabil und im grünen Bereich, ERZWINGE KEINEN Schritt — bestätige kurz und nenne stattdessen einen Punkt zum Beobachten. Ein erfundener Schritt ist genau die Floskel, die wir vermeiden.", + "Synthese statt Aufzählung: die Geschichte dessen, was die Daten bedeuten, zählt mehr als das Wiederholen jeder Zahl.", + "", + "TONALITÄT — ein warmer, motivierender Premium-Begleiter (jemand hat sich die Daten dieser Person wirklich angesehen und etwas Wertvolles für sie gefunden):", + "- Zweite Person (\"dein Blutdruck\", \"deine Werte\"), warm, direkt, ehrlich.", + "- Motivierend und vorwärtsgewandt: Wenn die Daten es hergeben, benenne den echten Erfolg klar und baue ein wenig Schwung auf — die Person soll sich gesehen und unterstützt fühlen, nicht belehrt. Die Ermutigung muss durch die Zahlen VERDIENT sein, nie ein reflexhaftes Kompliment.", + "- Autonomie-unterstützend: \"kann helfen\", \"einen Versuch wert\", nie \"du musst\".", + "- Nie alarmierend, nie moralisierend, nie diagnostisch — keine Krankheitsbehauptung. Formuliere als begründete Beobachtung, nicht als ärztlichen Rat.", + "- Auch ungünstige Werte ehrlich benennen: Befund → gegen die eigene Baseline einordnen → ein kleiner machbarer Schritt, als Chance formuliert, nicht als Versagen. Nicht verharmlosen, nicht dramatisieren.", + "- Keine Floskeln und keine bloße Zahlenwiederholung: jede warme Zeile ist an eine echte Zahl oder eine echte Veränderung geknüpft. Ein generischer Positiv-Einstieg (\"Deine Werte sehen gut aus\") ist verboten — verdiene die Ermutigung mit dem konkreten Befund.", + "", + "ERÖFFNUNG — mit der Bedeutung führen, nicht mit dem Messwert", + "Beginne mit dem Eindruck in klaren Worten — was das heute über die Person sagt — und bring die Zahl direkt danach als Beleg, nie als Erstes. Eine Zeile, die mit einem Wert eröffnet (\"Dein Ruhepuls ist 61 …\"), liest sich wie ein Ablesen; dieselbe Zeile, die mit der Bedeutung eröffnet (\"Dein Herz läuft diese Woche etwas ruhiger — rund 61 …\"), liest sich wie ein Begleiter, der wirklich hingesehen hat. Jede Aussage bleibt an einer echten Zahl verankert; nur die REIHENFOLGE ändert sich, Bedeutung zuerst und die Zahl direkt dahinter. Das gilt, ob die Fläche eine Metrik abdeckt oder viele.", + "", + "LÄNGE: 2-4 Sätze, ca. 30-60 Wörter — ist der Wert aber stabil und nichts zu tun, ist EIN knapper Satz besser, als ihn auf Länge zu strecken. Knapp und hochwertig. Keine bloße Zahlenwiederholung, kein Fülltext, keine generischen Floskeln.", + "", + "DATENLAGE EHRLICH BEWERTEN (nie einen Trend erfinden):", + "- Nur wenige Messpunkte/Einträge/Ereignisse oder ein leerer recent-Slice: ehrlich sagen, dass es für eine belastbare Tendenz noch zu wenige Daten sind, und EINEN Hinweis geben (z.B. regelmäßiger erfassen). Keinen Trend behaupten. Was \"wenige\" bedeutet, hängt von der Metrik ab — bei Stimmung sind es Einträge, bei der Einnahmetreue geplante Dosen.", + "- Jüngste Messung deutlich älter als ~7 Tage (dataCoverage.newestMeasurementDaysAgo): darauf hinweisen, dass die Werte nicht mehr aktuell sind.", + "- Korrelationen NUR erwähnen, wenn der r-Wert im Snapshot vorhanden ist und |r| > 0.4 ist. Fehlt das Feld, keine Korrelation interpretieren oder erfinden. Immer als \"Zusammenhang\" formulieren, nie als \"Ursache\".", + "", + "VERBOTENE FLOSKELN (signalisieren ungegroundeten Fülltext — nie ausgeben, außer im Disclaimer)", + "\"achte auf ausreichend Schlaf\", \"trinke genug Wasser\", \"regelmäßige Bewegung\", \"ärztlicher Rat empfohlen\".", + "", + "METRIK-IDENTIFIER GEHÖREN NICHT IN DEN FLIESSTEXT", + "Schreibe niemals Datenbank- bzw. Enum-Namen wie \"BLOOD_PRESSURE_SYS\", \"PULSE_BPM\", \"MOOD_SCORE\", \"MEDICATION_COMPLIANCE_PCT\", \"HEART_RATE_VARIABILITY\", \"RESTING_HEART_RATE\", \"ACTIVE_ENERGY_BURNED\", \"FLIGHTS_CLIMBED\", \"WALKING_RUNNING_DISTANCE\", \"VO2_MAX\", \"BODY_TEMPERATURE\" oder \"SLEEP_DURATION\" in nutzersichtbare Strings, und schreibe nie das wörtliche \"metric:\"-Chart-Token in den Fließtext. Verweise auf jede Metrik mit der natürlichsprachlichen Bezeichnung, die der Nutzer sieht — \"deine Systole\", \"dein Gewicht\", \"dein Puls\", \"deine Stimmung\", \"deine Medikamentenadhärenz\", \"dein Ruhepuls\", \"deine Schlafdauer\", \"deine Schritte\".", + "", + "GLP-1-DOSIS-SICHERHEIT (Vertrag, kein Stil)", + "Verschreibe, empfiehl oder ändere NIEMALS Medikamenten-Dosen, auch wenn der Snapshot einen GLP-1-Rezeptoragonisten benennt (Mounjaro, Ozempic, Wegovy, Zepbound, Trulicity, Saxenda, Rybelsus). Du darfst den benannten Wirkstoff und die aktuelle Titrationsstufe NENNEN (\"Woche 3 auf 7,5 mg\"), wenn der Snapshot sie trägt, aber schreibe nie \"erhöhe auf X mg\", \"erwäge die nächste Stufe Y mg\", \"bleibe auf Z mg\" oder eine Variante davon. Ein Plateau rahmt die nächste Entscheidung immer als Gespräch mit der behandelnden Ärztin. Bist du unsicher, ob etwas dosis-präskriptiv ist, behandle es so — und verweise an die Klinik.", + "", + "AKUTE WARNZEICHEN (Vertrag, kein Stil)", + "Beschreibt der Nutzer ein akutes Krisenzeichen — Brustschmerz oder Druck auf der Brust, Ohnmacht oder Beinahe-Ohnmacht (Synkope), ein plötzliches schweres Symptom (z. B. stärkster Kopfschmerz aller Zeiten, plötzliche Schwäche oder Sprachstörung, Atemnot), einen hypertensiven Notfallwert (systolisch ≥ 180 oder diastolisch ≥ 120 mit Symptomen), einen schwer-niedrigen oder sehr hohen Glukosewert (unter 54 mg/dL (3,0 mmol/L), oder unter 70 mg/dL (3,9 mmol/L) mit Symptomen, oder ≥ 250 mg/dL (13,9 mmol/L) mit Symptomen), anhaltendes Fieber ≥ 38,5 °C (101,3 °F) oder Gedanken an Suizid bzw. Selbstverletzung — sage in einem ruhigen Satz klar, dass das jetzt umgehende ärztliche Hilfe oder den Notruf braucht, und coache es NICHT anhand der Daten. Diagnostiziere nicht, benenne keine Erkrankung, schätze keinen Schweregrad aus den Zahlen — verweise nur auf umgehende Hilfe bzw. den Notruf und höre auf. Das ist eine geschlossene Liste; außerhalb davon bleibe wie gewohnt ruhig und nicht alarmierend.", + "", + "ERDUNG — NULL HALLUZINATIONEN", + "Jede Aussage muss auf einer Zahl beruhen, die im übergebenen Snapshot sichtbar ist. Lässt sich die Aussage keinem Snapshot-Feld zuordnen, lass sie weg und erfinde nie einen Messwert, um einer Anfrage zu entsprechen. Ordne jeden Wert gegen die EIGENE Baseline des Nutzers ein (jüngeres vs. längeres Mittel), nie gegen eine Bevölkerungsnorm. Nenne einen Zusammenhang nur, wenn der r-Wert vorhanden und |r| > 0,4 ist — immer als \"Zusammenhang\", nie als \"Ursache\".", + "", + "TONALITÄT — ein warmer, motivierender Begleiter, nie klinisch oder alarmierend", + "Schreibe in der zweiten Person, warm und direkt. Wenn die Daten es hergeben, benenne den echten Erfolg klar und baue ein wenig Schwung auf — die Person soll sich gesehen und unterstützt fühlen, nicht belehrt. Die Ermutigung muss durch die Zahlen VERDIENT sein, nie ein reflexhaftes Kompliment. Lob ist verankert, konkret und angemessen: an einer echten Zahl oder Veränderung festgemacht, nicht auf andere Nutzer übertragbar, und stiller Anerkennung für stille Erfolge. Über-Bestätigung ist eine Sicherheitsregression, keine Wärme — bestätige nie einen sich verschlechternden Trend oder eine unsichere Entscheidung; würdige die Mühe, nicht die Entscheidung, und bleib neutral (\"nichts zu tun — die gute Art von langweilig\"), wenn es nichts zu loben gibt, statt ein Kompliment zu erfinden. Sei autonomie-unterstützend (\"einen Versuch wert\", \"kann helfen\", nie \"du musst\"). Benenne auch ungünstige Werte ehrlich — Befund, dann gegen die eigene Baseline einordnen, dann ein kleiner machbarer Schritt, als Chance formuliert. Nie alarmierend, nie moralisierend, nie diagnostisch. Keine Floskeln und keine bloße Zahlenwiederholung. Beginne NICHT mit einem Kompliment über Datenmenge oder -qualität; verbotene Eröffnungen sind u.a. \"Datengrundlage ist sehr stark\", \"Your data foundation is strong\", \"Du hast eine solide Baseline\", \"Großartiger Datensatz\", ein generisches \"Deine Werte sehen gut aus\" und jede sinngemäße Umformulierung.", + "", + "KEINE EINSCHÄTZUNG — nie Mechanik nacherzählen", + "Eine Messanzahl (\"3-mal erfasst\"), ein Erfassungsrhythmus (\"alle paar Tage gemessen\") oder Bedien-Mechanik ist KEINE Einschätzung und gehört für sich genommen nicht in den Text. Nutze eine solche Angabe NUR, wenn sie eine Konsequenz trägt, und nenne dann die Konsequenz, nicht die Zahl: z. B. \"diese Woche zu wenige Werte für eine Tendenz — eine Morgenmessung würde es klären\", nicht \"du hast diese Woche zweimal gemessen\". Jeder Satz muss sich verdienen, indem er sagt, was ein Wert BEDEUTET oder was daraus folgt — nie, indem er erzählt, wie die Daten erhoben wurden.", + "", + "EINORDNEN, NICHT AUFZÄHLEN", + "Sag, was ein Wert BEDEUTET, nicht nur, was er ist. Trägt der User-Prompt einen EINORDNUNGS-KONTEXT-Block, nutze ihn: benenne, wo der Wert auf der Leitlinien-Skala liegt, was dieses Band in klaren, würdevollen Worten bedeutet und was praktisch folgt. Beurteile den Trend IM KONTEXT — ein \"leicht steigender\" Wert tief in einem günstigen Band ist eine Randnotiz; dieselbe Bewegung an oder nahe einer Grenze ist die Schlagzeile. Rahme jede Konsequenz OHNE Diagnose: \"Werte in diesem Bereich gelten laut Leitlinien als …\", nie \"du hast X\" oder \"du bist gefährdet für X\". Ist KEIN Einordnungs-Block vorhanden, hat die Metrik kein allgemeines Referenzband — ordne sie gegen die EIGENE Baseline der Person ein und sage klar, dass kein allgemeines Referenzband hinterlegt ist, statt eines zu erfinden.", + "", + "FORMATIERUNG — schreibe in kurzen Absätzen, getrennt durch eine LEERZEILE, je 1–3 Sätze; eine längere Antwort hat 2–4 Absätze, nie einen Block. Setze eine Leerzeile zwischen unterschiedliche Gedanken (z. B. \"wie es steht\" vs. \"eine Sache zum Ausprobieren\"). Ein stabiler Einzeiler bleibt EIN Absatz — strecke nicht auf einen zweiten. Wenn du wirklich drei oder mehr parallele Punkte aufzählst (Optionen, Befunde, Schritte), formatiere sie als schlichte Liste: ein Punkt pro Zeile, jede Zeile beginnt mit \"- \". Die EINE wichtigste Kernaussage darfst du mit **doppelten Sternchen** fett setzen, höchstens einmal pro Antwort. Sonst nichts: keine Überschriften, kein Kursiv, keine Backticks, keine nummerierten Listen, keine Emojis.", + "", + "BEISPIELE — beachte die UNTERSCHIEDLICHEN Formen (urteil-zuerst, trend-zuerst, Einzeiler). Sie illustrieren nur Form und Erdung; übernimm sie nie — jede Einschätzung nutzt die echten Snapshot-Zahlen:", + "- URTEIL-ZUERST (Bedeutung zuerst, Zahl als Beleg, ein Schritt): \"Dein Ruhepuls läuft gerade einen Tick niedriger als sonst — diese Woche rund 61 bpm, 5 unter deinem Monatsmittel von 66 und dein niedrigster seit Wochen. So ein Rückgang passt meist zu mehr Bewegung; die Routine, die das gebracht hat, lohnt sich beizubehalten.\"", + "- TREND-ZUERST (Richtung zuerst, dann der Stand): \"Dein systolischer Blutdruck geht seit drei Wochen ruhig nach unten, aktuell rund 124 mmHg — etwa 4 unter deinem 30-Tage-Schnitt. Nichts Dramatisches, einfach eine stetige Richtung.\"", + "- EINZEILER (stabil, KEIN erzwungener Schritt): \"Deine SpO₂ ist mit 97 % stabil und genau in deinem üblichen Bereich — nichts zu tun, die gute Art von langweilig.\"", + "- SCHLECHT (verbotene Floskel, ungegroundet — so NICHT): \"Deine Werte sehen gut aus. Achte auf ausreichend Schlaf und regelmäßige Bewegung.\"", + "", + "AUSGABEFORMAT: Antworte ausschließlich mit validem JSON in genau diesem Schema. Das Feld \"summary\" enthält die komplette Einschätzung auf Deutsch: 1-3 kurze Absätze mit je 1-3 Sätzen, getrennt durch eine Leerzeile als \\n\\n INNERHALB des JSON-Strings. Ein stabiler Einzeiler bleibt EIN Absatz:", + "{ \"summary\": \"...\" }", + "", + "METRIK — GEWICHT:", + "- Der Snapshot trägt weight.signal (der fertige Vergleich) + weight.summary + weight.series (graded). weight.latestDayFocus zeigt den jüngsten Tageswert, den Schritt zum vorherigen Messtag und ggf. den Blutdruck desselben Tages.", + "- Fokus dieser Karte ist der kontinuierliche TREND und das TEMPO über die Zeit (Tempo pro Woche, Plateaus, Meilensteine), nicht die WHO-Band-Einordnung — die trägt die BMI-Karte. Den BMI-Wert nur als Nebenbezug nennen, nie das Band als zentrale Aussage hier.", + "- Wochen-/Monatstrend zählt, nicht der Einzelwert: Führe mit weight.signal — current, delta gegenüber baselineLabel und outsideNormalSwing sind bereits berechnet. NENNE sie, rechne sie NICHT neu. Tageschwankungen von 1 kg-2 kg (Wasser, Tageszeit) sind normal und kein Befund.", + "- Eigenes Ziel: Trägt der Snapshot weight.target, ist das der Zielbereich, den die Person selbst gesetzt hat; weight.target.progress sagt, welche Richtung Fortschritt ist (gaining = zunehmen, losing = abnehmen, holding steady = halten). Beurteile jede Veränderung GEGEN dieses Ziel: Liegt die Person darunter, ist eine Zunahme Fortschritt und nie ein Rückschritt. Die Hinweise zu Tempo und Meilensteinen beim Abnehmen unten gelten nur, wenn Abnehmen die Fortschrittsrichtung ist oder kein Ziel vorliegt.", + "- Plateau: Verändert sich das Mittel über > ~2 Wochen um < 0.5 kg, ist das ein Plateau — bei einem Abnehmenden ehrlich benennen, ohne zu entmutigen.", + "- Tempo: Nachhaltiger Verlust liegt bei ~0.5 kg-1 kg pro Woche (DGE). Anhaltend > 1 kg pro Woche ist eher Wasser/Muskel als Fett — vorsichtig einordnen.", + "- Meilensteine gegen das eigene Maximum: ~5 % Verlust vom höchsten Wert (series-Max) bringt bereits metabolischen Nutzen, ~10 % einen deutlichen — wenn erreicht, ausdrücklich anerkennen.", + "- BMI-Bezug nur als knapper Nebensatz, wenn die Größe im Profil vorliegt (sonst nicht behaupten); die WHO-Band-Einordnung selbst gehört auf die BMI-Karte, nicht hierher.", + "- Zusammenhänge: weightVsSystolic.correlation / weightVsMeanBloodPressure.correlation und moodContext.moodVsWeightCorrelation nur erwähnen, wenn vorhanden und |r| > 0.4 — als Zusammenhang, nie als Ursache.", + "- Eine Botschaft: Schließe NUR DANN mit EINEM machbaren Schritt, wenn der Befund einen nahelegt (z.B. bei einem Plateau zur selben Tageszeit wiegen, um den echten Trend zu sehen, statt am Tagesrauschen zu hängen). Ist der Trend stabil und gibt es nichts Sinnvolles zu tun, erkenne das ehrlich an und nenne stattdessen einen Punkt, den man im Auge behalten kann, statt einen Schritt zu erzwingen." + ], + "messages": [ + { + "role": "user", + "content": [ + "Datum: 2026-06-15", + "OPENER HINT: Eröffne damit, was das heute für den Nutzer bedeutet, und dann die Zahl dahinter.", + "Schreibe eine kurze Einschätzung zum Gewicht dieser Person. Beginne mit der BEDEUTUNG in klaren Worten — Richtung und Tempo, nicht der Zahl (z. B. \"geht ruhig nach unten\", \"hält sich genau da, wo es sich eingependelt hat\") — und bring danach EINE konkrete Zahl aus dem Snapshot als Beleg, als kontinuierlichen Trend und Tempo gegen die eigene Wochen-/Monats-Baseline gelesen (Tempo pro Woche, Plateau, Meilenstein — nicht der Einzelwert und nicht das WHO-Band, das die BMI-Karte trägt); führe nie mit dem Wert. Schließe nur dann mit einem machbaren Schritt, wenn der Befund wirklich einen hergibt; ist nichts umsetzbar, lass den Schritt weg statt Fülltext zu erfinden. Konfidenz aus Messanzahl und Aktualität ableiten.", + "", + "VORHERIGE ANALYSE: keine vorhanden. Behandle dies als erste Analyse in diesem Bereich — kein Verbesserungs-/Verschlechterungs-Delta zu nennen.", + "", + "", + "ABWECHSLUNG: damit deine Karten nicht gleich klingen, darfst du diese mit dem aktuellsten Einzelwert und seiner Einordnung eröffnen — nur wenn die Daten diesen Blickwinkel wirklich tragen; nie erzwingen, nie einen Trend erfinden.", + "", + "DATENLAGE: 0 Tages-Buckets im Fenster, kein datierter Wert. — zu wenige Punkte für einen belastbaren Trend; entsprechend vorsichtig formulieren, keine Richtung behaupten.", + "", + "", + "{\"locale\":\"de\",\"generatedForDay\":\"2026-06-15\",\"focus\":\"weight\",\"dataCoverage\":{\"totalMeasurements\":0,\"totalSpanDays\":0,\"newestMeasurementDaysAgo\":null},\"weight\":{\"unit\":\"kg\",\"summary\":null,\"series\":{\"recent\":[],\"weekly\":[],\"monthly\":[],\"yearly\":[]},\"latestDayFocus\":null},\"bloodPressureContext\":{\"systolic\":{\"summary\":null,\"series\":{\"recent\":[],\"weekly\":[],\"monthly\":[],\"yearly\":[]}},\"diastolic\":{\"summary\":null,\"series\":{\"recent\":[],\"weekly\":[],\"monthly\":[],\"yearly\":[]}},\"pairedDaily\":[]},\"weightVsSystolic\":{\"correlation\":null,\"pairs\":[]},\"weightVsMeanBloodPressure\":{\"correlation\":null,\"pairs\":[]},\"moodContext\":null}" + ] + } + ], + "temperature": 0.45, + "maxTokens": 250, + "seed": 1618033, + "responseFormat": "json", + "timeoutMs": 60000 + } + ], + "enqueued": [], + "result": { + "hasProvider": false, + "text": "Für diese Karte liegt gerade keine Einschätzung vor. Gewicht liest sich als Verlauf, nicht als Tageszahl — unter konstanten Bedingungen gewogen, pendelt sich die normale Tagesschwankung von selbst ein. Die Richtung über Wochen, zusammen mit Blutdruck und BMI gelesen, ist das, was trägt.", + "cached": true, + "updatedAt": null + }, + "notes": [ + { + "userId": "parity-bare", + "metric": "weight", + "locale": "de", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "error" + }, + { + "userId": "parity-bare", + "metric": "weight", + "locale": "en", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "error" + }, + { + "userId": "parity-rich", + "metric": "weight", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": "4cc01c285b2f92494bb578350040930a348bdbd86863a98fd23c3dec38680143", + "snapshotHash": "47cecabde3126e95e9cd479b570e5644198b6882831fffb4d234b2a582a98df6", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "weight", + "locale": "en", + "text": "Parity note for fresh-en.", + "items": null, + "inputHash": "4cc01c285b2f92494bb578350040930a348bdbd86863a98fd23c3dec38680143", + "snapshotHash": "773348667d5c62724cc6803444fe5518c074e3f54eeb3d69a764b26c33e38255", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "weight", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "weight", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + } + ] + }, + "keyless-en": { + "calls": [], + "enqueued": [], + "result": { + "hasProvider": false, + "text": "A little off your usual lately. Your weight is at 84.2 right now — higher than your usual average of 82.6. Weigh in at the same time for a few days and the normal day-to-day swing sorts itself out.", + "cached": true, + "updatedAt": null + }, + "notes": [ + { + "userId": "parity-bare", + "metric": "weight", + "locale": "de", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "error" + }, + { + "userId": "parity-bare", + "metric": "weight", + "locale": "en", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "error" + }, + { + "userId": "parity-rich", + "metric": "weight", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": "4cc01c285b2f92494bb578350040930a348bdbd86863a98fd23c3dec38680143", + "snapshotHash": "47cecabde3126e95e9cd479b570e5644198b6882831fffb4d234b2a582a98df6", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "weight", + "locale": "en", + "text": "Parity note for fresh-en.", + "items": null, + "inputHash": "4cc01c285b2f92494bb578350040930a348bdbd86863a98fd23c3dec38680143", + "snapshotHash": "773348667d5c62724cc6803444fe5518c074e3f54eeb3d69a764b26c33e38255", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "weight", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "weight", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + } + ] + }, + "keyless-de": { + "calls": [], + "enqueued": [], + "result": { + "hasProvider": false, + "text": "Zuletzt etwas außerhalb deines üblichen Rahmens. Dein Gewicht liegt aktuell bei 84,2 — höher als dein üblicher Schnitt von 82,6. Wäge dich ein paar Tage zur gleichen Zeit, dann ordnet sich die normale Schwankung von selbst ein.", + "cached": true, + "updatedAt": null + }, + "notes": [ + { + "userId": "parity-bare", + "metric": "weight", + "locale": "de", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "error" + }, + { + "userId": "parity-bare", + "metric": "weight", + "locale": "en", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "error" + }, + { + "userId": "parity-rich", + "metric": "weight", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": "4cc01c285b2f92494bb578350040930a348bdbd86863a98fd23c3dec38680143", + "snapshotHash": "47cecabde3126e95e9cd479b570e5644198b6882831fffb4d234b2a582a98df6", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "weight", + "locale": "en", + "text": "Parity note for fresh-en.", + "items": null, + "inputHash": "4cc01c285b2f92494bb578350040930a348bdbd86863a98fd23c3dec38680143", + "snapshotHash": "773348667d5c62724cc6803444fe5518c074e3f54eeb3d69a764b26c33e38255", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "weight", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "weight", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + } + ] + }, + "keyless-read-only": { + "calls": [], + "enqueued": [], + "result": { + "hasProvider": false, + "text": "No assessment on this one right now. Weight reads as a trend rather than a daily number — weighed under consistent conditions, the normal day-to-day swing settles out on its own. The direction over weeks, read alongside blood pressure and BMI, is what carries.", + "cached": true, + "updatedAt": null + }, + "notes": [ + { + "userId": "parity-bare", + "metric": "weight", + "locale": "de", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "error" + }, + { + "userId": "parity-bare", + "metric": "weight", + "locale": "en", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "error" + }, + { + "userId": "parity-rich", + "metric": "weight", + "locale": "de", + "text": "Das ist kein JSON.", + "items": null, + "inputHash": "4cc01c285b2f92494bb578350040930a348bdbd86863a98fd23c3dec38680143", + "snapshotHash": "47cecabde3126e95e9cd479b570e5644198b6882831fffb4d234b2a582a98df6", + "dateKey": "2026-06-15", + "generatedAt": "2026-06-15T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "weight", + "locale": "en", + "text": "Parity note for fresh-en.", + "items": null, + "inputHash": "4cc01c285b2f92494bb578350040930a348bdbd86863a98fd23c3dec38680143", + "snapshotHash": "773348667d5c62724cc6803444fe5518c074e3f54eeb3d69a764b26c33e38255", + "dateKey": "2026-06-16", + "generatedAt": "2026-06-16T10:00:00.000Z", + "retryAt": null, + "negativeReason": null + }, + { + "userId": "parity-rich", + "metric": "weight", + "locale": "es", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + }, + { + "userId": "parity-rich", + "metric": "weight", + "locale": "fr", + "text": null, + "items": null, + "inputHash": null, + "snapshotHash": null, + "dateKey": "2026-06-15", + "generatedAt": null, + "retryAt": "2026-06-15T10:05:00.000Z", + "negativeReason": "screened" + } + ] + } +} diff --git a/tests/integration/status-generator-parity.test.ts b/tests/integration/status-generator-parity.test.ts new file mode 100644 index 000000000..03968a913 --- /dev/null +++ b/tests/integration/status-generator-parity.test.ts @@ -0,0 +1,775 @@ +/** + * Output parity for the per-card status generators. + * + * Every status card (the specialised cards, the generic HealthKit-metric + * card, the biomarker card, the general overview, and the batched warm pass) + * is driven against a real Postgres with fixed fixtures and a frozen clock, + * through its public entry point. The model is the only stub: it records the + * exact request it is sent and answers with a fixed completion. For each card + * and each arm (a fresh generation, a cache hit, an unchanged-data run, the + * next day, the read-only paths, no provider, no data, a screened reply and a + * failing model) the file records + * + * - every request the model received, prompts included, + * - the result the entry point returned, + * - the stored notes, decrypted, with their fingerprints and windows, + * - every out-of-band generation the read-only path queued, + * + * and compares the lot, byte for byte, with the golden files under + * `__golden__/status-parity/`. A refactor of the generators must leave every + * one of them unchanged; a deliberate change to what a card sends or answers + * rewrites them with `STATUS_PARITY_WRITE=1` in the same commit, so the diff + * shows exactly what moved. + */ +import { mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { resolve } from "node:path"; + +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +import { decryptFromBytes } from "@/lib/ai/coach/bytes-codec"; +import { settleBackgroundTasks } from "@/lib/logging/background-tasks"; + +import { getPrismaClient, truncateAllTables } from "./setup"; + +interface ModelReply { + content: string; + model: string; + tokensUsed: number; +} + +const model = vi.hoisted(() => ({ + calls: [] as unknown[], + reply: async (_params: unknown): Promise => { + throw new Error("no reply configured"); + }, +})); + +const queue = vi.hoisted(() => ({ enqueued: [] as unknown[] })); + +/** + * The negative-cache window a fallback opens is written without being awaited + * (the render must not wait on it). The writer is passed through unchanged; + * the test only keeps hold of the write so an arm can settle it before the + * stored notes are read. + */ +const pendingWrites = vi.hoisted(() => new Set>()); + +vi.mock("@/lib/ai/provider", async (importOriginal) => ({ + // The presence probe the capability reads stays real; only the chain the + // generation walks is replaced by the recording stand-in. + ...(await importOriginal()), + resolveProviderChain: async () => [ + { + providerType: "anthropic", + instance: { + type: "anthropic", + generateCompletion: async (params: unknown) => { + model.calls.push(params); + return model.reply(params); + }, + }, + }, + ], + resolveProvider: async () => ({ type: "none" }), +})); + +vi.mock( + "@/lib/jobs/insight-status-generate-shared", + async (importOriginal) => ({ + ...(await importOriginal< + typeof import("@/lib/jobs/insight-status-generate-shared") + >()), + enqueueStatusGeneration: async (args: unknown) => { + queue.enqueued.push(args); + }, + }), +); + +vi.mock("@/lib/insights/status-cache", async (importOriginal) => { + const actual = + await importOriginal(); + return { + ...actual, + writeStatusNegativeWindow: ( + args: Parameters[0], + ) => { + const write = actual.writeStatusNegativeWindow(args); + pendingWrites.add(write); + return write; + }, + }; +}); + +const GOLDEN_DIR = resolve(__dirname, "__golden__/status-parity"); +const WRITE = process.env.STATUS_PARITY_WRITE === "1"; +const NOW = new Date("2026-06-15T10:00:00.000Z"); +const DAY_MS = 24 * 60 * 60 * 1000; +const SLOW = 180_000; + +const RICH = "parity-rich"; +const BARE = "parity-bare"; +const KEYLESS = "parity-keyless"; +const BIOMARKER = "parity-ldl"; +const EMPTY_BIOMARKER = "parity-empty-marker"; + +// ── The model stand-in ─────────────────────────────────────────────────────── + +function answer(content: string): void { + model.reply = async () => ({ + content, + model: "parity-model", + tokensUsed: 42, + }); +} + +function answerSummary(text: string): void { + answer(JSON.stringify({ summary: text })); +} + +function fail(): void { + model.reply = async () => { + throw new Error("provider unavailable"); + }; +} + +/** One recorded request, with long prompt strings split into lines. */ +function readable(value: unknown): unknown { + if (typeof value === "string") { + return value.includes("\n") ? value.split("\n") : value; + } + if (Array.isArray(value)) return value.map(readable); + if (value && typeof value === "object") { + return Object.fromEntries( + Object.entries(value).map(([key, entry]) => [key, readable(entry)]), + ); + } + return value; +} + +// ── Fixtures ───────────────────────────────────────────────────────────────── + +function daysAgo(days: number, hour = 7): Date { + const at = new Date(NOW.getTime() - days * DAY_MS); + at.setUTCHours(hour, 15, 0, 0); + return at; +} + +async function makeAccount( + id: string, + options: { withProvider: boolean; profile: "rich" | "plain" | "none" }, +) { + const prisma = getPrismaClient(); + const profile = + options.profile === "rich" + ? { + heightCm: 178, + dateOfBirth: new Date("1984-03-09T00:00:00.000Z"), + gender: "MALE", + unitPreference: "imperial", + glucoseUnit: "mmol/L", + } + : options.profile === "plain" + ? { + heightCm: 165, + dateOfBirth: new Date("1991-11-20T00:00:00.000Z"), + gender: "FEMALE", + } + : {}; + return prisma.user.create({ + data: { + id, + username: id, + email: `${id}@example.test`, + role: "USER", + timezone: "Europe/Berlin", + locale: "en", + onboardingCompletedAt: new Date("2026-01-01T00:00:00.000Z"), + ...(options.withProvider + ? { + aiProviderChain: [{ providerType: "anthropic", enabled: true }], + // Presence only: the capability probe never decrypts. + aiAnthropicKeyEncrypted: "key-present", + } + : {}), + ...profile, + }, + }); +} + +async function seedMeasurements(userId: string): Promise { + const prisma = getPrismaClient(); + const rows: Array<{ + userId: string; + type: + | "WEIGHT" + | "BLOOD_PRESSURE_SYS" + | "BLOOD_PRESSURE_DIA" + | "PULSE" + | "RESTING_HEART_RATE" + | "BLOOD_GLUCOSE" + | "SLEEP_DURATION"; + value: number; + unit: string; + source: "MANUAL" | "APPLE_HEALTH"; + measuredAt: Date; + sleepStage?: "ASLEEP"; + }> = []; + for (let day = 0; day < 140; day++) { + const wave = Math.sin(day / 5); + if (day % 2 === 0) { + rows.push({ + userId, + type: "WEIGHT", + value: 84.2 - day * 0.015 + wave * 0.4, + unit: "kg", + source: "MANUAL", + measuredAt: daysAgo(day, 6), + }); + } + if (day % 3 !== 2) { + rows.push( + { + userId, + type: "BLOOD_PRESSURE_SYS", + value: Math.round(128 + wave * 6 - (day % 7)), + unit: "mmHg", + source: "MANUAL", + measuredAt: daysAgo(day, 8), + }, + { + userId, + type: "BLOOD_PRESSURE_DIA", + value: Math.round(82 + wave * 4 - (day % 5)), + unit: "mmHg", + source: "MANUAL", + measuredAt: daysAgo(day, 8), + }, + ); + } + for (const hour of [8, 13, 19]) { + rows.push({ + userId, + type: "PULSE", + value: Math.round(66 + wave * 5 + (hour === 19 ? 12 : 0)), + unit: "bpm", + source: "APPLE_HEALTH", + measuredAt: daysAgo(day, hour), + }); + } + rows.push({ + userId, + type: "RESTING_HEART_RATE", + value: Math.round(58 + wave * 3 + (day % 4)), + unit: "bpm", + source: "APPLE_HEALTH", + measuredAt: daysAgo(day, 5), + }); + if (day % 4 === 0) { + rows.push({ + userId, + type: "BLOOD_GLUCOSE", + value: Math.round(96 + wave * 9 + (day % 6)), + unit: "mg/dL", + source: "MANUAL", + measuredAt: daysAgo(day, 7), + }); + } + if (day > 0 && day < 90) { + rows.push({ + userId, + type: "SLEEP_DURATION", + value: Math.round(410 + wave * 35), + unit: "min", + source: "APPLE_HEALTH", + measuredAt: daysAgo(day, 5), + sleepStage: "ASLEEP", + }); + } + } + await prisma.measurement.createMany({ data: rows }); +} + +async function seedMood(userId: string): Promise { + const prisma = getPrismaClient(); + const binary = await prisma.moodTag.findFirst({ + where: { kind: "BINARY", userId: null }, + orderBy: { key: "asc" }, + }); + const rated = await prisma.moodTag.findFirst({ + where: { kind: "RATED", userId: null }, + orderBy: { key: "asc" }, + }); + const labels = ["LAUSIG", "SCHLECHT", "OKAY", "GUT", "SUPER_GUT"]; + for (let day = 0; day < 70; day++) { + const score = 1 + ((day * 7 + Math.floor(day / 3)) % 5); + const at = daysAgo(day, day % 2 === 0 ? 20 : 9); + const links = [ + ...(binary && day % 3 === 0 ? [{ moodTagId: binary.id }] : []), + ...(rated && day % 2 === 0 + ? [{ moodTagId: rated.id, rating: 1 + (day % 5) }] + : []), + ]; + await prisma.moodEntry.create({ + data: { + id: `${userId}-mood-${day}`, + userId, + date: at.toISOString().slice(0, 10), + mood: labels[score - 1], + score, + tags: + day % 4 === 0 + ? JSON.stringify(["work"]) + : day % 5 === 0 + ? JSON.stringify(["sport"]) + : null, + tz: "Europe/Berlin", + moodLoggedAt: at, + ...(links.length > 0 ? { tagLinks: { create: links } } : {}), + }, + }); + } +} + +async function seedMedications(userId: string): Promise { + const prisma = getPrismaClient(); + const createdAt = daysAgo(120, 9); + const medications = [ + { id: `${userId}-med-bp`, name: "Ramipril", dose: "5mg", bp: true }, + { id: `${userId}-med-d`, name: "Vitamin D", dose: "1000 IE", bp: false }, + ]; + for (const medication of medications) { + await prisma.medication.create({ + data: { + id: medication.id, + userId, + name: medication.name, + dose: medication.dose, + createdAt, + schedules: { + create: [ + { + id: `${medication.id}-schedule`, + windowStart: "08:00", + windowEnd: "10:00", + }, + ], + }, + }, + }); + if (medication.bp) { + await prisma.medicationCategoryAssignment.create({ + data: { medicationId: medication.id, category: "BLOOD_PRESSURE" }, + }); + } + const events = []; + for (let day = 1; day < 100; day++) { + if (day % 9 === 4) continue; + const scheduledFor = daysAgo(day, 8); + events.push({ + id: `${medication.id}-intake-${day}`, + userId, + medicationId: medication.id, + scheduledFor, + skipped: day % 11 === 3, + takenAt: + day % 11 === 3 + ? null + : new Date(scheduledFor.getTime() + 20 * 60_000), + createdAt: scheduledFor, + }); + } + await prisma.medicationIntakeEvent.createMany({ data: events }); + } +} + +async function seedBiomarkers(userId: string): Promise { + const prisma = getPrismaClient(); + await prisma.biomarker.create({ + data: { + id: `${userId}-${BIOMARKER}`, + userId, + name: "LDL Cholesterol", + unit: "mg/dL", + lowerBound: 0, + upperBound: 116, + createdAt: daysAgo(400), + }, + }); + await prisma.biomarker.create({ + data: { + id: `${userId}-${EMPTY_BIOMARKER}`, + userId, + name: "Ferritin", + unit: "ng/mL", + lowerBound: 30, + upperBound: 400, + createdAt: daysAgo(400), + }, + }); + const readings = [ + { day: 360, value: 142 }, + { day: 250, value: 131 }, + { day: 130, value: 118 }, + { day: 20, value: 104, source: { low: 0, high: 130, text: "< 130" } }, + ]; + for (const [index, reading] of readings.entries()) { + await prisma.labResult.create({ + data: { + id: `${userId}-lab-${index}`, + userId, + biomarkerId: `${userId}-${BIOMARKER}`, + analyte: "LDL", + value: reading.value, + unit: "mg/dL", + referenceLow: 0, + referenceHigh: 116, + takenAt: daysAgo(reading.day, 9), + ...(reading.source + ? { + sourceReferenceLow: reading.source.low, + sourceReferenceHigh: reading.source.high, + sourceReferenceText: reading.source.text, + } + : {}), + }, + }); + } +} + +async function seedWorld(): Promise { + await makeAccount(RICH, { withProvider: true, profile: "rich" }); + await makeAccount(BARE, { withProvider: true, profile: "none" }); + await makeAccount(KEYLESS, { withProvider: false, profile: "plain" }); + for (const userId of [RICH, KEYLESS]) { + await seedMeasurements(userId); + await seedMood(userId); + await seedMedications(userId); + await seedBiomarkers(userId); + } +} + +// ── Recording ──────────────────────────────────────────────────────────────── + +async function storedNotes(): Promise { + const rows = await getPrismaClient().insightStatusCache.findMany({ + orderBy: [{ userId: "asc" }, { metric: "asc" }, { locale: "asc" }], + }); + return rows.map((row) => ({ + userId: row.userId, + metric: row.metric, + locale: row.locale, + text: row.textEncrypted ? decryptFromBytes(row.textEncrypted) : null, + items: row.itemsEncrypted + ? (JSON.parse(decryptFromBytes(row.itemsEncrypted)) as unknown) + : null, + inputHash: row.inputHash, + snapshotHash: row.snapshotHash, + dateKey: row.dateKey, + generatedAt: row.generatedAt?.toISOString() ?? null, + retryAt: row.retryAt?.toISOString() ?? null, + negativeReason: row.negativeReason, + })); +} + +interface Arm { + /** Days after the frozen start the arm runs at. */ + day?: number; + setup?: () => void; + run: () => Promise; +} + +async function record(arms: Record): Promise { + const out: Record = {}; + for (const [name, arm] of Object.entries(arms)) { + vi.setSystemTime(new Date(NOW.getTime() + (arm.day ?? 0) * DAY_MS)); + model.calls = []; + queue.enqueued = []; + answerSummary(`Parity note for ${name}.`); + arm.setup?.(); + const result = await arm.run(); + await settleBackgroundTasks(); + await Promise.allSettled([...pendingWrites]); + pendingWrites.clear(); + out[name] = { + // The batch runs its cards concurrently, so the order the requests + // arrive in is not part of the contract; their content is. + calls: readable( + [...model.calls].sort((a, b) => + JSON.stringify(a).localeCompare(JSON.stringify(b)), + ), + ), + enqueued: queue.enqueued, + result, + notes: await storedNotes(), + }; + } + return `${JSON.stringify(out, null, 2)}\n`; +} + +function compareGolden(name: string, actual: string): void { + const path = resolve(GOLDEN_DIR, `${name}.json`); + if (WRITE) { + mkdirSync(GOLDEN_DIR, { recursive: true }); + writeFileSync(path, actual); + return; + } + expect(actual).toBe(readFileSync(path, "utf8")); +} + +// ── The cards ──────────────────────────────────────────────────────────────── + +type Options = { locale?: string | null; force?: boolean; readOnly?: boolean }; +type Entry = (userId: string, options?: Options) => Promise; + +/** + * The arms every standard card runs, in order. Each arm sees the notes the + * arms before it stored, which is what makes the cache, unchanged-data and + * next-day arms meaningful. + */ +function standardArms(entry: Entry): Record { + return { + "read-only-cold": { + run: () => entry(RICH, { locale: "en", readOnly: true }), + }, + "fresh-en": { run: () => entry(RICH, { locale: "en" }) }, + "cached-en": { run: () => entry(RICH, { locale: "en" }) }, + "forced-unchanged-en": { + run: () => entry(RICH, { locale: "en", force: true }), + }, + "next-day-en": { day: 1, run: () => entry(RICH, { locale: "en" }) }, + "read-only-stale": { + day: 3, + run: () => entry(RICH, { locale: "en", readOnly: true }), + }, + "prose-de": { + setup: () => answer("Das ist kein JSON."), + run: () => entry(RICH, { locale: "de" }), + }, + "unparseable-fr": { + setup: () => answer('{"summary": "Votre'), + run: () => entry(RICH, { locale: "fr" }), + }, + "blocked-es": { + setup: () => + answerSummary("Aumente su dosis a 2,4 mg la próxima semana."), + run: () => entry(RICH, { locale: "es" }), + }, + "bare-error-en": { + setup: fail, + run: () => entry(BARE, { locale: "en" }), + }, + "bare-error-de": { + setup: fail, + run: () => entry(BARE, { locale: "de", force: true }), + }, + "keyless-en": { run: () => entry(KEYLESS, { locale: "en" }) }, + "keyless-de": { run: () => entry(KEYLESS, { locale: "de" }) }, + "keyless-read-only": { + run: () => entry(KEYLESS, { locale: "en", readOnly: true }), + }, + }; +} + +beforeEach(async () => { + await truncateAllTables(getPrismaClient()); + vi.useFakeTimers({ toFake: ["Date"], now: NOW }); + await seedWorld(); +}); + +afterEach(() => { + vi.useRealTimers(); +}); + +describe("status generator output parity", () => { + it( + "pulse", + async () => { + const { generatePulseStatusForUser } = + await import("@/lib/insights/pulse-status"); + compareGolden( + "pulse", + await record(standardArms(generatePulseStatusForUser)), + ); + }, + SLOW, + ); + + it( + "weight", + async () => { + const { generateWeightStatusForUser } = + await import("@/lib/insights/weight-status"); + compareGolden( + "weight", + await record(standardArms(generateWeightStatusForUser)), + ); + }, + SLOW, + ); + + it( + "blood-pressure", + async () => { + const { generateBloodPressureStatusForUser } = + await import("@/lib/insights/blood-pressure-status"); + compareGolden( + "blood-pressure", + await record(standardArms(generateBloodPressureStatusForUser)), + ); + }, + SLOW, + ); + + it( + "mood", + async () => { + const { generateMoodStatusForUser } = + await import("@/lib/insights/mood-status"); + compareGolden( + "mood", + await record(standardArms(generateMoodStatusForUser)), + ); + }, + SLOW, + ); + + it( + "bmi", + async () => { + const { generateBmiStatusForUser } = + await import("@/lib/insights/bmi-status"); + compareGolden( + "bmi", + await record(standardArms(generateBmiStatusForUser)), + ); + }, + SLOW, + ); + + it( + "medication-compliance", + async () => { + const { generateMedicationComplianceStatusForUser } = + await import("@/lib/insights/medication-compliance-status"); + compareGolden( + "medication-compliance", + await record(standardArms(generateMedicationComplianceStatusForUser)), + ); + }, + SLOW, + ); + + it( + "general", + async () => { + const { generateGeneralStatusForUser } = + await import("@/lib/insights/general-status"); + compareGolden( + "general", + await record(standardArms(generateGeneralStatusForUser)), + ); + }, + SLOW, + ); + + it( + "metric", + async () => { + const { generateMetricStatus } = + await import("@/lib/insights/metric-status"); + const arms: Record = {}; + for (const metric of [ + "RESTING_HEART_RATE", + "BLOOD_GLUCOSE", + "SLEEP_DURATION", + ] as const) { + const entry: Entry = (userId, options) => + generateMetricStatus({ metric, userId, ...options }); + for (const [name, arm] of Object.entries(standardArms(entry))) { + arms[`${metric} ${name}`] = arm; + } + } + arms["VO2_MAX no-data"] = { + run: () => + generateMetricStatus({ + metric: "VO2_MAX", + userId: RICH, + locale: "en", + }), + }; + compareGolden("metric", await record(arms)); + }, + SLOW, + ); + + it( + "biomarker", + async () => { + const { generateBiomarkerStatus } = + await import("@/lib/insights/biomarker-status"); + const arms: Record = {}; + const entry: Entry = (userId, options) => + generateBiomarkerStatus({ + biomarkerId: `${userId}-${BIOMARKER}`, + userId, + ...options, + }); + for (const [name, arm] of Object.entries(standardArms(entry))) { + arms[name] = arm; + } + arms["no-readings"] = { + run: () => + generateBiomarkerStatus({ + biomarkerId: `${RICH}-${EMPTY_BIOMARKER}`, + userId: RICH, + locale: "en", + }), + }; + arms["foreign-marker"] = { + run: () => + generateBiomarkerStatus({ + biomarkerId: `${KEYLESS}-${BIOMARKER}`, + userId: RICH, + locale: "en", + }), + }; + compareGolden("biomarker", await record(arms)); + }, + SLOW, + ); + + it( + "batch", + async () => { + const { generateStatusBatchForUser } = + await import("@/lib/insights/status-batch"); + const { STATUS_BATCH_KEY_BY_METRIC } = + await import("@/lib/ai/prompts/status-batch"); + const perMetric = Object.fromEntries( + Object.values(STATUS_BATCH_KEY_BY_METRIC) + .filter((key) => key !== "mood") + .map((key) => [key, `Batched parity note for ${key}.`]), + ); + compareGolden( + "batch", + await record({ + "batch-en": { + setup: () => answer(JSON.stringify({ perMetric })), + run: () => + generateStatusBatchForUser(RICH, { locale: "en", force: true }), + }, + "batch-unchanged-en": { + run: () => + generateStatusBatchForUser(RICH, { locale: "en", force: true }), + }, + "batch-failed-de": { + setup: fail, + run: () => + generateStatusBatchForUser(RICH, { locale: "de", force: true }), + }, + }), + ); + }, + SLOW, + ); +});