# G3 — GYN fertility: infertility assessment · semen (WHO 2021) + andrology · previous ART · OI/IUI and IVF/ICSI/FET cycles as cards with their monitoring · diagnostic laparoscopy/hysteroscopy · the husband as a linked patient — and their summaries

Read `_COMMON.md` first. G3 is **two serial WPs**: G3a (BE: install the 6 fertility modules from the committed schema JSON, the semen / fertilisation computed keys incl. **per-row computed**, treatment cycles = C1 episodes with record-scope fields, the husband link, the 6 summary templates) → G3b (FE: cycle cards + monitoring grid, semen rows with the live WHO flag, husband link UX). Like G2, the assessment modules are **definitions in the G1 engine — no table, no component per module**; only the cycle cards and the husband link get bespoke UI.

**Depends on:** G1a ✅ (`Modules/Gyn`, `gyn_schema.json` — the 6 fert modules are already exported, `GynFieldPackSeeder`, `GynModuleRegistry`, `GynComputed`, `GynModuleValues`, `GynFieldWriteGuard`, `GynFeatures` `fertility`), G2a ✅ (summary-template + JS-fixture approach `export-g2a-fixtures.mjs`, the PCOS/endometriosis plan codes that open `oi`/`ivf`), C1a ✅ (`clinical_episodes` kind `treatment_cycle`, `encounters.episode_id`, record-scope answers `record_type=clinical_episode`, `PUT clinic/records/{type}/{id}/fields`), C1b ✅ (`fertility` + `couple` summary types already declared for `gyn_ob` in `ClinicalTypeRegistry::defaults()`). G3b needs G3a ✅, G1b ✅, G2b ✅.

## Spec (read before coding)
- `/home/moonui3/public_html/knowledge-base/plans/gyn-obstetrics-module-src/p3c-schema-fert.js` — **the whole file** (6 modules: sections, fields, options, show_if, `sum`). `Modules/Gyn/database/data/gyn_schema.json` is its committed machine form — confirm the 6 modules are complete there (do not hand-edit it).
- `p4a-engine-calc.js` `WHO21` (line 10), `CALC.semenTotal`, `CALC.semenWho`, `CALC.fertRate` (lines 21–23), `fmtD`; `p4c-scenarios.js` scenario 1 «عقم ثانوي + ألياف + دورات تنشيط» (infertility · semen · diag_lap · oi with 3 cycles = test fixtures; **no ivf / prev_art scenario** — hand-made cases).
- `../design-gyn-integration.md` §2 rows `treatment_cycles`, Summary generator, صلاحيات `gyn.*` · §3 (modules = cards; `oi/ivf` = cycle cards) · §4 computed list (`semenTotal, semenWho, fertRate`) · §7 decision 21 (husband = linked patient).
- `../visit-shell-prototype.html` `GYN_SUM` (line ~1001: `fertility` = profile, obhx, menstrual, infertility, semen, prev_art, imaging, oi, ivf, diag_lap, dx, team; `couple` = infertility, oi, ivf, semen, prev_art) and the `fert` tab (oi rendered `card:true`, one card per cycle).
- `../master-plan.md` G3 row (acceptance: **an OI cycle is a card with its monitoring · the semen interpretation is automatic · IVF with its outcome**).
- `../legacy-obgy-options-analysis.md` — read-only reference for the old infertility sheet (COS/IUI/ICSI/FOLLICULOM tables, husband diagnosis lists). Never connect to the legacy system, never persist its credentials.
- `G1-gyn-foundation.md` decisions 2–8 and `G2-gyn-core-modules.md` decisions 7–8 — binding here too.

