feat(cycle): choose cervix as the symptothermal secondary symptom

Let a user pick the Sensiplan double-check's second indicator — cervical
mucus (default) or cervix observation — mirroring the published NFP
standard where the temperature primary sign pairs with one chosen
secondary sign.

- `CycleProfile.secondarySymptom` (MUCUS default | CERVIX) drives which
  second sign the engine confirms against. Casual users keep temperature
  + mucus untouched; the choice lives behind the advanced cycle settings.
- `CycleDayLog` gains the three standard cervix signs — position
  (LOW/HIGH), firmness (FIRM/SOFT), opening/os (CLOSED/OPEN) — as nullable
  columns (migration 0157), wired through Zod, the day-log upsert, the
  DTO, the sync feed, backup/restore and OpenAPI. Additive and
  back-compatible: existing profiles backfill MUCUS, existing day-logs
  keep NULL cervix signs, iOS inherits the fields.
- The engine's `confirmSymptothermal` takes the secondary-symptom choice;
  on CERVIX it confirms against `detectCervixPeak` (last fertile-cervix
  day followed by three closed days — the cervix-closure rule) instead of
  the mucus peak. The double-check still requires BOTH the temperature
  shift AND the secondary peak: a single cervix sign never confirms, and
  the still-learning gate and conservative posture are unchanged.

Engine derived clean-room from the Arbeitsgruppe-NFP / Raith-Paula &
Frank-Herrmann standard.
This commit is contained in:
Marc-André Bombeck committed 2026-06-14 15:23:26 +02:00
1 parent 2c225ce094
commit 609d248252
18 files changed
+561 -6

No files matched your search

+11
View File
@@ -11,6 +11,10 @@ import {
flowLevelEnum,
ovulationTestEnum,
cervicalMucusEnum,
secondarySymptomEnum,
cervixPositionEnum,
cervixFirmnessEnum,
cervixOpeningEnum,
homeTestResultEnum,
cycleTrackingGoalEnum,
cycleDayLogInputSchema,
@@ -104,6 +108,9 @@ const cycleDayLogDto = z
temperatureExcluded: z.boolean(),
ovulationTest: ovulationTestEnum.nullable(),
cervicalMucus: cervicalMucusEnum.nullable(),
cervixPosition: cervixPositionEnum.nullable(),
cervixFirmness: cervixFirmnessEnum.nullable(),
cervixOpening: cervixOpeningEnum.nullable(),
sexualActivity: z.boolean(),
protectedSex: z.boolean().nullable(),
pregnancyTest: homeTestResultEnum.nullable(),
@@ -179,12 +186,16 @@ const cycleCalendarDayDto = z.object({
temperatureExcluded: z.boolean(),
ovulationTest: ovulationTestEnum.nullable(),
cervicalMucus: cervicalMucusEnum.nullable(),
cervixPosition: cervixPositionEnum.nullable(),
cervixFirmness: cervixFirmnessEnum.nullable(),
cervixOpening: cervixOpeningEnum.nullable(),
});
const cycleProfileDto = z
.object({
goal: cycleTrackingGoalEnum,
cycleTrackingEnabled: z.boolean(),
secondarySymptom: secondarySymptomEnum,
rawChartMode: z.boolean(),
predictionEnabled: z.boolean(),
discreetNotifications: z.boolean(),