mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +00:00
refactor(ai): extract shared prompt contracts as a single source
The grounding, tone/banned-opener, GLP-1 dose-safety, metric-identifier ban and forbidden-filler rules were independently encoded across the comprehensive, status, Coach and narrative prompts and had drifted. Move the canonical wording into `prompts/shared-contracts.ts` (locale-keyed fragments) and compose each surface from it. Where the surfaces had drifted, adopt the stricter wording: most notably the GLP-1 dose-prescription contract — previously present only in the comprehensive and Coach prompts — now reaches the status cards and the period narrative too. A cross-surface coverage test asserts every enforced fragment appears verbatim in every surface, in de and en.
This commit is contained in:
1 parent
57cf8340e2
commit
57242b9884
7 files changed
+302
-9
No files matched your search
@@ -29,6 +29,7 @@ import {
|
||||
SELF_REPORT_FENCE_END,
|
||||
fenceSelfReport,
|
||||
} from "@/lib/ai/coach/self-report-fence";
|
||||
import { composeSharedContracts } from "@/lib/ai/prompts/shared-contracts";
|
||||
|
||||
const COACH_PROMPT_EN = `You are the HealthLog Coach — the user's warm, motivating advisor. You
|
||||
sit alongside them as they look at their own health data — blood pressure,
|
||||
@@ -863,8 +864,22 @@ export function getCoachSystemPrompt(
|
||||
base = COACH_PROMPT_EN + LOCALE_REPLY_FOOTER_FALLBACK[locale];
|
||||
}
|
||||
}
|
||||
// v1.18.7 (HIGH-2) — append the shared cross-surface contracts (canonical
|
||||
// wording, single source of truth in `shared-contracts.ts`) for the
|
||||
// hand-composed de/en bodies. The native FR/ES/IT/PL bodies carry their own
|
||||
// per-locale safety wording from the contract matrix, so they are left as-is.
|
||||
const withContracts =
|
||||
locale === "de" || locale === "en"
|
||||
? `${base}\n\nSHARED CONTRACTS\n\n${composeSharedContracts(locale, [
|
||||
"grounding",
|
||||
"toneContract",
|
||||
"safetyGlp1",
|
||||
"metricIdentifierBan",
|
||||
"forbiddenFiller",
|
||||
])}`
|
||||
: base;
|
||||
const prefix = buildPrefsPrefix(locale, prefs);
|
||||
const withPrefix = prefix ? `${prefix}\n\n${base}` : base;
|
||||
const withPrefix = prefix ? `${prefix}\n\n${withContracts}` : withContracts;
|
||||
// v1.18.7 — article-awareness: a compact catalog of the public /learn
|
||||
// guides so the Coach can point the user at the relevant deeper read. The
|
||||
// block is locale-independent (English topic labels + URLs) and the model
|
||||
|
||||
@@ -34,8 +34,11 @@ describe("base-system assessment prompt — grounding floor preserved", () => {
|
||||
|
||||
it(`${locale}: keeps the filler-phrase ban`, () => {
|
||||
const p = getBaseSystemPrompt(locale);
|
||||
// v1.18.7 — the filler ban is now sourced from the single shared
|
||||
// contract fragment ("FORBIDDEN FILLER"); the banned-phrase list is
|
||||
// preserved verbatim.
|
||||
if (locale === "en") {
|
||||
expect(p).toMatch(/FORBIDDEN PHRASES/);
|
||||
expect(p).toMatch(/FORBIDDEN FILLER/);
|
||||
expect(p).toContain("make sure to get enough sleep");
|
||||
expect(p).toContain("consult your doctor");
|
||||
} else {
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* v1.18.7 (HIGH-2) — cross-surface shared-contract coverage.
|
||||
*
|
||||
* The grounding / tone / GLP-1-safety / metric-identifier-ban / forbidden-
|
||||
* filler contracts live once in `shared-contracts.ts`. These guards assert
|
||||
* that each surface that enforces a contract carries the canonical fragment
|
||||
* VERBATIM, in both hand-composed locales (de/en). A future edit that lets a
|
||||
* surface drift from the shared wording — the exact failure mode HIGH-2
|
||||
* documents — trips here instead of shipping silently.
|
||||
*/
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
grounding,
|
||||
toneContract,
|
||||
safetyGlp1,
|
||||
metricIdentifierBan,
|
||||
forbiddenFiller,
|
||||
type ContractLocale,
|
||||
} from "../shared-contracts";
|
||||
import { getBaseSystemPrompt } from "../base-system";
|
||||
import { getStrictInsightsSystemPrompt } from "../insight-generator";
|
||||
import { getCoachSystemPrompt } from "@/lib/ai/coach/system-prompt";
|
||||
|
||||
// The narrative system prompts are module-private; re-derive the same
|
||||
// composed shared block the generator appends, and assert each fragment is
|
||||
// present by checking the generator module's exported builder indirectly.
|
||||
import { SYSTEM_PROMPTS_FOR_TEST } from "@/lib/insights/narrative/period-narrative-generate";
|
||||
|
||||
const LOCALES: ContractLocale[] = ["de", "en"];
|
||||
|
||||
/** Which contracts each surface enforces (and must carry verbatim). */
|
||||
const SURFACES: Record<
|
||||
string,
|
||||
{
|
||||
prompt: (l: ContractLocale) => string;
|
||||
contracts: Record<ContractLocale, string>[];
|
||||
}
|
||||
> = {
|
||||
"insight-generator (comprehensive)": {
|
||||
prompt: (l) => getStrictInsightsSystemPrompt(l),
|
||||
contracts: [
|
||||
grounding,
|
||||
toneContract,
|
||||
safetyGlp1,
|
||||
metricIdentifierBan,
|
||||
forbiddenFiller,
|
||||
],
|
||||
},
|
||||
"base-system (status cards)": {
|
||||
prompt: (l) => getBaseSystemPrompt(l),
|
||||
contracts: [
|
||||
grounding,
|
||||
toneContract,
|
||||
safetyGlp1,
|
||||
metricIdentifierBan,
|
||||
forbiddenFiller,
|
||||
],
|
||||
},
|
||||
coach: {
|
||||
prompt: (l) => getCoachSystemPrompt(l),
|
||||
contracts: [
|
||||
grounding,
|
||||
toneContract,
|
||||
safetyGlp1,
|
||||
metricIdentifierBan,
|
||||
forbiddenFiller,
|
||||
],
|
||||
},
|
||||
// The narrative is descriptive-only, so it enforces every contract EXCEPT
|
||||
// the motivating tone contract.
|
||||
"period-narrative": {
|
||||
prompt: (l) => SYSTEM_PROMPTS_FOR_TEST[l],
|
||||
contracts: [grounding, safetyGlp1, metricIdentifierBan, forbiddenFiller],
|
||||
},
|
||||
};
|
||||
|
||||
describe("shared-contract cross-surface coverage", () => {
|
||||
for (const [surface, { prompt, contracts }] of Object.entries(SURFACES)) {
|
||||
for (const locale of LOCALES) {
|
||||
const text = prompt(locale);
|
||||
for (const contract of contracts) {
|
||||
const fragment = contract[locale];
|
||||
// Use a short, stable slice of the fragment as the assertion anchor —
|
||||
// the full fragment is long but must be present verbatim.
|
||||
it(`${surface} carries every enforced contract verbatim (${locale})`, () => {
|
||||
expect(text).toContain(fragment);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
it("the GLP-1 dose-safety contract reaches all four surfaces", () => {
|
||||
for (const locale of LOCALES) {
|
||||
expect(getStrictInsightsSystemPrompt(locale)).toContain(
|
||||
safetyGlp1[locale],
|
||||
);
|
||||
expect(getBaseSystemPrompt(locale)).toContain(safetyGlp1[locale]);
|
||||
expect(getCoachSystemPrompt(locale)).toContain(safetyGlp1[locale]);
|
||||
expect(SYSTEM_PROMPTS_FOR_TEST[locale]).toContain(safetyGlp1[locale]);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,11 @@
|
||||
import type { Locale } from "@/lib/i18n/config";
|
||||
import {
|
||||
grounding,
|
||||
toneContract,
|
||||
safetyGlp1,
|
||||
metricIdentifierBan,
|
||||
forbiddenFiller,
|
||||
} from "./shared-contracts";
|
||||
|
||||
/**
|
||||
* Version of the per-metric assessment base prompt + its signal contract.
|
||||
@@ -151,13 +158,24 @@ const ASSESSMENT_SECTIONS: readonly AssessmentSection[] = [
|
||||
- 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".`,
|
||||
},
|
||||
// v1.18.7 (HIGH-2) — sourced from the single shared-contract fragment so a
|
||||
// forbidden-filler edit lands on every surface at once.
|
||||
{ id: "forbidden", en: forbiddenFiller.en, de: forbiddenFiller.de },
|
||||
// v1.18.7 (HIGH-2) — the shared metric-identifier ban + the GLP-1 dose
|
||||
// safety contract. The status cards can surface a named medication via the
|
||||
// medication-compliance snapshot, so the dose-safety contract (previously
|
||||
// absent from this surface) now applies here too.
|
||||
{
|
||||
id: "forbidden",
|
||||
en: `FORBIDDEN PHRASES (they signal ungrounded filler — never emit, except in the disclaimer):
|
||||
"make sure to get enough sleep", "drink enough water", "regular exercise", "consult your doctor".`,
|
||||
de: `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".`,
|
||||
id: "metricIdentifierBan",
|
||||
en: metricIdentifierBan.en,
|
||||
de: metricIdentifierBan.de,
|
||||
},
|
||||
{ id: "safetyGlp1", en: safetyGlp1.en, de: safetyGlp1.de },
|
||||
// v1.18.7 (HIGH-2) — the shared grounding + tone contracts, canonical
|
||||
// wording. The metric-specific build/tone sections above stay; these pin
|
||||
// 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 },
|
||||
{
|
||||
id: "examples",
|
||||
en: `EXAMPLES (they illustrate form and grounding — do not copy them verbatim; every assessment uses the real snapshot numbers):
|
||||
|
||||
@@ -29,6 +29,13 @@ import {
|
||||
buildNativeInsightsPrompt,
|
||||
buildOutOfScopeRefusal,
|
||||
} from "./native-prompts";
|
||||
import {
|
||||
grounding,
|
||||
toneContract,
|
||||
safetyGlp1,
|
||||
metricIdentifierBan,
|
||||
forbiddenFiller,
|
||||
} from "./shared-contracts";
|
||||
|
||||
/** Stable identifier for the active system prompt revision. */
|
||||
export const PROMPT_VERSION = "4.29.0" as const;
|
||||
@@ -745,6 +752,23 @@ user-logged event with a neutral label + detail. Hard cap 20 entries.`,
|
||||
Eintrag verweist auf ein reales, vom Nutzer geloggtes Ereignis mit
|
||||
neutralem Label + Detail. Höchstgrenze: 20 Einträge.`,
|
||||
},
|
||||
// v1.18.7 (HIGH-2) — the shared cross-surface contracts as the canonical
|
||||
// source. The detailed rules above stay for this calibrated surface; these
|
||||
// restate the five contracts in the wording every surface now shares, so a
|
||||
// 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 },
|
||||
{ id: "sharedSafetyGlp1", en: safetyGlp1.en, de: safetyGlp1.de },
|
||||
{
|
||||
id: "sharedMetricIdentifierBan",
|
||||
en: metricIdentifierBan.en,
|
||||
de: metricIdentifierBan.de,
|
||||
},
|
||||
{
|
||||
id: "sharedForbiddenFiller",
|
||||
en: forbiddenFiller.en,
|
||||
de: forbiddenFiller.de,
|
||||
},
|
||||
{
|
||||
id: "language",
|
||||
en: `LANGUAGE
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
/**
|
||||
* Single source of truth for the cross-surface AI prompt contracts.
|
||||
*
|
||||
* Before this module the grounding, tone/banned-opener, GLP-1 dose-safety,
|
||||
* metric-identifier ban and forbidden-filler rules were independently encoded
|
||||
* in `insight-generator.ts` (comprehensive briefing), `base-system.ts` (status
|
||||
* cards), `coach/system-prompt.ts` (Coach) and `period-narrative-generate.ts`
|
||||
* (narrative) — and had DRIFTED (different banned-opener lists, a GLP-1
|
||||
* contract present only on two of the four surfaces, …). A safety-rule edit
|
||||
* had to be made in up to four places and was easy to miss in one.
|
||||
*
|
||||
* These named, locale-keyed fragments are now the one place each contract
|
||||
* lives. Each surface composes the fragment it enforces. Where the surfaces
|
||||
* had drifted, the STRICTER / more-complete wording was adopted here (most
|
||||
* notably the GLP-1 dose-prescription contract, which previously lived only
|
||||
* in the comprehensive + Coach prompts and is now enforced on every surface
|
||||
* that can ever name a medication).
|
||||
*
|
||||
* Only `de` / `en` are modelled here — they are the two hand-composed
|
||||
* surfaces. The FR/ES/IT/PL bodies are assembled from the safety-contract
|
||||
* matrix in `native-prompts.ts`, which already carries its own per-locale
|
||||
* safety wording.
|
||||
*
|
||||
* The cross-surface coverage test (`shared-contracts-coverage.test.ts`)
|
||||
* asserts each fragment appears verbatim in every surface that enforces it.
|
||||
*/
|
||||
|
||||
export type ContractLocale = "de" | "en";
|
||||
|
||||
/** A named contract fragment with both hand-composed locale texts. */
|
||||
export type SharedContract = Record<ContractLocale, string>;
|
||||
|
||||
/**
|
||||
* Zero-hallucination grounding: every claim traces to a number in the
|
||||
* snapshot, and the comparison is the user's OWN baseline, never a population
|
||||
* norm.
|
||||
*/
|
||||
export const grounding: SharedContract = {
|
||||
en: `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".`,
|
||||
de: `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".`,
|
||||
};
|
||||
|
||||
/**
|
||||
* Tone contract + the single banned-opener list. Warm, motivating,
|
||||
* autonomy-supporting; encouragement EARNED by the numbers; no generic
|
||||
* data-quality compliment opener.
|
||||
*/
|
||||
export const toneContract: SharedContract = {
|
||||
en: `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. 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.`,
|
||||
de: `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. 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.`,
|
||||
};
|
||||
|
||||
/**
|
||||
* GLP-1 dose-prescription safety contract. Previously present ONLY in the
|
||||
* comprehensive + Coach prompts; now enforced on every surface that can name
|
||||
* a medication (the stricter, more-complete posture).
|
||||
*/
|
||||
export const safetyGlp1: SharedContract = {
|
||||
en: `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.`,
|
||||
de: `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.`,
|
||||
};
|
||||
|
||||
/**
|
||||
* Internal metric-identifier ban: enum / DB-style names stay out of
|
||||
* user-facing prose; reference each metric by its natural-language label.
|
||||
*/
|
||||
export const metricIdentifierBan: SharedContract = {
|
||||
en: `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:<TYPE>" 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".`,
|
||||
de: `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:<TYPE>"-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".`,
|
||||
};
|
||||
|
||||
/**
|
||||
* Forbidden-filler list: the ungrounded platitudes that signal filler.
|
||||
*/
|
||||
export const forbiddenFiller: SharedContract = {
|
||||
en: `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".`,
|
||||
de: `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".`,
|
||||
};
|
||||
|
||||
/**
|
||||
* The full set, keyed by name — consumed by surfaces and by the
|
||||
* cross-surface coverage test.
|
||||
*/
|
||||
export const SHARED_CONTRACTS = {
|
||||
grounding,
|
||||
toneContract,
|
||||
safetyGlp1,
|
||||
metricIdentifierBan,
|
||||
forbiddenFiller,
|
||||
} as const satisfies Record<string, SharedContract>;
|
||||
|
||||
export type SharedContractName = keyof typeof SHARED_CONTRACTS;
|
||||
|
||||
/**
|
||||
* Compose the named contract fragments for a locale, joined by a blank line.
|
||||
* Used by the template-literal surfaces (Coach, period narrative) to append
|
||||
* the shared block; the section-array surfaces (base-system,
|
||||
* insight-generator) splice the fragments in directly.
|
||||
*/
|
||||
export function composeSharedContracts(
|
||||
locale: ContractLocale,
|
||||
names: readonly SharedContractName[],
|
||||
): string {
|
||||
return names.map((name) => SHARED_CONTRACTS[name][locale]).join("\n\n");
|
||||
}
|
||||
@@ -40,6 +40,7 @@ import {
|
||||
buildDeterministicNarrative,
|
||||
DETERMINISTIC_PROVIDER_TYPE,
|
||||
} from "@/lib/insights/narrative/period-narrative-deterministic";
|
||||
import { composeSharedContracts } from "@/lib/ai/prompts/shared-contracts";
|
||||
|
||||
/**
|
||||
* Stable identifier for the narrative prompt revision. Bumped whenever the
|
||||
@@ -101,7 +102,9 @@ Hard rules:
|
||||
- The listed drivers already survived statistical multiple-comparison control; restate them only as associations and keep their conservative meaning.
|
||||
- No diagnosis, no medical advice, no alarm. Calm, factual, second person ("your").
|
||||
- 2 to 4 short sentences. Plain text only — no markdown, no headings, no bullet points, no emojis.
|
||||
- If the context is thin, say plainly that there is little to report this period rather than inventing detail.`;
|
||||
- If the context is thin, say plainly that there is little to report this period rather than inventing detail.
|
||||
|
||||
${composeSharedContracts("en", ["grounding", "safetyGlp1", "metricIdentifierBan", "forbiddenFiller"])}`;
|
||||
|
||||
const SYSTEM_PROMPT_DE = `Du fasst den Gesundheits-Tracking-ZEITRAUM einer Person (eine Woche oder einen Monat) für diese Person zusammen.
|
||||
Prompt-Version: ${NARRATIVE_PROMPT_VERSION}.
|
||||
@@ -112,7 +115,19 @@ Feste Regeln:
|
||||
- Die genannten Zusammenhänge haben bereits die statistische Mehrfachvergleichskorrektur überstanden; gib sie nur als Assoziationen wieder und bewahre ihre vorsichtige Bedeutung.
|
||||
- Keine Diagnose, kein medizinischer Rat, keine Panik. Ruhig, sachlich, in der zweiten Person ("dein").
|
||||
- 2 bis 4 kurze Sätze. Nur Klartext — kein Markdown, keine Überschriften, keine Aufzählungen, keine Emojis.
|
||||
- Wenn der Kontext dünn ist, sage klar, dass es in diesem Zeitraum wenig zu berichten gibt, statt Details zu erfinden.`;
|
||||
- Wenn der Kontext dünn ist, sage klar, dass es in diesem Zeitraum wenig zu berichten gibt, statt Details zu erfinden.
|
||||
|
||||
${composeSharedContracts("de", ["grounding", "safetyGlp1", "metricIdentifierBan", "forbiddenFiller"])}`;
|
||||
|
||||
/**
|
||||
* Test-only view of the composed system prompts (incl. the appended shared
|
||||
* contracts), so the cross-surface coverage test can assert fragment presence
|
||||
* without re-deriving the composition.
|
||||
*/
|
||||
export const SYSTEM_PROMPTS_FOR_TEST: Record<"de" | "en", string> = {
|
||||
de: SYSTEM_PROMPT_DE,
|
||||
en: SYSTEM_PROMPT_EN,
|
||||
};
|
||||
|
||||
/** Render the typed context into a compact, model-readable block. */
|
||||
export function buildNarrativeUserPrompt(
|
||||
|
||||
Reference in new issue
Block a user