diff --git a/src/lib/ai/prompts/__tests__/shared-contracts-coverage.test.ts b/src/lib/ai/prompts/__tests__/shared-contracts-coverage.test.ts index 18eda4d66..a03314091 100644 --- a/src/lib/ai/prompts/__tests__/shared-contracts-coverage.test.ts +++ b/src/lib/ai/prompts/__tests__/shared-contracts-coverage.test.ts @@ -13,6 +13,8 @@ import { describe, expect, it } from "vitest"; import { grounding, toneContract, + antiRecitation, + interpretationDepth, safetyGlp1, safetyAcute, metricIdentifierBan, @@ -53,6 +55,10 @@ const SURFACES: Record< contracts: [ grounding, toneContract, + // v1.27.13 (Welle J) — anti-recitation + interpretation-depth reach the + // overview + briefing text too. + antiRecitation, + interpretationDepth, safetyGlp1, safetyAcute, metricIdentifierBan, @@ -69,6 +75,9 @@ const SURFACES: Record< contracts: [ grounding, toneContract, + // v1.27.13 (Welle J) — the assessment cards enforce both new contracts. + antiRecitation, + interpretationDepth, safetyGlp1, safetyAcute, metricIdentifierBan, diff --git a/src/lib/ai/prompts/base-system.ts b/src/lib/ai/prompts/base-system.ts index d3b78e88c..29446c95a 100644 --- a/src/lib/ai/prompts/base-system.ts +++ b/src/lib/ai/prompts/base-system.ts @@ -2,6 +2,8 @@ import type { Locale } from "@/lib/i18n/config"; import { grounding, toneContract, + antiRecitation, + interpretationDepth, safetyGlp1, safetyAcute, metricIdentifierBan, @@ -197,6 +199,18 @@ Synthese statt Aufzählung: die Geschichte dessen, was die Daten bedeuten, zähl // the cross-surface wording so an edit lands here and everywhere at once. { id: "sharedGrounding", en: grounding.en, de: grounding.de }, { id: "sharedTone", en: toneContract.en, de: toneContract.de }, + // v1.27.13 (Welle J) — the anti-recitation + interpretation-depth contracts. + // The assessment surface is exactly where the maintainer's "counts are nice + // but useless" complaint lands, so both contracts are enforced here: don't + // narrate mechanics, and place the value on its guideline scale + judge the + // trend by position (the per-metric INTERPRETATION CONTEXT block carries the + // computed placement). + { id: "sharedAntiRecitation", en: antiRecitation.en, de: antiRecitation.de }, + { + id: "sharedInterpretationDepth", + en: interpretationDepth.en, + de: interpretationDepth.de, + }, // v1.22 (W6) — paragraph formatting contract so a longer assessment renders // as real paragraphs through the shared `ProseBlocks` helper. { diff --git a/src/lib/ai/prompts/insight-generator.ts b/src/lib/ai/prompts/insight-generator.ts index 8ffd561e8..211640416 100644 --- a/src/lib/ai/prompts/insight-generator.ts +++ b/src/lib/ai/prompts/insight-generator.ts @@ -37,6 +37,8 @@ import { import { grounding, toneContract, + antiRecitation, + interpretationDepth, safetyGlp1, safetyAcute, metricIdentifierBan, @@ -841,6 +843,20 @@ neutralem Label + Detail. Höchstgrenze: 20 Einträge.`, // future safety edit is made once in `shared-contracts.ts`. { id: "sharedGrounding", en: grounding.en, de: grounding.de }, { id: "sharedTone", en: toneContract.en, de: toneContract.de }, + // v1.27.13 (Welle J) — the overview + briefing text carried the same + // recitation disease (counts, cadence). Both contracts apply here so the + // summary and the daily briefing interpret rather than enumerate; the + // briefing's own grounding/format machinery is untouched. + { + id: "sharedAntiRecitation", + en: antiRecitation.en, + de: antiRecitation.de, + }, + { + id: "sharedInterpretationDepth", + en: interpretationDepth.en, + de: interpretationDepth.de, + }, { id: "sharedSafetyGlp1", en: safetyGlp1.en, de: safetyGlp1.de }, { id: "sharedSafetyAcute", en: safetyAcute.en, de: safetyAcute.de }, { diff --git a/src/lib/ai/prompts/shared-contracts.ts b/src/lib/ai/prompts/shared-contracts.ts index 5e867e73b..a88e29ffd 100644 --- a/src/lib/ai/prompts/shared-contracts.ts +++ b/src/lib/ai/prompts/shared-contracts.ts @@ -76,6 +76,34 @@ Write in the second person, warm and direct. When the data earns it, name the ge 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.`, }; +/** + * v1.27.13 (Welle J) — anti-recitation contract. The maintainer's complaint, + * verbatim intent: "that I logged it three times, or in what rhythm values were + * taken, is nice but does nothing for me." Measurement counts, logging cadence, + * and page-mechanics narration are NOT insights. They may appear ONLY when they + * carry a consequence — and then as the consequence, not the count. + */ +export const antiRecitation: SharedContract = { + en: `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.`, + de: `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.`, +}; + +/** + * v1.27.13 (Welle J) — interpretation-depth contract. Assessments must + * interpret, not enumerate: place the value on its guideline scale, judge the + * trend BY that position, frame the consequence without diagnosis. Where the + * user prompt carries an INTERPRETATION CONTEXT block, lead from it; where it + * does not, interpret against the person's own baseline and say so honestly. + */ +export const interpretationDepth: SharedContract = { + en: `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.`, + de: `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.`, +}; + /** * GLP-1 dose-prescription safety contract. Previously present ONLY in the * comprehensive + Coach prompts; now enforced on every surface that can name @@ -163,6 +191,8 @@ export const formattingContract: SharedContract = { export const SHARED_CONTRACTS = { grounding, toneContract, + antiRecitation, + interpretationDepth, safetyGlp1, safetyAcute, metricIdentifierBan,