chore(cleanup): retire orphan exports and the stale legacy insights prompt module

Drop fourteen exported helpers and constants with zero call sites:
countMessages, buildGeneratePrompts, _resetSafetyContractsCacheForTests,
forEachLocaleAndRule, getPulseRange, trendLinePoints,
CHART_MINI_HEIGHT_PX, formatDateWithWeekday,
resolveGeneralStatusLocale, isSubPageSlug, recordHeartbeat,
EVENT_TYPE_LABELS, cleanupExpiredRateLimits, and sleepStageEnum.

Also retire the v1.4.20 legacy insights prompt scaffold —
getInsightsSystemPrompt, InsightsOutput, and the two dead
INSIGHTS_SYSTEM_PROMPT_EN/DE constants — plus the matching vi.mock
key in the generate route test that no longer maps to a live export.
Drop the empty __testables block in glp1-snapshot.ts, the legacy
helper reference in the insight-generator JSDoc, and the two stale
v1.4.16 InsightsPageHero comments in hero-strip + its test file.

Consume the MedicationComplianceChart compareBaseline prop via an
explicit void so the destructure documents the intentional accept-and-
defer pattern instead of looking like a missed wire-up.

Every symbol verified zero-caller via grep across src/, scripts/, and
__tests__/ before drop.
This commit is contained in:
Marc-André Bombeck committed 2026-05-15 21:16:35 +02:00
1 parent 0baea139a3
commit 267868c013
20 files changed
+15 -264

No files matched your search

