# G7 — GYN sexual health (private) · FSFI / FSDS-R (scored, baseline vs follow-up) · the couple summary without private content · the de-identified research export

Read `_COMMON.md` first. G7 is **two serial WPs**: G7a (BE: install the 3 p3g modules `sexhx · sexexam · sextx` from the committed schema JSON as **encounter-scope private answers** behind the G1a sexual gate, the FSFI (19 items, 6 domains) / FSDS-R (13 items) item definitions + their scorers, the 3 summary templates, the sexual-health read model (FSFI / FSDS-R series, baseline vs follow-up), the couple-summary privacy proof, the de-identified research export through the SEC2 layer) → G7b (FE: the private module host (dark card), the FSFI / FSDS-R score widgets, the baseline / follow-up card, the research-export screen). Like G2–G6 the modules are **definitions in the G1 engine — no table, no component per module**; only the two questionnaires (score widgets) and the export get bespoke UI.

**Depends on:** G1a–G6a ✅ (`Modules/Gyn`, `gyn_schema.json` — the 3 sex modules are already exported with `priv: true`, `feature: sexual`, every field `is_private=true`, the totals `gyn_sexexam_fsfi` (`score`, `computed_key gyn_fsfi`, `meta.items` d1…p19) and `gyn_sexexam_fsds` (`computed_key gyn_fsds`, q1…q13); `GynFieldPackSeeder`, `GynFieldWriteGuard` (sexual field without `gyn.sexual.edit` → 403), `GynModuleRegistry::visibleFor` (priv modules only with `gyn.sexual.view`), `GynFeatures` `sexual` (the `sex` category group already locks), `GrantGynPermissionsSeeder` (`gyn.sexual.*` ⇒ `clinic.fields.private`), the G6a PROM pattern `GynProms` + JS-fixture approach `export-g6a-fixtures.mjs`), E1e ✅ (`is_private` read / write gate `clinic.fields.private`, computed on save), C1b ✅ (`ClinicalSummaryService` — `NO_PRIVATE_TYPES = ['couple']`, private modules / fields dropped unless `include_private` + `clinic.fields.private`), SEC2 ✅ (`ClinicExportController`, `Support/Export/Deidentifier`, `AccessAudit::record`, `PatientDataScope`, perms `clinic.export.deidentified|identified`), SEC3 ✅ (`PatientHistoryAnswer` casts encrypt private answers when `security.pii.encrypt` — `value_num` is NULL for an encrypted private answer). G7b needs G7a ✅, G1b ✅, S1 ✅, G6b ✅ (the PROM widget it reuses).

