Address 21 previously undefined behaviors across specs: - Authentication: Replace email/password with OIDC (Pocket-ID) - Cycle tracking: Add fixed-luteal phase scaling formula with examples - Calendar: Document period logging behavior (preserve predictions) - Garmin: Clarify connection is required (no phase-only mode) - Dashboard: Add UI states, dark mode, onboarding, accessibility - Notifications: Document timezone batching approach - New specs: observability.md (health, metrics, logging) - New specs: testing.md (unit + integration strategy) - Main spec: Add backup/recovery, known limitations, API updates Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
3.9 KiB
Cycle Tracking Specification
Job to Be Done
When I log my period start date, I want the app to calculate my current cycle day and phase, so that training and nutrition guidance is accurate.
Core Concepts
Cycle Day
Day 1 = first day of menstruation.
Calculation:
cycleDay = ((currentDate - lastPeriodDate) mod cycleLength) + 1
Default cycle length: 31 days (configurable per user).
Cycle Phases
Phase boundaries scale based on cycle length using a fixed luteal, variable follicular approach. The luteal phase (14 days) is biologically consistent; the follicular phase expands or contracts with cycle length.
Phase Calculation Formula:
Given cycleLength (user-configurable, default 31):
| Phase | Days | Weekly Limit | Training Type |
|---|---|---|---|
| MENSTRUAL | 1-3 | 30 min | Gentle rebounding only |
| FOLLICULAR | 4 to (cycleLength - 16) | 120 min | Strength + rebounding |
| OVULATION | (cycleLength - 15) to (cycleLength - 14) | 80 min | Peak performance |
| EARLY_LUTEAL | (cycleLength - 13) to (cycleLength - 7) | 100 min | Moderate training |
| LATE_LUTEAL | (cycleLength - 6) to cycleLength | 50 min | Gentle rebounding ONLY |
Examples by Cycle Length:
| Phase | 28-day | 31-day | 35-day |
|---|---|---|---|
| MENSTRUAL | 1-3 | 1-3 | 1-3 |
| FOLLICULAR | 4-12 | 4-15 | 4-19 |
| OVULATION | 13-14 | 16-17 | 20-21 |
| EARLY_LUTEAL | 15-21 | 18-24 | 22-28 |
| LATE_LUTEAL | 22-28 | 25-31 | 29-35 |
API Endpoints
POST /api/cycle/period
Log a new period start date.
Request Body:
{
"startDate": "2024-01-10"
}
Behavior:
- Update
user.lastPeriodDate - Create
PeriodLogrecord for history - Recalculate today's cycle day and phase
GET /api/cycle/current
Returns current cycle information.
Response:
{
"cycleDay": 12,
"phase": "FOLLICULAR",
"daysUntilNextPhase": 3,
"phaseConfig": {
"weeklyLimit": 120,
"dailyAvg": 17,
"trainingType": "Strength + rebounding"
}
}
Cycle Utilities (src/lib/cycle.ts)
Functions:
getCycleDay(lastPeriodDate, cycleLength, currentDate)- Calculate day in cyclegetPhase(cycleDay)- Determine current phasegetPhaseConfig(phase)- Get phase configurationgetPhaseLimit(phase)- Get weekly intensity limit
Constants:
PHASE_CONFIGS- Array of phase definitions
Period History
PeriodLog Schema
interface PeriodLog {
id: string;
user: string;
startDate: Date;
created: Date;
}
History Display (/history)
- List of all logged period dates
- Calculated cycle lengths between periods
- Average cycle length over time
- Ability to edit/delete entries
Configurable Cycle Length
Users with irregular cycles can adjust:
- Default: 31 days
- Range: 21-45 days
- Affects phase day boundaries per formula above
Known Limitations
The following scenarios are out of scope for MVP:
- Pregnancy - Cycle tracking pauses; app not designed for pregnancy
- Menopause - Irregular/absent cycles not supported
- Hormonal birth control - May suppress or alter natural cycle phases
- Anovulatory cycles - App assumes ovulation occurs; no detection for skipped ovulation
Users in these situations should consult healthcare providers for personalized guidance.
Success Criteria
- Cycle day resets to 1 on period log
- Phase transitions at correct day boundaries
- Weekly limits adjust per phase automatically
- History shows all logged periods
Acceptance Tests
getCycleDayreturns 1 on period start dategetCycleDayhandles cycle rollover correctlygetPhasereturns correct phase for each day rangegetPhasescales correctly for 28-day cyclegetPhasescales correctly for 35-day cycle- POST
/api/cycle/periodupdates user record - GET
/api/cycle/currentreturns accurate phase info - Days beyond cycle length default to LATE_LUTEAL