## Decisions (do not re-litigate)
1. **Assessment modules** `infertility`, `semen`, `prev_art`, `diag_lap` = **encounter-scope L4 answers** (`clinical_type='gyn_ob'`, `module_key`, category `custom`, `fill_role=doctor`, not in intake), exactly as the JSON (repeat rows `semen.sa`, `prev_art.cyc` via `repeat_group` + `repeat_index`). Module visibility = the JSON module show_if (infertility opens on category `infertility` or complaint «infertility» open; semen on `infertility.open_semen` / male abnormal; prev_art on previous IVF/ICSI/FET/IUI/donor; diag_lap on plan laparoscopy/hysteroscopy/tubal surgery; oi on plan OI/IUI or PCOS plan letrozole/clomiphene; ivf on plan IVF/ICSI or semen plan ICSI/TESE+ICSI).
2. **Feature:** all 6 carry `feature='fertility'` (`gyn.feature.fertility` off ⇒ the `infertility` category, the 6 modules, the cycle actions and their summaries disappear; writes 422 `gyn_feature_off` through the existing `GynFieldWriteGuard`; the cycle endpoints 422 too). PCOS / endometriosis / tubal categories stay (G1a deviation stands).
3. **A treatment cycle IS a C1 episode** — `clinical_episodes` `kind='treatment_cycle'`, `clinical_type='gyn_ob'`, `categories=['infertility']` (so an attached visit keeps the infertility module open), `ref_type='gyn.cycle.oi'|'gyn.cycle.ivf'` (the card module), `title` = «Ovulation induction — cycle 3» / «ICSI — attempt 2». **Its fields are record-scope answers** (`record_type='clinical_episode'`, `record_id=<episode>`), written through the shared `PUT api/clinic/records/clinical_episode/{id}/fields` — no GYN value endpoint. **Deviation from design-gyn §2 / G1 decision 2** (`treatment_cycles` + `cycle_monitoring` Gyn tables): C1a already gives the entity (status, dates, team, timeline, encounter link) and record scope gives its fields; the monitoring visits are the module's own `table` fields (`gyn_oi_cycle_visits`, `gyn_ivf_stim` — one row per scan in `value_json`), so **no new table**. Lead follicle / follicle counts are computed from those rows at read time.
4. **oi / ivf field definitions** are seeded with `scope='record'` (the encounter path then refuses them — one home per value). OI's p3 `cycle` repeat becomes the episode itself: its children are seeded **flat** (`repeat_group=NULL`, row 0 of the record) and the repeat parent `gyn_oi_cycle` is not seeded. IVF is already flat. The module read (`GET api/gyn/schema`) reports `scope='record'`, `storage='episode'`, `record_type='clinical_episode'`, `episode_kind='treatment_cycle'` for both.
5. **Cycle lifecycle** (`Modules/Gyn/app/Services/TreatmentCycleService.php`, through C1's `EpisodeService` — no direct episode writes except the cancel status): open (module `oi|ivf`, optional `encounter_id` of the same patient — attached when editable and unattached; cycle number = the patient's count of that module + 1; initial answers `no/cd1/aim` (oi) or `kind/attempt/protocol/cd1` (ivf) written as record answers) · **one open cycle per patient at a time** (422) · close (outcome code required unless already answered → written as the module `outcome` answer; `EpisodeService::close`) · cancel (reason; outcome `cancelled`; episode status `cancelled` ⇒ C1a freezes its record answers) · attach a visit (`EpisodeService::attachEncounter`). List / show return the read model `{id, module, kind, number, status, opened_at, closed_at, title, encounter_ids, fields (code ⇒ value), monitoring: [{…row, lead_mm, counts:{r10,r14,r17,l10,l14,l17}}], monitoring_count, lead_follicle_mm, derived: {fert_rate, amh, afc}, outcome, summary}`.
6. **Computed** (in `GynComputed`, ported line by line from p4a, `{value, text, tone}`): `gyn_semen_total` (vol × conc, 1 decimal; null when either missing), `gyn_semen_who` (text/tone exactly `CALC.semenWho` — Azoospermia ⇒ `bad`, oligo/astheno/terato ⇒ `warn`, normo (+ «, low volume») ⇒ `''`, nothing ⇒ null; `value_json.flags` adds the per-parameter WHO 2021 lower-reference flags `{vol, conc, total, tm, pr, morph, vit}` = `low|ok` (limits 1.4 mL, 16 M/mL, 39 M, 42 %, 30 %, 4 %, 54 %)), `gyn_fert_rate` (round(2PN / (inseminated ‖ MII) × 100); null without a denominator). **Per-row computed is new:** `semen.sa.total` / `.interp` are repeat-child computed questions — `HistoryService::recomputeDerived` computes a computed question that has a `repeat_group` once per row (row-local values), upserts at that `repeat_index`, and removes the derived row of an index whose inputs are gone. `saveRecordAnswers` keeps C1a's «no derived recompute» rule: `fert_rate` and the cycle mirrors (`ivf.amh/afc` ← the patient's latest `gyn_imaging_amh` / `gyn_imaging_afc_total` answer across visits) are computed at read time by the cycle service and the IVF template.
7. **Semen values stay module answers** on her visit (the SA repeat rows) — not D1 observations (observation definitions are Clinic-seeded and the values belong to the husband). Ordering the husband's LIS semen test is P1/LIS work (`subject=spouse` + his `lab_patients` row) — out of scope.
8. **The husband as a linked patient** (design-gyn decision 21): `lab_patients.spouse_patient_id` (nullable, guarded Gyn migration, written with the query builder — the LIS model is not edited), set on **both** rows. `Modules/Gyn/app/Services/GynSpouseService.php`: link an existing patient of the company (not herself; neither side linked to someone else ⇒ 422), create the husband through `LabPatientWriteService::create` (MRN + partner like any patient; `gender=male`; name / phone / blood group prefilled from her `spouse_name` / `gyn_spouse_phone` / `gyn_spouse_blood`+`gyn_spouse_rh` answers, request values win; no name ⇒ 422) and link, unlink (both sides). Read model `{linked, spouse: {id, mrn, name, phone, age, blood_group}|null, answers: {spouse_name, gyn_spouse_age, …}}`; the header extension adds `spouse_patient_id` / `spouse_name`. **The `gyn_spouse_*` answers stay the reception intake record in G3** (turning them into mirrors of the linked row reshapes G1a's intake — Open, later).
9. **Summaries** = PHP templates `Modules/Gyn/app/Summary/{Infertility,Semen,PrevArt,Oi,Ivf,DiagLap}Template.php`, ported **line by line** from the p3c `sum` (English, identical output to the JS for the same values). `semen` interpolates `CALC.semenWho` — its `{t, cls}` result renders as `.t` (same documented deviation as G2a's rotterdam/rmi). **`oi` / `ivf` render every non-deleted cycle episode of the patient of that module** (the prototype card lists all cycles; OI as one «Ovulation induction: Cycle 1 …. Cycle 2 …» line, IVF one ported line per cycle joined by a space), `visits.length` = the monitoring rows of the cycle. The `fertility` / `couple` types already exist (C1b) — nothing to register. The summary module rule is `show_if OR answered`; `GynShowIfContext::answeredModules` adds `oi`/`ivf` when the patient has a cycle of that module, and **stops counting record-scope answers** as visit answers (they are `encounter_id NULL` and leaked into every visit).
10. **Permissions:** `gyn.cycles.view` (list/show), `gyn.cycles.manage` (open/close/cancel/attach **and** writing oi/ivf fields — the `GynFieldWriteGuard` refuses cycle-module fields without it, 403). Husband: `GET` needs `clinic.patients.view`; link/unlink `clinic.patients.update`; create `clinic.patients.create` (reception does it — no gyn.* needed). Grants (additive, `GrantGynPermissionsSeeder` + bridge migration): owner all; anchor `clinic.encounters.view` → cycles.view; `clinic.encounters.update` → cycles.view + manage; `gyn_consultant` / `gyn_doctor` / `gyn_team_member` view + manage; `gyn_nurse`, `gyn_sonographer` view.

## Shared-file rules
- Orchestrator-owned — REPORT: `Modules/Clinic/routes/api.php`, `RolePermissionSeeder.php`, `StandardRolesSeeder.php`, `ClinicPermissionDependencies`, setting seeders, permission-labels, `config/moonstack.php`. You own `Modules/Gyn/routes/api.php`.
- Hot file owned during G3a (minimal, additive): `Modules/Clinic/app/Services/HistoryService.php` — per-row computed only (decision 6). Everything else lives in `Modules/Gyn` (`src/app/features/gyn` for G3b).
- FE i18n: targeted keys under `GYN.*` only, ar + en; **no `I18N_VERSION` bump**.

---

## G3a — BE (migrations `2026_11_03_390000`–`390099`, in `Modules/Gyn/database/migrations`)

### Files
- `Modules/Gyn/app/Support/GynSchema.php` (+`G3_MODULES`, `CYCLE_MODULES`, `CYCLE_SKIP`), `GynComputed.php` (+3 keys), `GynShowIfContext.php` (answered modules), `GynFieldWriteGuard.php` (cycle permission), `GynExtensions.php` (templates + header), `Services/GynPackInstaller.php` (default set), `database/seeders/GynFieldPackSeeder.php` (cycle-module overrides), `database/seeders/GrantGynPermissionsSeeder.php`, `Http/Controllers/GynSchemaController.php` (module scope/storage), `routes/api.php`, `lang/{ar,en}/gyn.php`.
- New: `Services/TreatmentCycleService.php`, `Services/GynSpouseService.php`, `Support/SemenWho.php` (WHO 2021 limits + flags), `Http/Controllers/TreatmentCycleController.php`, `Http/Controllers/SpouseController.php`, `Summary/{Infertility,Semen,PrevArt,Oi,Ivf,DiagLap}Template.php`, `database/data/tools/export-g3a-fixtures.mjs` → `tests/Fixtures/g3a_expected.json`.
- Migrations: `390000_install_gyn_fertility_modules` (data, pattern 380000), `390001_add_spouse_patient_id_to_lab_patients` (guarded, nullable + index), `390002_gyn_fertility_permission_bridge` (pattern 360020).
- Clinic: `Services/HistoryService.php` (decision 6).

### Interfaces (`api/gyn`, company-scoped, patient data scope through `Patient::query()`)
| Method | Path | Permission | Body / result |
|---|---|---|---|
| GET | `patients/{patient}/cycles?module=&status=` | `gyn.cycles.view` | list of read models (newest first) |
| POST | `patients/{patient}/cycles` | `gyn.cycles.manage` | `{module: oi\|ivf, encounter_id?, kind?, aim?, cd1?, protocol?, number?}` → 201 read model |
| GET | `cycles/{episode}` | `gyn.cycles.view` | read model |
| POST | `cycles/{episode}/close` | `gyn.cycles.manage` | `{outcome?, reason?}` |
| POST | `cycles/{episode}/cancel` | `gyn.cycles.manage` | `{reason}` |
| PUT | `cycles/{episode}/encounter` | `gyn.cycles.manage` | `{encounter_id}` attach a visit |
| GET | `patients/{patient}/spouse` | `clinic.patients.view` | husband read model |
| PUT | `patients/{patient}/spouse` | `clinic.patients.update` | `{spouse_patient_id}` link |
| POST | `patients/{patient}/spouse` | `clinic.patients.create` | `{name?, phone?, blood_group?, date_of_birth?}` create + link → 201 |
| DELETE | `patients/{patient}/spouse` | `clinic.patients.update` | unlink |
Cycle fields: the shared `GET/PUT api/clinic/records/clinical_episode/{id}/fields`.

### Tests — `Modules/Gyn/tests/Feature/G3aGynFertilityTest.php` + `Modules/Gyn/tests/Unit/G3aGynFertilityComputedTest.php` (prefix `g3a_`)
installer seeds the 6 modules (oi/ivf `scope=record`, OI children flat, repeat parent absent) idempotently; category `infertility` opens infertility, `open_semen` opens semen, plan OI opens oi; two SA rows get their own derived total + WHO interpretation (row 1 oligo+astheno warn, row 2 normo) and clearing a row's conc removes its derived total; computed vectors = the JS (`semenTotal/semenWho/fertRate` incl. azoospermia bad, all-null, low volume, OAT, `inseminated` 0 → MII); infertility / semen / diag_lap summaries equal the JS for scenario 1; three OI cycle episodes with record answers + monitoring rows give the scenario-1 OI line; IVF hand-made cases (fresh ICSI with outcome, freeze-all with PGT, cancelled) equal the JS incl. `fert_rate`; one open cycle at a time; cancel freezes the record; close writes the outcome; cycle field write without `gyn.cycles.manage` 403; fertility off ⇒ cycle endpoints 422, module writes 422, modules hidden from schema + summary; husband create from the spouse answers (male, MRN, both rows linked), link refuses self / already linked, unlink clears both, header carries `spouse_patient_id`. Run also every `Modules/Gyn/tests`, `Af2IsolationTest`, `PlatformD0FieldIsolationTest`, `ClinicProfilePackTest`, `C1aEntitiesTest`, `C1bSummaryRecordTest`, `E1eAnswersTest`, `HistoryEngineTest` — keep green.

### Out of scope (G3a)
Any FE; obstetric / urogyn / sexual modules; LIS ordering for the husband; turning the `gyn_spouse_*` answers into mirrors; a fertility statistics dashboard; follow-up rules for cycles; feature-aware hiding of summary *types* (needs a Clinic hook — report).

---

## G3b — FE (`src/app/features/gyn/`; no migrations)

Design consult (cheap agent, not Fable — owner rule) on: the cycle card (header «OI — cycle 3 · CD1 … · lead 18 mm · → outcome», collapsed vs open), the monitoring grid (one row per scan, R/L follicle sizes, ET, E2, decision, add-row on a phone), the semen row's live WHO chip, the husband link panel.

### Do
1. `gyn-cycle-cards.component.ts` — the `oi` / `ivf` module card: lists the patient's cycles (`GET patients/{p}/cycles?module=`), «＋ دورة جديدة» (`POST`), each cycle a card rendering its record fields with `gyn-module-form` in record mode (`PUT clinic/records/clinical_episode/{id}/fields`), the `table` field as the monitoring grid, close / cancel actions, read-only when cancelled.
2. `computed.ts` (G1b file — extend): `semenTotal`, `semenWho`, `fertRate` mirrors identical to PHP for the G3a vectors (`g3a_expected.json`, `npx tsx` one-off; report).
3. Husband panel in the profile / spouse section: linked → name + MRN + link to his file + unlink; not linked → «ربط بملف موجود» (patient search) or «إنشاء ملف للزوج» (prefilled).
4. i18n `GYN.CYCLE.*`, `GYN.SPOUSE.*`, `GYN.SEMEN.*` (ar + en), no version bump. `npx ng build --base-href /app/` green. No deploy.

---

## Report back (each WP)
Files created/edited; installer counts; test output with pass counts; migrations + dev commands; Gyn route lines (Clinic route lines to paste — should be none); permissions/settings needed; CHANGELOG EN/AR; decisions taken; files needed but not touched; any p3c field the JSON lacks or the summary port could not match exactly.
