style: apply prettier across the tree

One-shot prettier pass so the new format:check gate starts from a clean
baseline. Formatting only — no behaviour change. Generator-owned and
vendored artifacts stay untouched via .prettierignore.
This commit is contained in:
Marc-André Bombeck committed 2026-06-19 22:20:31 +02:00
1 parent b3fea2ef59
commit 1f4ba5939d
1000 files changed
+12130 -8385

No files matched your search

@@ -80,8 +80,10 @@ describe("create schema", () => {
it("rejects a categoryKey other than custom", () => {
expect(
createCustomSymptomSchema.safeParse({ label: "X", categoryKey: "physical" })
.success,
createCustomSymptomSchema.safeParse({
label: "X",
categoryKey: "physical",
}).success,
).toBe(false);
});
});
+8 -1
View File
@@ -1,6 +1,13 @@
import { describe, expect, it } from "vitest";
import { addDays, dayDiff, isOnOrBefore, isWithin, parseDayMs, roundHalf } from "../day-math";
import {
addDays,
dayDiff,
isOnOrBefore,
isWithin,
parseDayMs,
roundHalf,
} from "../day-math";
/**
* The day-math is the shared substrate iOS re-implements bit-for-bit. These
+50 -10
View File
@@ -25,7 +25,10 @@ function profile(overrides: Partial<CycleProfile> = {}): CycleProfile {
} as CycleProfile;
}
function cycle(startDate: string, overrides: Partial<MenstrualCycle> = {}): MenstrualCycle {
function cycle(
startDate: string,
overrides: Partial<MenstrualCycle> = {},
): MenstrualCycle {
return {
id: `c-${startDate}`,
userId: "u1",
@@ -48,14 +51,33 @@ function cycle(startDate: string, overrides: Partial<MenstrualCycle> = {}): Mens
describe("buildCalendar", () => {
// Three ~28-day cycles → a real prediction.
const cycles = [
cycle("2026-01-01", { endDate: "2026-01-28", lengthDays: 28, periodEndDate: "2026-01-05" }),
cycle("2026-01-29", { endDate: "2026-02-25", lengthDays: 28, periodEndDate: "2026-02-02" }),
cycle("2026-01-01", {
endDate: "2026-01-28",
lengthDays: 28,
periodEndDate: "2026-01-05",
}),
cycle("2026-01-29", {
endDate: "2026-02-25",
lengthDays: 28,
periodEndDate: "2026-02-02",
}),
cycle("2026-02-26", { periodEndDate: "2026-03-02" }),
];
it("labels logged-flow days and emits a forecast for ≥2 cycles", () => {
const dayLogs: CalendarDayLogRow[] = [
{ date: "2026-02-26", flow: "MEDIUM", basalBodyTempC: null, temperatureExcluded: false, ovulationTest: null, cervicalMucus: null, cervixPosition: null, cervixFirmness: null, cervixOpening: null, hasSymptoms: true },
{
date: "2026-02-26",
flow: "MEDIUM",
basalBodyTempC: null,
temperatureExcluded: false,
ovulationTest: null,
cervicalMucus: null,
cervixPosition: null,
cervixFirmness: null,
cervixOpening: null,
hasSymptoms: true,
},
];
const { prediction, days } = buildCalendar(
profile(),
@@ -99,9 +121,18 @@ describe("buildCalendar", () => {
it("surfaces the fertile window for the conception goal once past cold start", () => {
// Four starts → three completed cycles → not still-learning.
const fourCycles = [
cycle("2026-01-01", { endDate: "2026-01-29", periodEndDate: "2026-01-05" }),
cycle("2026-01-29", { endDate: "2026-02-26", periodEndDate: "2026-02-02" }),
cycle("2026-02-26", { endDate: "2026-03-26", periodEndDate: "2026-03-02" }),
cycle("2026-01-01", {
endDate: "2026-01-29",
periodEndDate: "2026-01-05",
}),
cycle("2026-01-29", {
endDate: "2026-02-26",
periodEndDate: "2026-02-02",
}),
cycle("2026-02-26", {
endDate: "2026-03-26",
periodEndDate: "2026-03-02",
}),
cycle("2026-03-26", { periodEndDate: "2026-03-30" }),
];
const { days } = buildCalendar(
@@ -163,9 +194,18 @@ describe("buildCalendar", () => {
it("emits normal fertile/ovulation/phase output once ≥3 cycles are observed", () => {
// Four ~28-day starts → three COMPLETED cycle lengths → cyclesObserved=3.
const fourCycles = [
cycle("2026-01-01", { endDate: "2026-01-29", periodEndDate: "2026-01-05" }),
cycle("2026-01-29", { endDate: "2026-02-26", periodEndDate: "2026-02-02" }),
cycle("2026-02-26", { endDate: "2026-03-26", periodEndDate: "2026-03-02" }),
cycle("2026-01-01", {
endDate: "2026-01-29",
periodEndDate: "2026-01-05",
}),
cycle("2026-01-29", {
endDate: "2026-02-26",
periodEndDate: "2026-02-02",
}),
cycle("2026-02-26", {
endDate: "2026-03-26",
periodEndDate: "2026-03-02",
}),
cycle("2026-03-26", { periodEndDate: "2026-03-30" }),
];
const { prediction, stillLearning, days } = buildCalendar(
+5 -2
View File
@@ -5,7 +5,8 @@
*/
import { describe, it, expect, vi, beforeEach } from "vitest";
const isModuleEnabledMock = vi.fn<(userId: string, key: string) => Promise<boolean>>();
const isModuleEnabledMock =
vi.fn<(userId: string, key: string) => Promise<boolean>>();
const getOrCreateCycleProfileMock = vi.fn();
vi.mock("@/lib/modules/gate", () => ({
@@ -50,7 +51,9 @@ describe("isCycleEnabled", () => {
it("gates on UPPERCASE FEMALE only (server enum spelling)", () => {
// Lowercase / other spellings never enable by derivation.
expect(isCycleEnabled("female", { cycleTrackingEnabled: null })).toBe(false);
expect(isCycleEnabled("female", { cycleTrackingEnabled: null })).toBe(
false,
);
expect(isCycleEnabled("OTHER", { cycleTrackingEnabled: null })).toBe(false);
});
@@ -32,7 +32,9 @@ describe("cycle/healthkit-mapping — identifier ownership", () => {
});
it("does NOT own unrelated identifiers", () => {
expect(isCycleHkIdentifier("HKQuantityTypeIdentifierStepCount")).toBe(false);
expect(isCycleHkIdentifier("HKQuantityTypeIdentifierStepCount")).toBe(
false,
);
});
});
@@ -83,7 +85,10 @@ describe("cycle/healthkit-mapping — ovulation + cervical mucus + tests", () =>
HK_OVULATION_TEST,
"HKCategoryValueOvulationTestResultLuteinizingHormoneSurge",
),
).toEqual({ kind: "day-log", fields: { ovulationTest: "POSITIVE_LH_SURGE" } });
).toEqual({
kind: "day-log",
fields: { ovulationTest: "POSITIVE_LH_SURGE" },
});
expect(mapHkCycleSample(HK_OVULATION_TEST, "4")).toEqual({
kind: "day-log",
fields: { ovulationTest: "ESTROGEN_SURGE" },
@@ -141,10 +146,7 @@ describe("cycle/healthkit-mapping — sexual activity + protection", () => {
describe("cycle/healthkit-mapping — contraceptive routes to day-log + profile", () => {
it("records the method AND carries a profile nudge", () => {
expect(
mapHkCycleSample(
HK_CONTRACEPTIVE,
"HKCategoryValueContraceptiveOral",
),
mapHkCycleSample(HK_CONTRACEPTIVE, "HKCategoryValueContraceptiveOral"),
).toEqual({
kind: "day-log+profile",
fields: { contraceptive: "ORAL" },
@@ -164,9 +166,9 @@ describe("cycle/healthkit-mapping — symptoms", () => {
});
it("skips an explicit not-present symptom sample", () => {
expect(
mapHkCycleSample("HKCategoryTypeIdentifierHeadache", "1"),
).toEqual({ kind: "skip" });
expect(mapHkCycleSample("HKCategoryTypeIdentifierHeadache", "1")).toEqual({
kind: "skip",
});
expect(hkSymptomIsPresent("1")).toBe(false);
expect(hkSymptomIsPresent("HKCategoryValueSeverityNotPresent")).toBe(false);
expect(hkSymptomIsPresent("3")).toBe(true);
+26 -11
View File
@@ -16,11 +16,7 @@ import type { CrossMetricMeasurement } from "@/lib/insights/mood-aggregates";
import type { CyclePhase } from "../types";
/** Build a measurement row on a given YYYY-MM-DD at noon UTC. */
function m(
type: string,
day: string,
value: number,
): CrossMetricMeasurement {
function m(type: string, day: string, value: number): CrossMetricMeasurement {
return {
type,
value,
@@ -55,7 +51,9 @@ function fixture(opts: {
const k = dayKey(dayIdx++);
phaseByDay.set(k, "LUTEAL");
// tiny deterministic jitter so the group has non-zero variance
measurements.push(m("RESTING_HEART_RATE", k, opts.lutealRhr + (i % 3) * 0.5));
measurements.push(
m("RESTING_HEART_RATE", k, opts.lutealRhr + (i % 3) * 0.5),
);
}
for (let i = 0; i < opts.n; i++) {
const k = dayKey(dayIdx++);
@@ -130,7 +128,11 @@ describe("computePhaseMetricCrosstab — MOOD + GLUCOSE outcomes (QA HIGH)", ()
});
it("surfaces a luteal-vs-follicular MOOD contrast (injected synthetic channel)", () => {
const { phaseByDay, measurements } = contrastFor(MOOD_CHANNEL_KEY, 4.4, 2.6);
const { phaseByDay, measurements } = contrastFor(
MOOD_CHANNEL_KEY,
4.4,
2.6,
);
const rows = computePhaseMetricCrosstab({ phaseByDay, measurements });
const mood = rows.find((r) => r.metricKey === "mood");
expect(mood).toBeDefined();
@@ -193,7 +195,9 @@ describe("computePhaseMetricCrosstab", () => {
});
const rows = computePhaseMetricCrosstab({ phaseByDay, measurements });
// No real separation → nothing clears p < 0.05.
expect(rows.find((r) => r.metricKey === "restingHeartRate")).toBeUndefined();
expect(
rows.find((r) => r.metricKey === "restingHeartRate"),
).toBeUndefined();
});
it("ignores MENSTRUAL / OVULATORY days (only the two contrast phases count)", () => {
@@ -298,7 +302,12 @@ describe("discoverPhaseCorrelations", () => {
const phaseByDay = new Map<string, CyclePhase>();
const measurements: CrossMetricMeasurement[] = [];
const base = new Date(Date.UTC(2026, 0, 1));
const phases: CyclePhase[] = ["MENSTRUAL", "FOLLICULAR", "OVULATORY", "LUTEAL"];
const phases: CyclePhase[] = [
"MENSTRUAL",
"FOLLICULAR",
"OVULATORY",
"LUTEAL",
];
for (let i = 0; i < 42; i++) {
const d = new Date(base);
d.setUTCDate(d.getUTCDate() + i);
@@ -326,13 +335,19 @@ describe("selectHeadlinePhaseRow", () => {
});
it("prefers resting heart rate", () => {
const rows = [row("weight"), row("restingHeartRate"), row("heartRateVariability")];
const rows = [
row("weight"),
row("restingHeartRate"),
row("heartRateVariability"),
];
expect(selectHeadlinePhaseRow(rows)?.metricKey).toBe("restingHeartRate");
});
it("falls back to HRV when RHR is absent", () => {
const rows = [row("weight"), row("heartRateVariability")];
expect(selectHeadlinePhaseRow(rows)?.metricKey).toBe("heartRateVariability");
expect(selectHeadlinePhaseRow(rows)?.metricKey).toBe(
"heartRateVariability",
);
});
it("falls back to the strongest remaining row", () => {
+13 -3
View File
@@ -108,8 +108,16 @@ describe("cycle/phase — phaseSeries", () => {
it("emits a categorical value per day across the span", () => {
const series = phaseSeries("2024-01-01", "2024-01-28", CYCLE);
expect(series).toHaveLength(28);
expect(series[0]).toEqual({ date: "2024-01-01", phase: "MENSTRUAL", dayOfCycle: 1 });
expect(series[27]).toEqual({ date: "2024-01-28", phase: "LUTEAL", dayOfCycle: 28 });
expect(series[0]).toEqual({
date: "2024-01-01",
phase: "MENSTRUAL",
dayOfCycle: 1,
});
expect(series[27]).toEqual({
date: "2024-01-28",
phase: "LUTEAL",
dayOfCycle: 28,
});
});
it("marks out-of-cycle days null at the span edges", () => {
@@ -131,6 +139,8 @@ describe("cycle/phase — phaseSeries", () => {
};
expect(phaseForDay("2024-03-14", dstCycle).dayOfCycle).toBe(14);
expect(phaseForDay("2024-03-14", dstCycle).phase).toBe("OVULATORY");
expect(phaseForDay(addDays("2024-03-01", 27), dstCycle).dayOfCycle).toBe(28);
expect(phaseForDay(addDays("2024-03-01", 27), dstCycle).dayOfCycle).toBe(
28,
);
});
});
+108 -23
View File
@@ -65,7 +65,10 @@ describe("cycle/prediction — robust statistics", () => {
describe("cycle/prediction — cycle-length estimator (§1, §5)", () => {
it("regular 28-day user: median 28, MAD 0 → sigma floor 1.0", () => {
const est = estimateCycleLength([28, 28, 28, 28, 28, 28], "TRYING_TO_CONCEIVE");
const est = estimateCycleLength(
[28, 28, 28, 28, 28, 28],
"TRYING_TO_CONCEIVE",
);
expect(est.lengthRounded).toBe(28);
expect(est.sigma).toBe(1.0); // SIGMA_FLOOR (MAD = 0)
expect(est.cyclesObserved).toBe(6);
@@ -74,14 +77,20 @@ describe("cycle/prediction — cycle-length estimator (§1, §5)", () => {
it("irregular user: high MAD widens sigma", () => {
// lengths 24,28,32,26,30,28 → median 28, deviations 4,0,4,2,2,0 → MAD 2
const est = estimateCycleLength([24, 28, 32, 26, 30, 28], "TRYING_TO_CONCEIVE");
const est = estimateCycleLength(
[24, 28, 32, 26, 30, 28],
"TRYING_TO_CONCEIVE",
);
expect(est.lengthRounded).toBe(28);
expect(est.sigma).toBeCloseTo(1.4826 * 2, 6); // 2.9652
});
it("excludes a 3-MAD outlier from the point estimate but counts it against confidence", () => {
// 28,28,29,28,28 + one wild 60-day (missed-log: >= 1.75*28 = 49).
const est = estimateCycleLength([28, 28, 29, 28, 28, 60], "TRYING_TO_CONCEIVE");
const est = estimateCycleLength(
[28, 28, 29, 28, 28, 60],
"TRYING_TO_CONCEIVE",
);
// 60 excluded → median over kept = 28, kept count = 5.
expect(est.lengthRounded).toBe(28);
expect(est.cyclesObserved).toBe(5);
@@ -122,7 +131,13 @@ describe("cycle/prediction — period-length estimator (§2)", () => {
};
function flowDay(date: string, flow: DayLogInput["flow"]): DayLogInput {
return { date, flow, basalBodyTempC: null, ovulationTest: null, cervicalMucus: null };
return {
date,
flow,
basalBodyTempC: null,
ovulationTest: null,
cervicalMucus: null,
};
}
it("counts a contiguous 5-day bleeding run", () => {
@@ -149,7 +164,9 @@ describe("cycle/prediction — period-length estimator (§2)", () => {
});
it("returns 0 when startDate itself has no bleeding", () => {
expect(observedPeriodLength("2024-01-01", [flowDay("2024-01-02", "MEDIUM")])).toBe(0);
expect(
observedPeriodLength("2024-01-01", [flowDay("2024-01-02", "MEDIUM")]),
).toBe(0);
});
it("estimatePeriodLength prefers explicit periodEndDate", () => {
@@ -286,7 +303,12 @@ describe("cycle/prediction — predictCycle calendar path (§1–§3)", () => {
cervicalMucus: null,
});
}
const result = predictCycle(cycles, logs, BASE_PROFILE, addDays(lastStart, 7));
const result = predictCycle(
cycles,
logs,
BASE_PROFILE,
addDays(lastStart, 7),
);
// density 1 → logSparsity 0 → penalty 1 → halfWidth round(1*1*1)=1.
expect(result.nextPeriodStartLow).toBe(addDays(result.nextPeriodStart, -1));
// cAdherence = 0.4 + 0.6*1 = 1.0 → confidence 1*1*1.0 = 0.98 (clamped from 1.0).
@@ -296,8 +318,18 @@ describe("cycle/prediction — predictCycle calendar path (§1–§3)", () => {
});
describe("cycle/prediction — symptothermal confirmation (§4.2)", () => {
function bbtDay(date: string, t: number, mucus: DayLogInput["cervicalMucus"] = null): DayLogInput {
return { date, flow: null, basalBodyTempC: t, ovulationTest: null, cervicalMucus: mucus };
function bbtDay(
date: string,
t: number,
mucus: DayLogInput["cervicalMucus"] = null,
): DayLogInput {
return {
date,
flow: null,
basalBodyTempC: t,
ovulationTest: null,
cervicalMucus: mucus,
};
}
function cervixDay(
@@ -529,7 +561,10 @@ describe("cycle/prediction — symptothermal confirmation (§4.2)", () => {
logs.push(bbtDay(addDays(start, 8), 36.62));
// temp ovulation = 2024-01-06. Add a mucus peak (EGG_WHITE) on 2024-01-06,
// confirmed by 3 drier days after it (Sensiplan post-peak count).
const mucusDay = (date: string, m: DayLogInput["cervicalMucus"]): DayLogInput => ({
const mucusDay = (
date: string,
m: DayLogInput["cervicalMucus"],
): DayLogInput => ({
date,
flow: null,
basalBodyTempC: null,
@@ -670,7 +705,10 @@ describe("cycle/prediction — symptothermal confirmation (§4.2)", () => {
logs.push(bbtDay(addDays(riseStart, 1), 36.62));
logs.push(bbtDay(addDays(riseStart, 2), 36.63));
// mucus peak agreeing with the temp ovulation, confirmed by 3 drier days.
const mucusDay = (date: string, m: DayLogInput["cervicalMucus"]): DayLogInput => ({
const mucusDay = (
date: string,
m: DayLogInput["cervicalMucus"],
): DayLogInput => ({
date,
flow: null,
basalBodyTempC: null,
@@ -682,7 +720,12 @@ describe("cycle/prediction — symptothermal confirmation (§4.2)", () => {
logs.push(mucusDay(addDays(ovDay, 2), "DRY"));
logs.push(mucusDay(addDays(ovDay, 3), "DRY"));
const result = predictCycle(cycles, logs, BASE_PROFILE, addDays(lastStart, 16));
const result = predictCycle(
cycles,
logs,
BASE_PROFILE,
addDays(lastStart, 16),
);
expect(result.ovulationConfirmed).toBe(true);
expect(result.method).toBe("BLENDED");
expect(result.predictedOvulation).toBe(ovDay);
@@ -768,7 +811,10 @@ describe("cycle/prediction — Marquette LH/OPK ovulation anchor (§4, C2)", ()
logs.push(bbt(riseStart, 36.6));
logs.push(bbt(addDays(riseStart, 1), 36.62));
logs.push(bbt(addDays(riseStart, 2), 36.63));
const mucus = (date: string, m: DayLogInput["cervicalMucus"]): DayLogInput => ({
const mucus = (
date: string,
m: DayLogInput["cervicalMucus"],
): DayLogInput => ({
date,
flow: null,
basalBodyTempC: null,
@@ -782,7 +828,12 @@ describe("cycle/prediction — Marquette LH/OPK ovulation anchor (§4, C2)", ()
// A contradicting LH surge far from the temp shift must NOT override it.
logs.push(lhDay(addDays(ovDay, 5)));
const result = predictCycle(cycles, logs, BASE_PROFILE, addDays(lastStart, 20));
const result = predictCycle(
cycles,
logs,
BASE_PROFILE,
addDays(lastStart, 20),
);
expect(result.ovulationConfirmed).toBe(true);
expect(result.predictedOvulation).toBe(ovDay); // symptothermal wins
});
@@ -793,7 +844,8 @@ describe("cycle/prediction — temperature-trend retrospective ovulation (§4.3)
const start = "2024-02-01";
const nights: NightlyTempInput[] = [];
// 6 trailing nights at 36.30 → mean 36.30.
for (let i = 0; i < 6; i++) nights.push({ date: addDays(start, i), valueC: 36.3 });
for (let i = 0; i < 6; i++)
nights.push({ date: addDays(start, i), valueC: 36.3 });
// 4-night window: 3 elevated (>= 36.45) + 1 not.
nights.push({ date: addDays(start, 6), valueC: 36.46 });
nights.push({ date: addDays(start, 7), valueC: 36.47 });
@@ -810,13 +862,20 @@ describe("cycle/prediction — temperature-trend retrospective ovulation (§4.3)
const lastStart = cycles[cycles.length - 1].startDate;
const start = addDays(lastStart, 5);
const nights: NightlyTempInput[] = [];
for (let i = 0; i < 6; i++) nights.push({ date: addDays(start, i), valueC: 36.3 });
for (let i = 0; i < 6; i++)
nights.push({ date: addDays(start, i), valueC: 36.3 });
nights.push({ date: addDays(start, 6), valueC: 36.46 });
nights.push({ date: addDays(start, 7), valueC: 36.47 });
nights.push({ date: addDays(start, 8), valueC: 36.48 });
nights.push({ date: addDays(start, 9), valueC: 36.49 });
const result = predictCycle(cycles, [], BASE_PROFILE, addDays(lastStart, 20), nights);
const result = predictCycle(
cycles,
[],
BASE_PROFILE,
addDays(lastStart, 20),
nights,
);
expect(result.ovulationConfirmed).toBe(true);
expect(result.method).toBe("BLENDED");
expect(result.predictedOvulation).toBe(addDays(start, 5));
@@ -824,8 +883,18 @@ describe("cycle/prediction — temperature-trend retrospective ovulation (§4.3)
});
describe("cycle/prediction — multi-cycle window scoping (QA HIGH)", () => {
function bbtDay(date: string, t: number, mucus: DayLogInput["cervicalMucus"] = null): DayLogInput {
return { date, flow: null, basalBodyTempC: t, ovulationTest: null, cervicalMucus: mucus };
function bbtDay(
date: string,
t: number,
mucus: DayLogInput["cervicalMucus"] = null,
): DayLogInput {
return {
date,
flow: null,
basalBodyTempC: t,
ovulationTest: null,
cervicalMucus: mucus,
};
}
/** Build a confirmable symptothermal shift (temp + agreeing mucus) on `ovDay`. */
@@ -839,7 +908,10 @@ describe("cycle/prediction — multi-cycle window scoping (QA HIGH)", () => {
logs.push(bbtDay(riseStart, 36.6));
logs.push(bbtDay(addDays(riseStart, 1), 36.62));
logs.push(bbtDay(addDays(riseStart, 2), 36.63));
const mucusDay = (date: string, m: DayLogInput["cervicalMucus"]): DayLogInput => ({
const mucusDay = (
date: string,
m: DayLogInput["cervicalMucus"],
): DayLogInput => ({
date,
flow: null,
basalBodyTempC: null,
@@ -882,9 +954,17 @@ describe("cycle/prediction — multi-cycle window scoping (QA HIGH)", () => {
const staleOv = addDays("2024-01-01", 13); // months ago
const currentOv = addDays(lastStart, 13); // current cycle
const logs = [...symptothermalAround(staleOv), ...symptothermalAround(currentOv)];
const logs = [
...symptothermalAround(staleOv),
...symptothermalAround(currentOv),
];
const result = predictCycle(cycles, logs, BASE_PROFILE, addDays(lastStart, 16));
const result = predictCycle(
cycles,
logs,
BASE_PROFILE,
addDays(lastStart, 16),
);
expect(result.ovulationConfirmed).toBe(true);
expect(result.predictedOvulation).toBe(currentOv);
expect(result.nextPeriodStart).toBe(addDays(currentOv, 14));
@@ -932,10 +1012,15 @@ describe("cycle/prediction — luteal clamp single source of truth (QA HIGH)", (
const cycles = cyclesFromGaps("2024-01-01", [28, 28, 28]);
const lastStart = cycles[cycles.length - 1].startDate;
// Profile asks for luteal 8 → engine clamps to 10.
const profile: CycleProfileInput = { ...BASE_PROFILE, lutealPhaseLength: 8 };
const profile: CycleProfileInput = {
...BASE_PROFILE,
lutealPhaseLength: 8,
};
const result = predictCycle(cycles, [], profile, addDays(lastStart, 5));
// predictedOvulation = nextStart − clampedLuteal(10).
expect(result.predictedOvulation).toBe(addDays(result.nextPeriodStart, -10));
expect(result.predictedOvulation).toBe(
addDays(result.nextPeriodStart, -10),
);
});
});
+1 -4
View File
@@ -308,10 +308,7 @@ export async function restoreCycleData(
temperatureExcluded: d.temperatureExcluded ?? false,
ovulationTest: enumOrNull(d.ovulationTest, OVULATION_TESTS) as never,
cervicalMucus: enumOrNull(d.cervicalMucus, CERVICAL_MUCUS) as never,
cervixPosition: enumOrNull(
d.cervixPosition,
CERVIX_POSITIONS,
) as never,
cervixPosition: enumOrNull(d.cervixPosition, CERVIX_POSITIONS) as never,
cervixFirmness: enumOrNull(
d.cervixFirmness,
CERVIX_FIRMNESSES,
+9 -3
View File
@@ -206,11 +206,17 @@ export function toCyclePredictionDTO(
// a fertile window the rest of the app refuses to show. `ovulationConfirmed`
// stays goal-gated only: a confirmed shift is observed data, not a prior.
fertileWindowStart:
goalAllowsFertile && !result.stillLearning ? result.fertileWindowStart : null,
goalAllowsFertile && !result.stillLearning
? result.fertileWindowStart
: null,
fertileWindowEnd:
goalAllowsFertile && !result.stillLearning ? result.fertileWindowEnd : null,
goalAllowsFertile && !result.stillLearning
? result.fertileWindowEnd
: null,
predictedOvulation:
goalAllowsFertile && !result.stillLearning ? result.predictedOvulation : null,
goalAllowsFertile && !result.stillLearning
? result.predictedOvulation
: null,
ovulationConfirmed: goalAllowsFertile ? result.ovulationConfirmed : false,
confidence: result.confidence,
cyclesObserved: result.cyclesObserved,