## Spec (read before coding)
- `/home/moonui3/public_html/knowledge-base/plans/gyn-obstetrics-module-src/p3g-schema-sex.js` — **the whole file** (3 modules: sections, fields, options, show_if, `sum`; `FSFI_ITEMS`). `Modules/Gyn/database/data/gyn_schema.json` is its committed machine form (`gyn_sexhx_*`, `gyn_sexexam_*`, `gyn_sextx_*`) — do not hand-edit it. **The field-level `priv` of `red` / `red_action` / `private_note` was flattened** (the module is `priv`, so every field is `is_private=true`).
- `p4a-engine-calc.js` `CALC.fsfi` / `CALC.fsds` (lines 42–43), the CALC_TABLE rows 59 / 160–162 («No private notes by default · couple summary separate», «Private clinical note لا تظهر في couple summary/export»); SUM_TYPES `couple` (line 177).
- `p4b-engine-ui.js` line 68 `buildSummary('couple')` (presenting concern + joint session + plan + exclusion note) — **superseded**, see decision 4.
- `p4c-scenarios.js` scenario 4 «ألم جنسي (Module خاص) — تحليل بالخطوات» (`sexhx · sexexam · sextx` = the fixture).
- `../visit-shell-prototype.html` — the private dark card (`.cc.private`, «🔒 خاص — صلاحية gyn.sexual.view · يُسأل على انفراد · لا يظهر في أي ملخص»), `gSummaryText('couple')` = the fertility modules, the note «الـ modules الخاصة (Sexual health) والـ private notes مستبعدة من كل أنواع الملخص — بما فيها Couple».
- `../design-gyn-integration.md` §2 rows `prom_scores` (`fsfi_total, fsds_total`), `clinical_notes`, صلاحيات `gyn.sexual.view/edit`, §4 computed (`fsfi, fsds`), §5 row «خاص بالنسا» (default = the treating doctor only · the private note excluded from any summary / export except `include_private` with the permission · couple summary a separate type without private), §7 decisions 24 (⏳), G5 (questionnaires ⏳).
- `../design-data-layers.md` §3 (facts vs measurements) and §3.1 `is_private` (needs `clinic.fields.private`).
- `../master-plan.md` G7 row (acceptance: **the module is invisible to anyone but the doctor · the couple summary has no private note**), decision 24.
- `SEC2-access-audit.md` decision «De-identification» + Do §9 (G7 reuses `Deidentifier`).
- `../legacy-obgy-options-analysis.md` — read-only reference. Never connect to the legacy system, never persist its credentials.
- `G1-gyn-foundation.md` decisions 5 / 6 / 8 / 12 (the two private gates), `G6-urogyn.md` decisions 1 / 4 / 9 / 10 — binding here too.

