Merge the separator sweep into the day and Coach polish

This commit is contained in:
Marc-André Bombeck committed 2026-10-09 13:44:58 +02:00
commit da92dabb55
39 files changed
+1027 -881

No files matched your search

@@ -0,0 +1,76 @@
import { describe, it, expect } from "vitest";
import { readFileSync, readdirSync } from "node:fs";
import { join } from "node:path";
/**
* No middle-dot separator in visible strings.
*
* A value like "Added {date} · {size}" or "Weight · body fat · muscle" reads
* as a list of fragments rather than as the app's plain voice. Facts are
* joined with a comma, a colon or a real phrase instead. This guard walks
* every bundle under `messages/` and fails on any string that contains a
* space, U+00B7 and a space. A middle dot inside a unit ("mL/(kg·min)") has
* no surrounding spaces and is not matched.
*
* A key that genuinely needs the pattern goes into `ALLOWED` with the reason
* written next to it. An allowed key that no longer contains the pattern
* fails too, so the list cannot go stale.
*/
const MESSAGES_DIR = join(__dirname, "../../messages");
const SEPARATOR = " · ";
/** Key path -> why the separator is right there. Empty by intent. */
const ALLOWED: Record<string, string> = {};
function flatten(
obj: unknown,
prefix: string,
out: Map<string, string>,
): Map<string, string> {
if (obj == null || typeof obj !== "object") return out;
for (const [k, v] of Object.entries(obj as Record<string, unknown>)) {
const key = prefix ? `${prefix}.${k}` : k;
if (typeof v === "string") out.set(key, v);
else if (typeof v === "object") flatten(v, key, out);
}
return out;
}
const bundles = readdirSync(MESSAGES_DIR)
.filter((file) => file.endsWith(".json"))
.sort();
describe("message bundles carry no ' · ' separator", () => {
it("finds the bundles", () => {
expect(bundles.length).toBeGreaterThanOrEqual(7);
});
for (const file of bundles) {
const values = flatten(
JSON.parse(readFileSync(join(MESSAGES_DIR, file), "utf8")),
"",
new Map(),
);
it(`${file} walks a non-empty set of strings`, () => {
expect(values.size).toBeGreaterThan(1000);
});
it(`${file} has no dot separator outside the allowlist`, () => {
const offenders = [...values]
.filter(
([key, value]) => value.includes(SEPARATOR) && !(key in ALLOWED),
)
.map(([key, value]) => `${key} = ${JSON.stringify(value)}`);
expect(offenders).toEqual([]);
});
it(`${file} has no stale allowlist entry`, () => {
const stale = Object.keys(ALLOWED).filter(
(key) => !(values.get(key) ?? "").includes(SEPARATOR),
);
expect(stale).toEqual([]);
});
}
});
@@ -154,7 +154,7 @@ describe("<BodySitesSection>", () => {
});
expect(html).toContain("Procedures and visits");
expect(html).toContain("Arthroscopy");
expect(html).toContain("Knee · Left");
expect(html).toContain("Knee (Left)");
expect(html).toContain('href="/documents?doc=d1"');
expect(html).toContain("MRI knee report");
expect(html).toContain("Conditions");
@@ -116,7 +116,7 @@ describe("BD-Zielbereich tile (v1.4.28 FB-C1/C2 rewrite)", () => {
it("renders the empty-hint placeholder instead of a percentage on thin data (v1.17 W1b)", () => {
const html = render(
<TrendCard
label="BP in Target · 23d"
label="BP in Target (23d)"
latest={null}
emptyHint="collecting data"
unit="%"
@@ -130,7 +130,7 @@ describe("BD-Zielbereich tile (v1.4.28 FB-C1/C2 rewrite)", () => {
expect(html).toContain('data-slot="trend-card-empty-hint"');
expect(html).toContain("collecting data");
// The dynamic span label is shown in place of the static "· 90 T".
expect(html).toContain("BP in Target · 23d");
expect(html).toContain("BP in Target (23d)");
// No numeric headline value renders when the hint takes the slot.
expect(html).not.toContain('data-slot="trend-card-value"');
});
@@ -188,8 +188,12 @@ describe("<DayView>", () => {
const html = await render();
expect(html).toContain("Running that day");
expect(html).toContain("Common cold");
expect(html).toContain("Day 4 · since");
expect(html).toContain("Winter course · Day 64 of 151");
// Name first, the quiet day count below; an open span names its start
// as a date, its year left out when it is the viewed day's year.
expect(html).toContain("Day 4, since December 31");
expect(html).toContain("Vitamin D 1,000 IU Winter course");
expect(html).toContain("Day 64 of 151");
expect(html).not.toContain("ongoing");
expect(html).toContain('href="/illness/ep1"');
});
@@ -228,10 +232,11 @@ describe("<DayView>", () => {
const html = await render();
const rows = html.split('data-slot="day-running-item"').slice(1);
const med = rows.find((row) => row.includes("Levothyroxine")) ?? "";
expect(med).toContain("50 µg");
expect(med).toContain("Levothyroxine 50 µg");
expect(med.split("</li>")[0]).not.toMatch(/Day \d|since/);
const leave = rows.find((row) => row.includes("Parental leave")) ?? "";
expect(leave).toContain("Family · Day 34 of 90");
expect(leave).toMatch(/data-slot="tag-chip"[^>]*>Family</);
expect(leave).toContain("Day 34 of 90");
expect(leave).not.toContain("FAMILY");
});
+58 -19
View File
@@ -23,8 +23,10 @@ import {
type LucideIcon,
} from "lucide-react";
import { TagChip } from "@/components/ui/tag-chip";
import {
LIFE_EVENT_CATEGORIES,
type DateKey,
type DayEvent,
type DayEventKind,
type DayNotable,
@@ -32,6 +34,7 @@ import {
type DayRunningKind,
type DayValue,
} from "@/lib/day/contract";
import { resolveIntlLocale } from "@/lib/format-locale";
import { useFormatters, useTranslations } from "@/lib/i18n/context";
import { cn } from "@/lib/utils";
@@ -138,7 +141,7 @@ export function DayNotableLines({
<Sparkles className="mt-px size-3.5 shrink-0" aria-hidden="true" />
<span>
{withMetric && notable.type
? `${labelFor(tileKeyOf(notable.type))} · `
? `${labelFor(tileKeyOf(notable.type))}: `
: null}
{text(notable)}
</span>
@@ -200,28 +203,59 @@ function dayMeta(
return meta;
}
export function DayRunning({ items }: { items: readonly DayRunningItem[] }) {
const { t } = useTranslations();
const fmt = useFormatters();
/** Running kinds whose `sub` is a dose and reads as part of the name. */
const DOSE_SUB_KINDS: ReadonlySet<DayRunningKind> = new Set([
"medication",
"medicationCourse",
]);
export function DayRunning({
items,
date,
}: {
items: readonly DayRunningItem[];
/** The viewed day; a start date in another year names its year. */
date?: DateKey;
}) {
const { t, locale } = useTranslations();
if (items.length === 0) return null;
const viewedYear = date
? date.slice(0, 4)
: String(new Date().getUTCFullYear());
const intlLocale = resolveIntlLocale(locale);
const sinceText = (since: string): string =>
new Intl.DateTimeFormat(intlLocale, {
day: "numeric",
month: "long",
...(since.slice(0, 4) === viewedYear ? {} : { year: "numeric" }),
timeZone: "UTC",
}).format(new Date(`${since}T12:00:00.000Z`));
return (
<section className="space-y-2" data-slot="day-running">
<h3 className={DAY_SECTION_LABEL}>{t("day.groups.running")}</h3>
<ul>
{items.map((item) => {
const parts = [
item.sub === null ? null : dayMeta(item.kind, item.sub, t),
const title = dayTitle(item.kind, item.title, t);
const doseInName = item.sub !== null && DOSE_SUB_KINDS.has(item.kind);
const name = doseInName ? `${title} ${item.sub}` : title;
const chip =
item.sub !== null && !doseInName
? dayMeta(item.kind, item.sub, t)
: null;
const since =
item.dayCount === null && item.since !== null
? sinceText(item.since)
: null;
const detail =
item.dayIndex !== null
? item.dayCount !== null
? t("day.dayOf", { n: item.dayIndex, m: item.dayCount })
: t("day.dayN", { n: item.dayIndex })
: null,
item.dayCount === null && item.since !== null
? t("day.since", {
date: fmt.dateShortSmartCalendar(item.since),
})
: null,
].filter((part): part is string => !!part);
: since !== null
? t("day.dayNSince", { n: item.dayIndex, date: since })
: t("day.dayN", { n: item.dayIndex })
: since !== null
? t("day.sinceDate", { date: since })
: null;
const body = (
<>
<span
@@ -230,12 +264,17 @@ export function DayRunning({ items }: { items: readonly DayRunningItem[] }) {
style={{ backgroundColor: RUNNING_COLOR[item.kind] }}
/>
<span className="min-w-0 flex-1">
<span className="block truncate text-sm font-medium">
{dayTitle(item.kind, item.title, t)}
</span>
<span className="text-muted-foreground block truncate text-xs">
{parts.join(" · ")}
<span className="flex min-w-0 items-center gap-2">
<span className="min-w-0 truncate text-sm font-medium">
{name}
</span>
{chip ? <TagChip className="shrink-0">{chip}</TagChip> : null}
</span>
{detail ? (
<span className="text-muted-foreground block truncate text-xs">
{detail}
</span>
) : null}
</span>
{item.href ? (
<ChevronRight
@@ -518,13 +518,13 @@ describe("buildProcedureChoices", () => {
{
encounterId: "enc-knee",
name: "Knee arthroscopy",
detail: "Knee (left) · 2025-10-02",
detail: "Knee (left), 2025-10-02",
},
{ encounterId: "enc-2", name: "Day clinic", detail: "2025-10-02" },
{
encounterId: "enc-3",
name: "Procedure or surgery",
detail: "Knee · 2025-10-02",
detail: "Knee, 2025-10-02",
},
]);
});
+2 -2
View File
@@ -198,8 +198,8 @@ export function DocumentCard({
</DayLinkStated>
) : (
date
)}{" "}
· {size}
)}
, {size}
</p>
{showFilename ? (
// Filename on its OWN muted line so it never clips the date/size
@@ -869,7 +869,7 @@ export function DocumentDetailSheet({
{doc.filename ?? title}
</p>
<p className="text-muted-foreground text-xs">
{t("documents.detail.previewUnavailable")} ·{" "}
{t("documents.detail.previewUnavailable")},{" "}
{formatBytes(doc.byteSize, locale)}
</p>
<DocumentDownloadButton
@@ -1185,8 +1185,15 @@ export function DocumentDetailSheet({
date: format.date(doc.createdAt),
size: formatBytes(doc.byteSize, locale),
})}
{doc.filename ? ` · ${doc.filename}` : ""}
</p>
{doc.filename ? (
<p
className="text-muted-foreground truncate text-xs"
title={doc.filename}
>
{doc.filename}
</p>
) : null}
{/* Where an imported document came from (#1038). Meta, so
muted like the line above; the id is what the person looks
@@ -80,7 +80,7 @@ function factHeadline(fact: ExtractedFactDto): string {
return (fact.data as ConditionFactData).label;
}
const data = fact.data as MedicationStatementFactData;
return data.dose ? `${data.name} · ${data.dose}` : data.name;
return data.dose ? `${data.name} ${data.dose}` : data.name;
}
/** The date the fact states, if any (YYYY-MM-DD). */
@@ -262,18 +262,16 @@ function FactRow({
</p>
<p className="text-muted-foreground text-xs">
{typeLabel}
{date ? ` · ${calendarDate(date)}` : ""}
{isObservation &&
(fact.data as ObservationFactData).referenceText ? (
<>
{" · "}
{t("documents.review.statedRange", {
range: (fact.data as ObservationFactData)
.referenceText as string,
})}
</>
) : null}
{date ? `, ${calendarDate(date)}` : ""}
</p>
{isObservation && (fact.data as ObservationFactData).referenceText ? (
<p className="text-muted-foreground text-xs break-words">
{t("documents.review.statedRange", {
range: (fact.data as ObservationFactData)
.referenceText as string,
})}
</p>
) : null}
{fact.provenance.anchored && fact.provenance.sourceText ? (
<p className="text-muted-foreground text-xs break-words italic">
“{fact.provenance.sourceText}”
+1 -1
View File
@@ -527,7 +527,7 @@ export function DocumentsView() {
label: `${
visit.practitioner?.name ??
encounterKindText(t, visit.kind as EncounterKind)
} · ${format.date(visit.occurredAt)}`,
}, ${format.date(visit.occurredAt)}`,
})),
[visits.data, t, format],
);
@@ -588,7 +588,7 @@ export function DocumentSourcePicker({
: null,
]
.filter(Boolean)
.join(" · ");
.join(", ");
return (
<li key={row.sourceId}>
<button
+1 -1
View File
@@ -546,7 +546,7 @@ export function buildProcedureChoices(
procedure.reason?.trim() ||
procedure.practitionerName?.trim() ||
labels.kindName,
detail: site ? `${site} · ${date}` : date,
detail: site ? `${site}, ${date}` : date,
};
});
}
@@ -138,7 +138,7 @@ describe("<ProceduresSection>", () => {
html.indexOf('data-encounter-id="older"'),
);
expect(html).toContain("Meniscus repair");
expect(html).toContain("Knee · Left");
expect(html).toContain("Knee (Left)");
expect(html).toContain("2 of 2");
});
@@ -182,7 +182,7 @@ describe("<ProceduresSection>", () => {
/data-slot="procedures-site-all"[^>]*aria-pressed="true"|aria-pressed="true"[^>]*data-slot="procedures-site-all"/,
);
expect(html).toMatch(
/aria-pressed="false"[^>]*>Knee · Left|data-slot="procedures-site"[^>]*aria-pressed="false"/,
/aria-pressed="false"[^>]*>Knee \(Left\)|data-slot="procedures-site"[^>]*aria-pressed="false"/,
);
});
@@ -38,7 +38,7 @@ const okResult: CorrelationResult = {
pValue: 0.001,
confidenceBand: { low: -0.85, high: -0.39, label: "high" },
interpretation:
"Higher medication compliance lines up with lower systolic readings — a pattern worth watching.",
"Higher medication compliance lines up with lower systolic readings, a pattern worth watching.",
points: [
{ x: 60, y: 140 },
{ x: 70, y: 135 },
@@ -255,7 +255,7 @@ describe("<MessageThread>", () => {
it("folds an older message's provenance into the thought-process row, never a raw key", () => {
const html = render(<MessageThread conversation={baseConversation} />);
expect(html).toContain('data-slot="coach-turn-steps-done"');
expect(html).toContain("Thought process · 1 step");
expect(html).toContain("Thought process (1 step)");
expect(html).not.toContain('data-slot="coach-source-chips"');
expect(html).not.toContain("insights.coach.metric.");
expect(html).not.toContain("n=26");
@@ -479,7 +479,7 @@ describe("<MessageThread>", () => {
expect(html).not.toContain("What I&#x27;m looking at");
expect(html).not.toContain("138 mmHg");
// The areas the answer drew on stay reachable from the row.
expect(html).toContain("Thought process · 1 step");
expect(html).toContain("Thought process (1 step)");
});
it("names the thought-process row in German under 'de'", () => {
@@ -487,7 +487,7 @@ describe("<MessageThread>", () => {
<MessageThread conversation={baseConversation} />,
"de",
);
expect(html).toContain("Denkprozess · 1 Schritt");
expect(html).toContain("Denkprozess (1 Schritt)");
expect(html).not.toContain("Worauf bezieht sich das?");
});
@@ -413,7 +413,7 @@ describe("after the answer", () => {
);
expect(html).toContain('data-state="done"');
expect(html).toMatch(
/data-slot="coach-turn-steps-done"[^>]*>Thought process · 5 steps</,
/data-slot="coach-turn-steps-done"[^>]*>Thought process \(5 steps\)</,
);
expect(html).not.toContain("text-shimmer");
expect(html).not.toContain("waiting-dot");
@@ -428,7 +428,7 @@ describe("after the answer", () => {
<CoachTurnActivity activity={[ANSWER_DONE]} steps={[]} active={false} />,
"de",
);
expect(de).toContain("Denkprozess · 1 Schritt<");
expect(de).toContain("Denkprozess (1 Schritt)<");
const pl = render(
<CoachTurnActivity
activity={[THINKING_DONE, FETCH_BP, ANSWER_DONE]}
@@ -437,7 +437,7 @@ describe("after the answer", () => {
/>,
"pl",
);
expect(pl).toContain("Tok myślenia · 3 kroki<");
expect(pl).toContain("Tok myślenia (3 kroki)<");
});
it("an older message counts its steps, or the areas it drew on", () => {
@@ -445,7 +445,7 @@ describe("after the answer", () => {
render(
<CoachTurnActivity activity={[]} steps={[BP, SLEEP]} active={false} />,
),
).toContain("Thought process · 2 steps<");
).toContain("Thought process (2 steps)<");
expect(
render(
<CoachTurnActivity
@@ -455,7 +455,7 @@ describe("after the answer", () => {
areas={["bp"]}
/>,
),
).toContain("Thought process · 1 step<");
).toContain("Thought process (1 step)<");
});
it("says an answer is needed while a question waits, without shimmer", () => {
@@ -681,7 +681,7 @@ describe("the visible row text", () => {
),
),
),
).toBe("Thought process · 5 steps");
).toBe("Thought process (5 steps)");
expect(
textOf(
rowHtml(render(<CoachTurnActivity activity={[]} steps={[]} active />)),
@@ -132,6 +132,10 @@ export function MoodFactorMetricCrosstab({
const metricLabel = t(
METRIC_LABEL_KEY[row.metricKey] ?? row.metricKey,
);
const pairLabel = t("insights.mood.factorCrosstab.pairLabel", {
factor: factorLabel,
metric: metricLabel,
});
// v1.32.26 — the weight ("kg") row follows the metric/imperial
// preference: the unit symbol + the values (averages + delta, all in
// kg, offset-free) convert together. Every other display is unitless
@@ -168,12 +172,9 @@ export function MoodFactorMetricCrosstab({
)}
<span
className="text-foreground min-w-0 flex-1 truncate"
title={`${factorLabel} · ${metricLabel}`}
title={pairLabel}
>
{t("insights.mood.factorCrosstab.pairLabel", {
factor: factorLabel,
metric: metricLabel,
})}
{pairLabel}
</span>
<span className="text-foreground shrink-0 text-sm font-semibold tabular-nums">
{deltaText}
@@ -139,6 +139,10 @@ export function MoodTagMetricCrosstab({
const metricLabel = t(
METRIC_LABEL_KEY[row.metricKey] ?? row.metricKey,
);
const pairLabel = t("insights.mood.crosstab.pairLabel", {
tag: tagLabel,
metric: metricLabel,
});
const unit = t(UNIT_KEY[row.display]);
const up = row.delta >= 0;
const deltaText = `${up ? "+" : ""}${formatValue(row.delta, row.display, nf)} ${unit}`;
@@ -160,12 +164,9 @@ export function MoodTagMetricCrosstab({
)}
<span
className="text-foreground min-w-0 flex-1 truncate"
title={`${tagLabel} · ${metricLabel}`}
title={pairLabel}
>
{t("insights.mood.crosstab.pairLabel", {
tag: tagLabel,
metric: metricLabel,
})}
{pairLabel}
</span>
<span className="text-foreground shrink-0 text-sm font-semibold tabular-nums">
{deltaText}
@@ -265,7 +265,7 @@ describe("<MedicationCard> — open overdue slot (v1.16.4)", () => {
client,
);
expect(html).toContain("Overdue ·");
expect(html).toContain("Overdue since");
expect(html).toContain("can still be taken");
expect(html).toContain("text-warning");
expect(html).not.toContain("Next intake: Tomorrow,");
@@ -291,7 +291,7 @@ describe("<MedicationCard> — open overdue slot (v1.16.4)", () => {
expect(html).toContain("Next intake:");
expect(html).toContain("Tomorrow");
expect(html).not.toContain("Overdue ·");
expect(html).not.toContain("Overdue since");
expect(html).not.toContain("can still be taken");
});
});
@@ -401,9 +401,14 @@ export function EnvironmentSection() {
key={tr.id}
className="flex items-center justify-between gap-2 p-2"
>
<span className="text-sm">
{tr.label} · {fmt.dateShortSmartCalendar(tr.startDate)} –{" "}
{fmt.dateShortSmartCalendar(tr.endDate)}
<span className="min-w-0">
<span className="block truncate text-sm">{tr.label}</span>
<span className="text-muted-foreground block text-xs">
{t("settings.sections.environment.travel.range", {
start: fmt.dateShortSmartCalendar(tr.startDate),
end: fmt.dateShortSmartCalendar(tr.endDate),
})}
</span>
</span>
<ConfirmButton
slot="travel-remove"
@@ -79,7 +79,7 @@ describe("the list", () => {
);
expect(html).toContain('data-antigen="typhoid"');
expect(html).toContain('data-antigen="hepatitis-a"');
expect(html.match(/· Travel combo/g)).toHaveLength(2);
expect(html.match(/>Travel combo</g)).toHaveLength(2);
});
});
@@ -63,7 +63,7 @@ export function VaccinationDocumentSuggestion({
: (candidate.vaccineName ?? t("vaccinations.suggestion.unnamed"));
// A date stored at UTC midnight: render the calendar date, not the
// instant (the previous evening west of UTC).
return `${name} · ${formatCalendarDate(candidate.occurredAt.slice(0, 10), dateFormat, locale)}`;
return `${name}, ${formatCalendarDate(candidate.occurredAt.slice(0, 10), dateFormat, locale)}`;
};
const onPick = (candidate: VaccinationSuggestion) => {
@@ -209,7 +209,7 @@ function DoseRow({
</span>
) : null}
{showIdentity && identity ? (
<span className="text-muted-foreground">· {identity}</span>
<span className="text-muted-foreground">{identity}</span>
) : null}
</div>
<div className="text-muted-foreground flex flex-wrap items-center gap-x-2 gap-y-0.5 text-xs">
@@ -757,7 +757,7 @@ describe("doctor-report surgical history section", () => {
);
const text = await extractText(bytes);
expect(text).toContain("Operationen und Eingriffe");
expect(text).toMatch(/Knee · Links/);
expect(text).toMatch(/Knee \(Links\)/);
});
it("omits the section when there is no procedure", async () => {
@@ -819,7 +819,7 @@ describe("doctor-report condition body site", () => {
it("prints the site with its side in the report's language", async () => {
const text = await render(CONDITIONS, "de");
expect(text).toContain("Körperstelle");
expect(text).toMatch(/Knee · Links/);
expect(text).toMatch(/Knee \(Links\)/);
});
it("keeps the old table when no condition names a site", async () => {
@@ -100,7 +100,7 @@ describe("readCoachCorrelations", () => {
pValue: 0.001,
qValue: 0.02,
interpretation:
"Higher time in daylight tends to go with higher next-day sleep duration in your data — a pattern worth watching, not a cause.",
"Higher time in daylight tends to go with higher next-day sleep duration in your data. It is a pattern to watch, not a cause.",
lagDays: 1,
},
],
@@ -345,7 +345,7 @@ describe("executeCoachTool", () => {
lagDays: 1,
n: 42,
r: 0.31,
note: "Higher time in daylight tends to go with higher next-day sleep duration in your data — a pattern worth watching, not a cause.",
note: "Higher time in daylight tends to go with higher next-day sleep duration in your data. It is a pattern to watch, not a cause.",
},
],
coincident: {
@@ -118,7 +118,7 @@ describe("formatRelationsBlock — cross-metric correlations, descriptive only",
const rels: RelevantCorrelation[] = [
{
interpretation:
"Higher time in daylight tends to go with higher next-day sleep duration in your data — a pattern worth watching, not a cause.",
"Higher time in daylight tends to go with higher next-day sleep duration in your data. It is a pattern to watch, not a cause.",
n: 42,
r: 0.51,
},
@@ -457,7 +457,7 @@ describe("generateMetricStatus — generation path", () => {
vi.mocked(getRelevantCorrelationsForMetric).mockResolvedValueOnce([
{
interpretation:
"Higher time in daylight tends to go with lower next-day resting heart rate in your data — a pattern worth watching, not a cause.",
"Higher time in daylight tends to go with lower next-day resting heart rate in your data. It is a pattern to watch, not a cause.",
n: 35,
r: -0.46,
},
@@ -157,8 +157,8 @@ describe("buildCoachReadStrip — the pattern line speaks the reader's language"
// A sentence that is different but wrong is still a broken page.
expect(note).toBe(
"Wenn die Symptomstärke höher liegt, scheint die Schlafdauer am " +
"Folgetag nach bisheriger Datenlage eher niedriger auszufallen — ein " +
"Muster, das Beachtung verdient, keine Ursache.",
"Folgetag nach bisheriger Datenlage eher niedriger auszufallen. Das ist " +
"ein Muster, das Beachtung verdient, keine Ursache.",
);
});
@@ -170,8 +170,8 @@ describe("buildCoachReadStrip — the pattern line speaks the reader's language"
expect(strip.driver).not.toBeNull();
expect(strip.driver!.note).toBe(
"Higher symptom severity looks like it goes with lower next-day sleep " +
"duration in your data, on the evidence so far — a pattern worth " +
"watching, not a cause.",
"duration in your data, on the evidence so far. It is a pattern " +
"to watch, not a cause.",
);
});