@@ -217,6 +225,25 @@ const styles = StyleSheet.create({
fontSize: 12,
marginTop: 2,
},
+ safetyNotice: {
+ backgroundColor: colors.surfaceSecondary,
+ borderColor: colors.border,
+ borderRadius: radius.lg,
+ borderWidth: StyleSheet.hairlineWidth,
+ gap: spacing.xs,
+ marginTop: spacing.sm,
+ padding: spacing.md,
+ },
+ safetyNoticeTitle: {
+ color: colors.text,
+ fontSize: fontSize.base,
+ fontWeight: fontWeight.semibold,
+ },
+ safetyNoticeText: {
+ color: colors.textSecondary,
+ fontSize: fontSize.base,
+ lineHeight: 20,
+ },
emptyText: {
color: colors.textTertiary,
fontSize: 14,
diff --git a/packages/scoring/README.md b/packages/scoring/README.md
index 9f456122d8..06311e0450 100644
--- a/packages/scoring/README.md
+++ b/packages/scoring/README.md
@@ -42,7 +42,7 @@ console.log({
| `@dofek/scoring/today-plan` | Deterministic ready/insufficient-data Today Plan result with a primary action, supporting facts, confidence, freshness, and shared presentation helpers |
| `@dofek/scoring/sleep-performance` | Sleep-performance components, tiers, and recommended-bedtime calculation |
| `@dofek/scoring/healthspan-years` | Score-to-years mapping and formatting |
-| `@dofek/scoring/menstrual-cycle` | Cycle-phase estimation and display metadata |
+| `@dofek/scoring/menstrual-cycle` | Cycle-phase estimation, display metadata, and shared safety copy |
| `@dofek/scoring/breathwork` | Built-in breathing techniques and session-duration helpers |
| `@dofek/scoring/loading-policy` | Blocking-loading state policy |
| `@dofek/scoring/query-cache` | Shared query-cache age constant |
@@ -60,7 +60,9 @@ console.log({
components equally.
- Healthspan display deltas map scores from 0–100 onto +3 to -2 years.
- Cycle phases estimate ovulation as `cycleLength - 14`; this is a display
- estimate, not a clinical assessment.
+ estimate, not a clinical assessment. The shared safety notice follows
+ [Apple's Cycle Tracking limitation](https://support.apple.com/en-au/120356)
+ that these estimates must not be used for birth control or diagnosis.
- The design-token modules contain values only; they do not install fonts or
render UI.
diff --git a/packages/scoring/src/menstrual-cycle.test.ts b/packages/scoring/src/menstrual-cycle.test.ts
index a2ab3c1b90..b2a85eb8de 100644
--- a/packages/scoring/src/menstrual-cycle.test.ts
+++ b/packages/scoring/src/menstrual-cycle.test.ts
@@ -1,5 +1,13 @@
import { describe, expect, it } from "vitest";
-import { type CyclePhase, computePhase } from "./menstrual-cycle.ts";
+import { CYCLE_TRACKING_SAFETY_NOTICE, type CyclePhase, computePhase } from "./menstrual-cycle.ts";
+
+describe("CYCLE_TRACKING_SAFETY_NOTICE", () => {
+ it("defines the shared tracking-only boundary", () => {
+ expect(CYCLE_TRACKING_SAFETY_NOTICE).toBe(
+ "Tracking estimates only. Do not use for birth control or diagnosis.",
+ );
+ });
+});
describe("computePhase", () => {
it("returns menstrual phase for days 1-5", () => {
diff --git a/packages/scoring/src/menstrual-cycle.ts b/packages/scoring/src/menstrual-cycle.ts
index ff968c2a11..6b265a4ee5 100644
--- a/packages/scoring/src/menstrual-cycle.ts
+++ b/packages/scoring/src/menstrual-cycle.ts
@@ -14,6 +14,9 @@ import { chartColors, statusColors } from "./colors.ts";
export type CyclePhase = "menstrual" | "follicular" | "ovulatory" | "luteal";
+export const CYCLE_TRACKING_SAFETY_NOTICE =
+ "Tracking estimates only. Do not use for birth control or diagnosis.";
+
/**
* Compute the cycle phase for a given day within a cycle.
* @param dayOfCycle 1-based day number within the cycle
diff --git a/packages/web/src/routes/cycle.test.tsx b/packages/web/src/routes/cycle.test.tsx
index 689578d7ab..3e7ca86ee8 100644
--- a/packages/web/src/routes/cycle.test.tsx
+++ b/packages/web/src/routes/cycle.test.tsx
@@ -150,6 +150,20 @@ describe("CyclePage", () => {
expect(screen.getByText("Period history could not be loaded.")).toBeTruthy();
});
+ it("shows the tracking-only safety boundary beside the phase estimate", () => {
+ state.phaseQuery.data = {
+ phase: "menstrual",
+ dayOfCycle: 3,
+ cycleLength: 28,
+ };
+
+ renderCyclePage();
+
+ expect(screen.getByRole("note", { name: "Cycle tracking safety notice" })).toHaveTextContent(
+ "Tracking estimates only. Do not use for birth control or diagnosis.",
+ );
+ });
+
it("renders the server-provided duration label", () => {
state.historyQuery.data = [
{
diff --git a/packages/web/src/routes/cycle.tsx b/packages/web/src/routes/cycle.tsx
index 8e93ef6e2b..cc13ae4dc1 100644
--- a/packages/web/src/routes/cycle.tsx
+++ b/packages/web/src/routes/cycle.tsx
@@ -1,5 +1,5 @@
import { formatDateYmd } from "@dofek/format/format";
-import { PHASE_DISPLAY } from "@dofek/scoring/menstrual-cycle";
+import { CYCLE_TRACKING_SAFETY_NOTICE, PHASE_DISPLAY } from "@dofek/scoring/menstrual-cycle";
import { createFileRoute } from "@tanstack/react-router";
import { useState } from "react";
import { PageLayout } from "../components/PageLayout.tsx";
@@ -77,6 +77,14 @@ function CyclePage() {
{currentPhase.data !== undefined && currentPhase.error ? (
) : null}
+