## Decisions (do not re-litigate)
1. **The 3 modules are encounter-scope private answers** (the JSON scope), `feature='sexual'`, category `custom`, doctor fill, `is_private=true` (JSON). No entity, no table. Visibility = the G1a gates, unchanged: `GET gyn/schema` lists them only with `gyn.sexual.view` (and their field definitions only with `clinic.fields.private`); a write needs `clinic.fields.private` (E1e, 403) **and** `gyn.sexual.edit` (guard, 403); feature off ⇒ modules leave the schema, field writes 422 `gyn_feature_off`, every new G7 endpoint 422 `gyn_feature_off`. Default holders = `gyn_consultant` / `gyn_doctor` / `owner` (G1a seeder) — «الافتراضي الطبيبة المعالجة بس».
2. **Defence in depth on the summary:** `GynModuleRegistry::modulesFor` (C1b's ModuleCatalog) hides a `priv` module (never-passing show_if) from a user without `gyn.sexual.view` — so a user holding `clinic.fields.private` for another specialty cannot pull sexual-health text into a `clinic` summary with `include_private=1`.
3. **FSFI / FSDS-R = scored private answers, items seeded in code** (G6a decision 4 pattern; the JSON `score` field cannot hold its items). `Modules/Gyn/app/Support/GynSexual.php` declares both instruments, seeds the **item questions** (single-select, `module_key=sexexam`, **`is_private=true`**, create-only per (company, code), a removed code never resurrected) — the **totals are the JSON's** `gyn_sexexam_fsfi` / `gyn_sexexam_fsds` (computed keys `gyn_fsfi` / `gyn_fsds`, stored derived on save):
   - **FSFI** items `gyn_sexexam_fsfi_{d1,d2,a3…a6,l7…l10,o11…o13,s14…s16,p17…p19}`; options 0–5 (0 = no sexual activity / did not attempt) except **d1, d2, s15, s16 = 1–5** (the instrument's ranges; p3g says `max 5` for all — the scorer is the JS one and does not special-case 0). Domains (sum × factor, 1 decimal): desire ×0.6, arousal ×0.3, lubrication ×0.3, orgasm ×0.4, satisfaction ×0.4, pain ×0.4; a domain only when all its items are answered; total = the 6 domains' sum (0–36) only when all 6 are complete (else `partial (n/6 domains)`, no value); **≤ 26.55 ⇒ «(≤26.55: suggests dysfunction)», tone warn**. Stored `{value, text, tone, domains{desire…pain}}`.
   - **FSDS-R** items `gyn_sexexam_fsds_q1…q13` (0–4); total 0–52 only when all 13 are answered (else `partial (n/13)`); **≥ 11 ⇒ «(≥11: clinically significant distress)», tone warn**.
   - Item labels: FSFI = the p3g `FSFI_ITEMS` short domain labels; FSDS-R = «Distress item n» (never the licensed wording); option labels short anchors. The owner confirms the instrument list (master-plan G5 ⏳).
   - **Separate from `GynProms`** on purpose: the G6a PROM series / `GET patients/{p}/proms` read with `clinic.history.view` only — private instruments must never enter it.
4. **Couple summary = C1b's invariant, no private content at all.** The `couple` type stays `NO_PRIVATE_TYPES` with C1b's module list (infertility · oi · ivf · semen · prev_art — the visit-shell prototype); none of sexhx / sexexam / sextx (all private) ever renders in it, whatever `include_private` or the caller's permissions. **Deviation from p4b `buildSummary('couple')`** (which printed the presenting concern, the joint-session topics / goals / note and the plan): the later prototype + design-gyn §5 + master acceptance win; the `gyn_sextx_couple_note` label «تظهر في Couple summary» is now misleading (G7b: relabel in the UI hint only). A consented «shared disclosure» subset would need a Clinic engine allow-list (field-level `shareable`) — **reported, not implemented**.
5. **Summaries** = `Modules/Gyn/app/Summary/{Sexhx,Sexexam,Sextx}Template.php`, ported **line by line** from the p3g `sum` (English, identical to the JS). They only ever run when the engine let private content through (`clinic` type + `include_private` + `clinic.fields.private` + `gyn.sexual.view`); `SummaryInput::includePrivate` is the JS `S && !S.nopriv` gate for the sexhx red-flag / private-note tail. `CALC.fsfi` / `fsds` `{t, cls}` render as `.t` (G2a / G6a deviation). The FSFI / FSDS-R lines read the item answers (the sexexam template rebuilds `v.fsfi` / `v.fsds` from the item codes, the `SuiTemplate` ICIQ pattern).
6. **Sexual-health read model** `GET gyn/patients/{p}/sexual-health` (`gyn.sexual.view` + `clinic.fields.private`, feature on): `{fsfi: {series [{encounter_id, date, value, text, tone, domains, phase}], baseline, latest, change}, fsds: {series, baseline, latest, change}, follow_ups [sextx fu rows: date, distress, fsfi, goal_met], features.sexual}`. Baseline = the latest visit whose `fsfi_phase` = Baseline (else the first total); latest = the newest total; change = latest − baseline (FSFI higher = better → `improved` when > 0; FSDS-R lower = better). Values are decoded in PHP from the model (SEC3: an encrypted private answer has `value_num` NULL — never order / filter on it in SQL). Partial totals are not series points.
7. **De-identified research export** `GET gyn/research/export?from=&to=&modules[]=&include_private=0` → a zip of `answers.csv` (long: patient_key, encounter_key, encounter_year, age_years, module_key, field_code, input_type, repeat_index, value, value_num), `scores.csv` (one row per scored total per visit: instrument, value, tone, + FSFI domains) and `dictionary.csv` (field_code, module_key, label_en, input_type, options `code=label|…`). Rules — **stricter than SEC2's generic export**:
   - route `permission:clinic.export.deidentified`; private modules (sexhx / sexexam / sextx) only with `include_private=1` **and** `clinic.fields.private` **and** `gyn.sexual.view` (a request naming a private module without them → 403; without `include_private` they are simply left out); a feature-off module requested → 422 `gyn_feature_off`, otherwise silently excluded; an unknown module → 422.
   - only `gyn_ob` encounters of the company, the date range on the visit date, the caller's `PatientDataScope`; encounter-scope answers of those visits only (record-scope fields are out of G7a).
   - values: coded choices (codes; multi = `|`-joined codes), booleans, numerics, score / computed **numeric value only** (their text may embed dates), dates → **year**; **never** free text (`text`, `textarea`), `media`, `table` / `repeat` parents, nor the strictly-private codes `gyn_sexhx_red`, `gyn_sexhx_red_action`, `gyn_sexhx_private_note` (safety disclosures — excluded whatever the flags).
   - every row passes through SEC2's `Deidentifier::row()` (pseudonym `patient_key` / `encounter_key` identical to SEC2's export, DOB → age at the visit, dates → years, identity / free-text keys dropped); values read through the `PatientHistoryAnswer` casts (SEC3 — never raw SQL on `answer_value`).
   - recorded with `AccessAudit::record('export', null, 'export', null, {kind: 'gyn_research', deidentified: true, rows, rows_by_file, patient_count, modules, include_private, filters})` — the Gyn route group's `RecordPatientAccess` only audits `RouteMap` names, so the explicit call is mandatory.
8. **Permissions:** no new `gyn.*` (G4a / G6a precedent). The read model reuses `gyn.sexual.view` (+ `clinic.fields.private`); the export reuses `clinic.export.deidentified`. Report: whether `gyn_consultant` / `gyn_doctor` should hold `clinic.export.deidentified` is the owner's policy call (not granted in G7a).

## Shared-file rules
- Orchestrator-owned — REPORT: `Modules/Clinic/routes/api.php` (none needed), `RolePermissionSeeder.php`, `StandardRolesSeeder.php`, `ClinicPermissionDependencies`, setting seeders, permission-labels, `config/moonstack.php`, SEC2's `RouteMap` (the G7 read-model route name for the view audit). You own `Modules/Gyn/routes/api.php` and `GrantGynPermissionsSeeder` (untouched in G7a — decision 8).
- No Clinic file is edited in G7a (hooks: `ComputedRegistry::extend`, `SummaryTemplateRegistry`, the ModuleCatalog binding, `Deidentifier`, `AccessAudit`, `PatientDataScope`).
- FE i18n (G7b): targeted keys under `GYN.*` only, ar + en; **no `I18N_VERSION` bump**.

---

## G7a — BE (migrations `2026_11_03_430000`–`430099`, in `Modules/Gyn/database/migrations`)

### Files
- Edit: `Support/GynSchema.php` (+`G7_MODULES`, `SEXUAL_STRICT`), `Support/GynComputed.php` (+`gyn_fsfi`, `gyn_fsds`), `Support/GynModuleRegistry.php` (decision 2), `Support/GynExtensions.php` (3 templates), `Services/GynPackInstaller.php` (default set + the item seed), `Http/Controllers/GynSchemaController.php` (the FSFI / FSDS-R items in the sexexam fields + the `proms` map entries), `routes/api.php`, `lang/{ar,en}/gyn.php`.
- New: `Support/GynSexual.php`, `Services/GynSexualService.php`, `Services/GynResearchExport.php`, `Http/Controllers/GynSexualController.php`, `Summary/{Sexhx,Sexexam,Sextx}Template.php`, `database/data/tools/export-g7a-fixtures.mjs` → `tests/Fixtures/g7a_expected.json`.
- Migration: `430000_install_gyn_sexual_modules` (data, pattern 420000: the 3 modules + the FSFI / FSDS-R items on every gyn_ob company).

### Interfaces (`api/gyn`, company-scoped, patient data scope)
| Method | Path | Permission | Result |
|---|---|---|---|
| GET | `patients/{patient}/sexual-health` | `gyn.sexual.view` (+ `clinic.fields.private` in the controller) | decision 6 |
| GET | `research/export` | `clinic.export.deidentified` (+ decision 7 checks) | zip (decision 7) |
Fields: the shared encounter answers path (E1e + guard gates).

### Tests — `Modules/Gyn/tests/Feature/G7aGynSexualHealthTest.php` + `Modules/Gyn/tests/Unit/G7aGynSexualComputedTest.php` (prefix `g7a_`)
installer seeds the 3 modules (encounter scope, all private) + the 32 items (private) idempotently; FSFI / FSDS-R vectors = the JS (complete, partial, all-empty, the 26.55 / 11 boundaries); the 3 summary ports = the JS for scenario 4 (+ hand-made edge cases, the private tail on / off); save path: FSFI / FSDS-R items → the totals derived (`value_json.domains`); privacy: schema hides the modules without `gyn.sexual.view`, write without `gyn.sexual.edit` 403, without `clinic.fields.private` 403; a `clinic` summary with `include_private` by a `clinic.fields.private` holder **without** `gyn.sexual.view` has no sexual section; the **couple** summary for the full scenario with `include_private=1` by the doctor has no sexhx / sexexam / sextx section and none of the FSFI / red-flag / private-note / couple-note strings; the read model (baseline vs follow-up, 403 / 422); the research export (no identity columns, pseudonym = SEC2's, years only, no free text, strict codes never, private modules only with the flags, 403 / 422, one `access_audit` export row); SEC3 encryption on: scoring, summary exclusion, read model and export still hold. Run also every `Modules/Gyn/tests`, `Sec2AccessAuditTest`, `Sec2DeidentifierTest`, `Modules/Core/tests/Feature/Sec3PiiEncryptionTest`, `C1bSummaryRecordTest`, `E1eAnswersTest` — keep green.

### Out of scope (G7a)
Any FE; a consented couple-shareable subset (decision 4 — Clinic engine change); educational materials («مواد تعليمية», master G7 row — patient handouts belong to P5 templates / owner content); k-anonymity / small-cell suppression of the export; record-scope fields in the export; licensed questionnaire wording; a sexual-health D2 alert for red flags (safety plan stays clinical).

---

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

Design consult (cheap agent, not Fable — owner rule) on: the private module card (dark `.cc.private`, «🔒 خاص» tag, asked individually), the FSFI widget (19 items grouped by the 6 domains, live domain + total chips, the 26.55 cut-off), the FSDS-R widget (13 items, ≥ 11), the baseline / follow-up card from `GET patients/{p}/sexual-health`, the research-export dialog (date range, modules, include-private toggle only with the permissions, the de-identification note).

**G7a hand-off notes:** `GET gyn/schema` lists the FSFI / FSDS-R item fields inside `sexexam` `fields[]` (after the module's schema fields) and its `proms` map `{gyn_sexexam_fsfi: {instrument: 'fsfi', items, scale: '2–36', domains: {desire: [codes]…}}, gyn_sexexam_fsds: {instrument: 'fsds', items, scale: '0–52'}}`; the totals are `score` fields (the widget the module form defers). Item `meta` = `{sexual_score: fsfi|fsds, score_total, score_item}` (own keys — never GynProms' `prom_*`, so a private item is never read as a urogyn PROM). `computed.ts` mirrors `fsfi` / `fsds` (vectors in `g7a_expected.json`). The summary card shows no sexual section in the couple type by design (decision 4).

### Do
1. Private module styling in `gyn-module-host` for `priv` modules; the score widgets for `gyn_sexexam_fsfi` / `gyn_sexexam_fsds` (reuse the G6b PROM widget with domain grouping).
2. The sexual-health card (series, baseline / follow-up / change).
3. The research-export dialog (`clinic.export.deidentified`; include-private only with `clinic.fields.private` + `gyn.sexual.view`).
4. `computed.ts` identical to PHP for the G7a vectors (`npx tsx` one-off; report).
5. i18n `GYN.SEXUAL.*`, `GYN.RESEARCH.*` (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 — none); permissions/settings needed; CHANGELOG EN/AR; decisions taken; files needed but not touched; any p3g field the JSON lacks or the summary port could not match exactly.