@@ -77,7 +77,6 @@ vi.mock("@/lib/insights/features", () => ({
}));
vi.mock("@/lib/insights/prompt", () => ({
getInsightsSystemPrompt: vi.fn(() => "system"),
buildUserPrompt: vi.fn(() => "user"),
}));
@@ -191,7 +191,15 @@ interface MedicationComplianceChartProps {
export function MedicationComplianceChart({
title,
userTimezone = "Europe/Berlin",
compareBaseline,
}: MedicationComplianceChartProps) {
// v1.4.27 R4 RC3 — the prop is accepted so the dashboard can pass
// `compareBaseline={compareBaseline}` uniformly across every chart.
// The comparison overlay itself is deferred: medication compliance is
// a percentage-of-doses metric whose prior-period overlay needs a
// second window of intake events. Discard explicitly so the
// destructure is intentional rather than a missed wire-up.
void compareBaseline;
const { isAuthenticated } = useAuth();
const { t, locale } = useTranslations();
const fmt = useFormatters();
@@ -5,11 +5,9 @@ import { HeroStrip } from "../hero-strip";
import type { DailyBriefing as DailyBriefingPayload } from "@/lib/ai/schema";
/**
* v1.4.20 phase B1 — Insights hero strip.
*
* Replaces v1.4.16 `<InsightsPageHero>`. Pinning the slots so future
* polish can't silently drop the greeting / subtitle / action row /
* suggested-prompt strip.
* Insights hero strip — pins the slots so future polish can't
* silently drop the greeting / subtitle / action row / suggested-
* prompt strip.
*/
function render(node: React.ReactNode, locale: "en" | "de" = "en") {
+3 -3
View File
@@ -15,10 +15,10 @@ import {
import type { DailyBriefing as DailyBriefingPayload } from "@/lib/ai/schema";
/**
* v1.4.20 phase B1 — Insights redesign hero strip.
* Insights redesign hero strip.
*
* Replaces the v1.4.16 `<InsightsPageHero>` with the wider band from
* the design handoff (`prototype/artboard-fullpage.jsx → BriefingHero`):
* The wider band lifted from the design handoff
* (`prototype/artboard-fullpage.jsx → BriefingHero`):
*
* - locale-aware time-of-day greeting line ("Good morning, …" /
* "Guten Morgen, …") + a narrative subtitle (the briefing
-7
View File
@@ -410,10 +410,3 @@ export async function buildGlp1SnapshotBlock(
};
}
/** Exported for tests. */
export const __testables = {
deriveDrugNames,
parseTagList,
parseDaysOfWeek,
predictNextInjection,
};
-7
View File
@@ -317,10 +317,3 @@ export async function deleteConversation(
return true;
}
/**
* Count assistant + user turns in one conversation. Used by the
* 20-turn cap before a summarise-and-restart pass.
*/
export async function countMessages(conversationId: string): Promise<number> {
return prisma.coachMessage.count({ where: { conversationId } });
}
-38
View File
@@ -72,41 +72,3 @@ Fokussiere auf das Zusammenspiel der verschiedenen Parameter und identifiziere d
${snapshotJson}`;
}
// Backward-compatible export for insights/generate route (Task 8 uses this)
export function buildGeneratePrompts(
featuresJson: string,
privacyMode: string,
locale: Locale,
): { systemPrompt: string; userPrompt: string } {
if (locale === "en") {
const modeLabel =
privacyMode === "raw"
? "Aggregated data + raw values (entire available period, anonymised)"
: "Aggregated data only (no exact timestamps or raw values)";
return {
systemPrompt: getGeneralStatusSystemPrompt(locale),
userPrompt: `Analyse the following health data.
Data mode: ${modeLabel}
Use each metric's coverage object for measurement frequency and time spans.
Use correlations for cross-metric analysis and historicalComparison for temporal context.
${featuresJson}`,
};
}
const modeLabel =
privacyMode === "raw"
? "Aggregierte Daten + Rohdaten (gesamter verfügbarer Zeitraum, anonymisiert)"
: "Nur aggregierte Daten (keine exakten Zeitstempel oder Rohwerte)";
return {
systemPrompt: getGeneralStatusSystemPrompt(locale),
userPrompt: `Analysiere die folgenden Gesundheitsdaten.
Datenmodus: ${modeLabel}
Beachte die coverage-Objekte jeder Metrik für Informationen zu Messhäufigkeit und Zeiträumen.
Nutze die correlations-Daten für Kreuzanalysen und historicalComparison für temporale Einordnung.
${featuresJson}`,
};
}
+1 -2
View File
@@ -714,8 +714,7 @@ const INSIGHTS_LOCALE_REPLY_FOOTER_FALLBACK: Record<
/**
* Returns the active scope-hardened system prompt for a given locale.
* Use this in place of the legacy `getInsightsSystemPrompt` once the
* route migrates to `generateInsight()` (planned v1.4.16).
* The route consumes this via `generateInsight()`.
*/
export function getStrictInsightsSystemPrompt(locale: Locale): string {
if (locale === "de") return SYSTEM_PROMPT_DE;
-20
View File
@@ -187,11 +187,6 @@ export function loadSafetyContracts(locale: Locale): SafetyContractMatrix {
return data;
}
/** Clear the in-process cache. Used in tests that swap matrices. */
export function _resetSafetyContractsCacheForTests(): void {
cache = {};
}
/**
* Return the locale-specific body of a ground rule. For EN the matrix
* stores it under `.en`; for every other locale it lives under
@@ -213,20 +208,5 @@ export function getGroundRuleBody(
return body;
}
/**
* Iterate every (locale, ground-rule-key) pair. Used by the parity +
* refusal-probe tests so they cover the full 14×6 = 84 grid without
* hard-coding the locale list.
*/
export function forEachLocaleAndRule(
fn: (locale: Locale, key: GroundRuleKey) => void,
): void {
for (const locale of ALL_LOCALES) {
for (const key of GROUND_RULE_KEYS) {
fn(locale, key);
}
}
}
/** Ordered locale list — exported so tests can iterate identically. */
export const SAFETY_CONTRACT_LOCALES = ALL_LOCALES;
-7
View File
@@ -305,13 +305,6 @@ export function getWeightRange(heightCm: number): { min: number; max: number } {
};
}
/**
* Normal resting pulse range (AHA).
*/
export function getPulseRange(): { min: number; max: number } {
return { min: 60, max: 100 };
}
/**
* Recommended nightly sleep duration for adults (AASM/SRS).
*/
-51
View File
@@ -104,57 +104,6 @@ export function trendSlope(
};
}
// ── Trend Line Points ────────────────────────────────────
/**
* Generate two data points representing the linear trend line endpoints.
* Used by chart components to overlay a regression line.
*/
export function trendLinePoints(
data: DataPoint[],
windowDays: number,
): { start: DataPoint; end: DataPoint } | null {
if (data.length < 2) return null;
const sorted = [...data].sort((a, b) => a.date.getTime() - b.date.getTime());
const cutoff =
sorted[sorted.length - 1].date.getTime() - windowDays * 24 * 60 * 60 * 1000;
const window = sorted.filter((p) => p.date.getTime() >= cutoff);
if (window.length < 2) return null;
const startTime = window[0].date.getTime();
const points = window.map((p) => ({
x: (p.date.getTime() - startTime) / (24 * 60 * 60 * 1000),
y: p.value,
}));
const n = points.length;
const sumX = points.reduce((s, p) => s + p.x, 0);
const sumY = points.reduce((s, p) => s + p.y, 0);
const sumXY = points.reduce((s, p) => s + p.x * p.y, 0);
const sumXX = points.reduce((s, p) => s + p.x * p.x, 0);
const denominator = n * sumXX - sumX * sumX;
if (denominator === 0) return null;
const slope = (n * sumXY - sumX * sumY) / denominator;
const intercept = (sumY - slope * sumX) / n;
const endDays = points[points.length - 1].x;
return {
start: {
date: window[0].date,
value: Math.round(intercept * 100) / 100,
},
end: {
date: window[window.length - 1].date,
value: Math.round((intercept + slope * endDays) * 100) / 100,
},
};
}
// ── Anomaly Detection ────────────────────────────────────
export interface Anomaly {
-7
View File
@@ -16,13 +16,6 @@
/** Full-size trend-card body height (non-mini chart cards on the dashboard). */
export const CHART_HEIGHT_PX = 240;
/**
* Compact / mini variant used by inline previews (rationale cards,
* dashboard tile carve-outs). Smaller so the chart fits inside a host
* card without doubling its vertical footprint.
*/
export const CHART_MINI_HEIGHT_PX = 160;
/**
* Range-preset options for charts that expose a 7d / 30d / 90d / All
* range selector. Each entry carries the i18n keys for the visible
-4
View File
@@ -61,7 +61,3 @@ export function formatTime(date: Date | string): string {
return formatters().time(date);
}
/** Locale-aware short weekday + date. */
export function formatDateWithWeekday(date: Date | string): string {
return formatters().dateWithWeekday(date);
}
-5
View File
@@ -403,8 +403,3 @@ export async function generateGeneralStatusForUser(
};
}
export function resolveGeneralStatusLocale(
locale: string | null | undefined,
): SupportedLocale {
return normalizeLocale(locale);
}
-71
View File
@@ -40,77 +40,6 @@ export interface ComparisonSnapshot {
}>;
}
const INSIGHTS_SYSTEM_PROMPT_DE = `Du bist ein Gesundheitsdaten-Insights-Assistent. Du analysierst aggregierte Gesundheitsdaten und gibst klare, datenbasierte Hinweise.
WICHTIGE REGELN:
- Erkläre immer, welche Datenpunkte zu deinen Schlussfolgerungen geführt haben.
- Gib ein Konfidenzniveau an (niedrig/mittel/hoch) basierend auf der Datenmenge UND Datendichte.
- Antworte auf Deutsch. Das Feld "confidence" muss exakt einer der englischen Enum-Schlüssel ("niedrig"|"mittel"|"hoch") sein — nicht übersetzen.
DATENABDECKUNG UND MESSZEITRÄUME:
- Jede Metrik enthält ein "coverage"-Objekt mit: count (Anzahl Messungen), spanDays (Zeitspanne in Tagen), avgDaysBetween (durchschnittlicher Abstand zwischen Messungen), oldestDaysAgo und newestDaysAgo.
- Berücksichtige diese Informationen bei deiner Analyse:
* Weniger als 5 Messpunkte pro Metrik: Sage explizit, dass noch nicht genügend Daten für eine fundierte Aussage vorliegen.
* Große Lücken (avgDaysBetween > 7): Weise darauf hin, dass die Daten spärlich sind und Trends nur eingeschränkt belastbar sind.
* Daten über einen langen Zeitraum (spanDays > 60) aber wenig Messpunkte: Versuche trotzdem eine grobe Entwicklung abzuleiten, weise aber auf die eingeschränkte Aussagekraft hin.
* Wenn die neueste Messung länger als 7 Tage zurückliegt (newestDaysAgo > 7): Erwähne, dass die Daten nicht aktuell sind.
- context.dataSpanDays zeigt den Gesamtzeitraum aller Messungen, context.oldestMeasurementDaysAgo und context.newestMeasurementDaysAgo den ältesten und neuesten Messpunkt.
AUSGABEFORMAT (JSON):
{
"changed": "Was hat sich verändert? (Bezug auf Zeiträume und Datenbasis)",
"stable": "Was ist stabil geblieben?",
"drivers": "Mögliche Zusammenhänge und Hypothesen (mit Vorsicht formuliert)",
"nextSteps": "Nächste kleine Schritte",
"confidence": "niedrig|mittel|hoch",
"limitations": "Einschränkungen dieser Analyse (inkl. Datenlücken und Messhäufigkeit)"
}
Antworte NUR mit validem JSON im obigen Format. Alle natürlichsprachigen Felder auf Deutsch.`;
const INSIGHTS_SYSTEM_PROMPT_EN = `You are a health-data insights assistant. You analyse aggregated health data and provide clear, data-backed observations.
KEY RULES:
- Always explain which data points drove each conclusion.
- Provide a confidence level (niedrig/mittel/hoch) based on data volume AND density.
- Reply in English. The "confidence" field must contain exactly one of the stable enum keys ("niedrig"|"mittel"|"hoch") — do not translate.
DATA COVERAGE AND MEASUREMENT WINDOWS:
- Each metric carries a "coverage" object with: count (number of measurements), spanDays (timespan in days), avgDaysBetween (mean spacing between measurements), oldestDaysAgo and newestDaysAgo.
- Factor these into the analysis:
* Fewer than 5 points per metric: state explicitly that there is not yet enough data for a reliable statement.
* Large gaps (avgDaysBetween > 7): note that the data is sparse and trends are only weakly supported.
* Long span (spanDays > 60) but few points: still try to derive a coarse direction, but flag the limited reliability.
* If the newest measurement is more than 7 days old (newestDaysAgo > 7): mention that the data is not current.
- context.dataSpanDays shows the overall span of all measurements; context.oldestMeasurementDaysAgo and context.newestMeasurementDaysAgo show the oldest and newest points.
OUTPUT FORMAT (JSON):
{
"changed": "What changed? (Reference time spans and data basis.)",
"stable": "What stayed stable?",
"drivers": "Possible associations and hypotheses (worded cautiously).",
"nextSteps": "Next small steps.",
"confidence": "niedrig|mittel|hoch",
"limitations": "Limitations of this analysis (incl. data gaps and measurement frequency)."
}
Reply with VALID JSON ONLY in the format above. All natural-language fields in English.`;
export function getInsightsSystemPrompt(locale: Locale): string {
return locale === "en"
? INSIGHTS_SYSTEM_PROMPT_EN
: INSIGHTS_SYSTEM_PROMPT_DE;
}
export interface InsightsOutput {
changed: string;
stable: string;
drivers: string;
nextSteps: string;
confidence: "niedrig" | "mittel" | "hoch";
limitations: string;
}
export function buildUserPrompt(
featuresJson: string,
privacyMode: string,
-3
View File
@@ -46,6 +46,3 @@ export const SUB_PAGE_SLUGS = Object.keys(SUB_PAGE_METRIC) as SubPageSlug[];
*/
export const INSIGHTS_OVERVIEW_PATH = "/insights" as const;
export function isSubPageSlug(value: string): value is SubPageSlug {
return (SUB_PAGE_SLUGS as readonly string[]).includes(value);
}
-5
View File
@@ -44,11 +44,6 @@ export function markWorkerStarted() {
status.lastHeartbeat = new Date().toISOString();
}
export function recordHeartbeat() {
const status = getStatus();
status.lastHeartbeat = new Date().toISOString();
}
export function recordReminderCheck() {
const status = getStatus();
status.lastReminderCheck = new Date().toISOString();
-9
View File
@@ -20,15 +20,6 @@ export const EVENT_TYPES = [
] as const;
export type EventType = (typeof EVENT_TYPES)[number];
export const EVENT_TYPE_LABELS: Record<EventType, string> = {
MEDICATION_REMINDER: "Medication reminder",
MEASUREMENT_ANOMALY: "Anomalous measurements",
COMPLIANCE_LOW: "Low compliance",
WITHINGS_SYNC_FAILED: "Withings sync failed",
SYSTEM_ALERT: "System alerts",
PERSONAL_RECORD: "Personal records",
};
/**
* Per-event default policy when no `NotificationPreference` row
* exists. The dispatcher reads this to decide whether to fire on a
-10
View File
@@ -51,16 +51,6 @@ export async function checkRateLimit(
};
}
/**
* Delete expired rate limit entries. Called periodically by pg-boss.
*/
export async function cleanupExpiredRateLimits(): Promise<number> {
const result = await prisma.$executeRaw`
DELETE FROM rate_limits WHERE reset_at < NOW()
`;
return result;
}
/**
* Rate limit response headers.
*/
-9
View File
@@ -41,15 +41,6 @@ export const glucoseContextEnum = z.enum([
"BEDTIME",
]);
export const sleepStageEnum = z.enum([
"IN_BED",
"AWAKE",
"ASLEEP",
"REM",
"CORE",
"DEEP",
]);
export const measurementSourceEnum = z.enum([
"MANUAL",
"WITHINGS",