ページ · Health Debug
M6 — Cycle (Fertility) — Session Card
Planning artifact. This card is opened by a future build session so it knows exactly what to build for module M6 Cycle (Fertility). It contains no production code. It is faithful to
docs/plan/13-execution-modules-and-sessions.md(the session framework),03-protocol-engines.md §6(the engine spec),04-backend-togo.md(backend conventions),02-platform-divergences.md §2.1(this module closes Divergence #1 on iOS), and12-legacy-reference-map.md(legacy precedents).
1. Purpose
The Cycle (Fertility) engine tracks user-logged menstrual cycles and offers predictions only once enough consistent data exists, and otherwise fails safe to "no prediction." It is a backend state machine in ToGO (the Single Source of Truth); every client — web, mcp, apple, android, desktop (there is no chrome surface for this module) — only renders the engine state the API returns and submits log intents. This module closes platform Divergence #1 (Meds & Cycle Tracking, historically missing on iOS): in the greenfield build the 3-cycle prediction gate and irregularity fail-safe live once, server-side, so the iOS/Android/web screens are pure presentation against an already-live contract and can never re-derive the rule. Because cycle/fertility data is highly sensitive, cross-user isolation (404-not-403) and per-user SSE/event routing (no cross-user leakage) are hard requirements, not niceties — the legacy build had to fix an event broker that leaked across users, and this module must not reintroduce that class of bug.
2. Engine parameters relevant to this module
Spec-fixed constants (from 03-protocol-engines.md §6.2 and shared context — do NOT invent others)
| Parameter | Value | Notes |
|---|---|---|
| Calibration requirement | 3 consecutive logged cycles before any prediction is made | Hard gate. No prediction, ever, below this. |
| Irregularity rule | On irregularity the engine fails safe: no prediction | Predictions are withdrawn, never "widened." |
| Prediction sentinel | unavailable (+ reason code) | NOT NULL rule: an explicit sentinel value, never SQL NULL. |
| States | Insufficient / Calibrated / Suspended | Categorical only — the user never sees a confidence percentage (Core Opinion 1). |
| "Consecutive" | no unlogged cycle between logged ones | Engine never infers a skipped cycle happened. |
State transitions (03 §6.3): Insufficient → Calibrated on the 3rd consecutive cycle; Calibrated → Suspended when irregularity is detected; a logging gap resets the consecutive counter to Insufficient; Suspended → Insufficient on recalibration (a fresh 3-consecutive run — see PENDING items).
Legacy precedent (PENDING RATIFICATION — precedents from cycle.go, NOT spec-fixed; carry as fail-safe categorical/temporal, no medical certainty)
These are the concrete numbers the legacy engine used. They are candidate answers, not authority — the greenfield spec (03 §6.2, and Open Question #6 in 03 §Open Questions) leaves the quantitative definition of "irregularity" undefined. Use them as the default implementation to be ratified during the backend session, and encode whatever is ratified as literal constants inside the engine core/ package.
| Precedent | Legacy value (cycle.go) |
|---|---|
| Minimum cycles for prediction | MinCyclesForPrediction = 3 (matches the spec gate) |
| Irregularity threshold | IrregularThresholdDays = 9-day spread → refuse to predict |
| Averaging window | average of the last 6 cycles |
| Ovulation estimate | next cycle start −14 days |
| Fertile window | −5 / +1 day around the ovulation estimate |
| BBT confirmation | 3 readings ≥ +0.2 °C over the prior 6-day mean |
| Pregnancy dating | Naegele: +280 days from cycle start |
| Safety data | SafetyProvider interface with NoSafetyData{} default — no drug DB wired, by design |
All predictions are behavioral-data extrapolations, explicitly labelled non-medical, and every AI-adjacent / fertility surface ends in "Consult your doctor" (Core Opinion 2). The engine never smooths, averages away, or best-guesses an irregular history.
Data model (this module owns)
- Tables:
cyclesandcycle_days(per-user). Every columnNOT NULLwith a defined default; UUID ids;timestamptztimestamps (04 §4/§6). - Endpoints (names from
03 §6.6, under the/api/v1/prefix per04 §API):POST /api/v1/engines/cycle/logs(log a cycle event; acceptsIdempotency-Key),GET /api/v1/engines/cycle/state(calibration state + prediction-or-unavailable+ reason),GET /api/v1/engines/cycle/history. - Engine output shape:
{ state, consecutive_cycles_logged, prediction: value|unavailable, reason }.
3. Session table
One row per session. Surface order per 13 §3: backend → web → mcp → (no chrome) → apple → android → desktop. The backend session must land before any client session (13 §4). Replace <date> (YYYY-MM-DD) and NN (per-cell counter) when the session actually runs.
| Session ID | Surface | Scope (what this one session builds) | Entry deps | Done criteria (template from 13 §5) | Plan docs to read |
|---|---|---|---|---|---|
S-<date>-M6-backend-NN | backend | The cycle ToGO plugin: pure core/ state machine (Insufficient/Calibrated/Suspended, the 3-consecutive gate, irregularity → no-prediction fail-safe, unavailable sentinel + reason) + plugin.go registration; cycles + cycle_days expand-contract migration; the three endpoints; engine unit tests as executable spec; integration test on real Postgres. Ratify the PENDING legacy numbers (§2) and encode them literally in core/. Verify cross-user isolation (404-not-403) and that any SSE/event emitted for cycle carries no cross-user leakage. | M0.1–M0.4 (foundation backend: kernel, TOGO auth + isolation test, plugin framework, expand-contract + error-envelope harness); M1 backend as the reference-engine pattern to copy | Backend-engine template: plugin registered on the kernel; migration is expand-contract and every column NOT NULL with a default; endpoints return computed state; engine unit tests encode every spec/ratified constant literally and pass (no prediction until 3 consecutive cycles; fail-safe = no prediction on irregularity); integration test on real Postgres passes (idempotency replay; contradictory-edit → Suspended; cross-user isolation 404-not-403); no protocol constant appears outside the engine's core/ package | 13 (§3–§6), 03 §6 (engine spec), 04 (§2 layout, §3 sqlc+Atlas, §4 NOT NULL rule, §5 expand-contract, §8 plugin/purity, §9 testing), 12 §2 (legacy cycle.go constants) |
S-<date>-M6-web-NN | web | The cycle screen (/engines/cycle): renders server state — shows calibration progress / "needs 3 consecutive logged cycles" when Insufficient or Suspended, and shows a prediction only when the server state is Calibrated. Cycle-log entry control submits intents only. Both themes; Arabic RTL. | S-<date>-M6-backend-NN; M0.5–M0.7 (web shell, theme engine, i18n engine); M1 web as the reference card pattern | Client-engine template: renders the cycle endpoint state and submits log intents only; thin-client/SSOT audit passes — no gate/irregularity/prediction logic in client code, grep for the protocol constants finds nothing, prediction shown only when server says Calibrated; both themes render per the strict color spec; Arabic RTL mirrors correctly (incl. progress/calibration bars); strings are ICU, none concatenated; "Consult your doctor" deferral rendered verbatim | 13 (§5 client template, §6), 03 §6, 05 (§3 routing /engines/cycle, §4 query keys, §5b RTL strategy, component inventory), 02 §2.1 (Divergence #1) |
S-<date>-M6-mcp-NN | mcp | Cycle status read-only MCP tool(s): cycle_status → GET /api/v1/engines/cycle/state, returned verbatim. No prediction tool beyond what the backend exposes — the engine's fail-safe rules apply unchanged; no client-side inference. | S-<date>-M6-backend-NN; M0.8 (MCP server + CLI skeleton) | MCP template: each tool calls the real API with personal-access-token auth, returns server state verbatim, holds no protocol logic and no AI keys; the excluded tools stay excluded (no cycle/fertility prediction tool beyond the backend state/history reads; the general complete_break exclusion still holds) | 13 (§5 MCP template, §6), 11 (§thin-layer rule, planned tools, deliberately-excluded tools incl. "no cycle/fertility prediction tool"), 03 §6 |
S-<date>-M6-apple-NN | apple | Native Swift Meds & Cycle screen (iOS; + macOS/widget surfaces as they apply) rendering cycle engine state against the live API — this session closes Divergence #1 on iOS. No local re-implementation of the 3-cycle gate or irregularity rule. | S-<date>-M6-backend-NN; M1 apple (Apple project scaffold + reference engine pattern) | Client-engine template: renders the cycle endpoint state and submits log intents only; thin-client/SSOT audit passes (no gate/prediction/irregularity logic in Swift — grep finds no protocol constants; iOS never re-derives the rule); both themes per the strict color spec; Arabic RTL mirrors correctly; strings are ICU/String-Catalog, none concatenated; "Consult your doctor" deferral present | 13 (§5 client template, §6), 03 §6, 07 (§principles, project layout, iOS app row — "Meds & Cycle tracking (closes Divergence #1)"), 02 §2.1 |
S-<date>-M6-android-NN | android | Native Kotlin/Compose cycle screen (phone; + Wear/Glance as they apply) rendering cycle engine state; cycle-log intents only. | S-<date>-M6-backend-NN; M1 android (Android scaffold + reference engine pattern) | Client-engine template: renders the cycle endpoint state and submits log intents only; thin-client/SSOT audit passes (no protocol logic in Kotlin, grep finds no constants); both themes per the strict color spec; Arabic RTL mirrors correctly; strings are ICU, none concatenated; "Consult your doctor" deferral present | 13 (§5 client template, §6), 03 §6, 08 (project layout, engine screen pattern, en/ar parity), 02 §2.1 |
S-<date>-M6-desktop-NN | desktop | Electron cycle surface, reusing the M6 web components (13 §3 step 7) — thin renderer of server cycle state. | S-<date>-M6-backend-NN; S-<date>-M6-web-NN (component reuse); M1 desktop (Electron scaffold pattern) | Client-engine template: renders the cycle endpoint state and submits log intents only; thin-client/SSOT audit passes (reused web components carry no protocol logic; PAT auth via safeStorage, no keys embedded); both themes per the strict color spec; Arabic RTL mirrors correctly; strings are ICU, none concatenated | 13 (§5 client template, §6), 03 §6, 09 (Electron layout, web-component reuse), 05 (reused components) |
Session count: 6 (backend, web, mcp, apple, android, desktop — no chrome).
4. Notes & gotchas
Legacy precedents worth reusing
internal/protocols/cycle.goand its*_test.goare the executable protocol specification — validate the newcore/engine against those cases first (12 §2). Constants:MinCyclesForPrediction=3,IrregularThresholdDays=9, last-6-cycle average, ovulation −14d, fertile window −5/+1d, BBT +0.2 °C ×3 over prior 6-day mean, Naegele +280d.- The legacy
SafetyProvider/NoSafetyData{}seam (no drug DB wired, by design) is the correct posture — keep the safety hook empty rather than inventing safety data. - Legacy per-user CRUD already modeled
cyclesandcycle_days(andpregnancieswith a partial-unique "one ongoing per user") — a schema precedent, but re-evaluate the legacy anti-patterns (text ids → use UUIDs; absent dates as empty text → use proper defaults; no FK to the auth users table; CSV-text fields).
Liabilities to avoid
- #1 liability — duplicated protocol logic on a client. The legacy iOS app re-implemented engines locally and never called the API; that is exactly the drift this module closes. Every client session's done-criteria include the thin-client/SSOT grep audit for precisely this reason.
- Sensitive-data routing. Fertility data is sensitive: the legacy fix replaced ToGO's event broker (which leaked events across users) with a per-user SSE hub carrying resource names only, no state (
12 §4). Any cycle event/SSE emitted here must be per-user with zero cross-user leakage; foreign rows return 404 not 403. Add an isolation assertion to the backend integration test. - Never emit a prediction outside
Calibrated. The prediction field must carry theunavailablesentinel (not SQL NULL) plus a reason inInsufficient/Suspended. Clients must show a prediction only when the server state isCalibrated— no client-side "we probably have enough data" heuristic. - No medical certainty. Predictions are behavioral extrapolations, never fertility guarantees or contraceptive advice; all such copy defers to "Consult your doctor" (Core Opinion 2).
PENDING DESIGN / PENDING RATIFICATION
- PENDING RATIFICATION: the quantitative definition of "irregularity" is undefined in spec (
03 §6.2, Open Question #6). Ratify the legacy 9-day-spread / last-6-average / BBT numbers (or a replacement) during the backend session and encode them literally incore/. - PENDING (spec open,
03 §6.7): whether recalibration afterSuspendedrequires a fresh full 3-consecutive-cycle run (planning default: yes, fail-safe); and whether backfilled historical cycles count toward "consecutive" (planning default: stored but not counted — only forward-logged cycles calibrate). - PENDING PRODUCT/CLINICAL (
03 §7): whether an active contraceptive method (M7) should suspend cycle predictions — a product/clinical decision, not an engine guess. Coordinate at M7, do not hardcode an interaction now. - PENDING SCOPE: pregnancy dating (Naegele +280d) and a
pregnanciestable — decide in the backend session whether it is in M6 scope or deferred; it is a legacy precedent, not a spec-fixed M6 deliverable. - PENDING DESIGN: the calibration-progress UI (Insufficient state) and the cycle/prediction screen layouts across web/apple/android come from the separate design track (
02 §2.1,03 §6.3).
5. Handoff reminder
At the end of every M6 session, per 13 §6: write one session-handoff memory to the healthdebug brain (retain, then memory/edit to attach metadata — retain cannot set metadata) using the exact handoff content structure, with source_ref: handoff/M6-cycle-fertility/<surface>/<n> (unique per session; never reuse) and metadata: {project:"healthdebug", category:"session-handoff", era:"greenfield", status:"current", surface:"<surface>", module:"M6", title:"Handoff <session-id>", next:"<module+surface>"}. Then update the single build-ledger memory (source_ref: build-ledger, edited in place) so one recall shows the whole frontier and the recommended next session. Any architectural decision or new open question also gets its own classified memory and, if it changes the plan, an edit to the relevant plan doc.