# G1 — GYN foundation: the `gyn_ob` pack (schema → definitions), previous pregnancies + G/P/A/L, categories, module host, default layout, features, roles, clinic summary, max lab grid

Read `_COMMON.md` first. G1 is **two serial WPs**: G1a (BE: `Modules/Gyn` + schema export + seeder + obstetric history + categories + computed + summaries + permissions) → G1b (FE: `features/gyn` + shell registration + header slot + lab grid). Read the shared parts plus your own section.

**Depends on:** C1a+C1b ✅ (episodes, record scope, `ClinicalTypeRegistry`, `SummaryTemplateRegistry`, `ModuleCatalog` contract, `PatientRecordService::extend`, `TimelineService::extend`), D2a+D2b ✅ (lookup-aware choice validation in `HistoryService`, `clinical_lookups.code/meta`, `ShowIfContextBuilder::extend`, `PatientHeaderExtensions`, conditions/alerts), E1e ✅. G1b needs G1a ✅, S1 ✅, C1c ✅, D2c ✅.

## Spec (read before coding)
- `../design-gyn-integration.md` — **all of it** (esp. §1 decisions 1–8, §2 table, §3 «الـ layout الافتراضي gyn_ob» + «فين الـ cat», §4 types/show_if/computed/vocab/«الشبكة القصوى», §7 decisions 16, 21, G3, G6).
- `../design-visit-shell.md` §2.1, §2.3, §2.6, §3 (categories card, module host, private card, summary, timeline), §9 row «نسا».
- `../design-data-layers.md` §4 rows «الزوج/ة», «الحمل/الرضاعة», «الدورة الشهرية», §5 step 2 (specialty intake), §6 (specialty contract + rules 1–5).
- `../master-plan.md` G1 row (acceptance: new patient — reception fills the husband, nurse the history, the doctor finds G/P/A/L ready · picking «AUB» opens its card · a centre that turns Urogyn off loses those choices).
- `../visit-shell-prototype.html` — the approved GYN UI: `GYN_CATS`, `GYN_INDEX`, `GYN_MODS`, `GYN_SUM` (~line 998–1001), `gev()` (show_if), `gField()`/`gModuleHTML()` (module rendering, «long option list → picker», repeat rows), `CARDS.gyn_cats/gyn_modules/gyn_summary/gyn_timeline` (~1086–1097), the `gyn` SPECIALTIES entry (default layout ~line 515–524), `GYN_FEAT`/`GYN_FERT_CATS`.
- The schema itself (single source of every GYN field): `/home/moonui3/public_html/knowledge-base/plans/gyn-obstetrics-module-src/p3a-schema-core.js` … `p3g-schema-sex.js`; calcs `p4a-engine-calc.js` (`CALC`, `alertsOf`), evaluator `p4b-engine-ui.js` line 11 (`ev`). **Read p3a and p3b fully**; skim the rest for shapes.
- `../legacy-obgy-options-analysis.md` (pick-don't-type rules).

## Current code you build on (read)
- BE: `Modules/Clinic/app/Support/{ShowIfEvaluator,ShowIfContext,ShowIfContextBuilder,ComputedRegistry,ClinicProfilePacks}.php`, `Services/{HistoryService,LookupCatalogService,ClinicProfilePackService}.php`, `Models/{HistoryQuestion,HistoryAnswerOption,ClinicalLookup,ClinicComplaint,PatientHistoryAnswer}.php`, `Enums/{ClinicalType,HistoryInputType}.php`, `Http/Controllers/{VisitLayoutController,PatientProfileFieldController,EncounterHistoryController}.php`, `Clinical/ClinicalTypeRegistry.php`, `Services/Clinical/Summary/*`. Module scaffold to mirror: `Modules/Dental` (module.json, composer.json, `app/Providers/*ServiceProvider.php`, `routes/api.php`, `database/{migrations,seeders}`, `lang`, `tests`). Setting pattern for features: S1 stores client features per type in `clinic.visit_layouts[<type>].features` (`VisitLayoutController`).
- Column limits (verified): `history_questions.code` 64, `section` 64, `module_key` 32, `repeat_group` 32, `computed_key` 32, `lookup_key` 48; `clinical_lookups.lookup_key` 48, `code` 64 (D2a). `lab_patients.blood_group` enum already carries Rh (`A+ … O-`).
- FE: `features/clinic/visit-shell/{card-catalog.ts,visit-layout.model.ts,resolve-layout.ts}` and the shell's card `@switch`; `shared/components/clinical-picker/*`; `features/clinic/shared/catalog-grid-picker/*` (the max-visibility grid, already used by `encounter/orders/rad-orders`); `features/clinic/encounter/orders/lab-orders.component.*` + `lab-catalog.util.ts`; `features/clinic/clinical-types/clinical-types.ts`; D2c's `patient-alert-strip` (specialty slot).

## Decisions already made (do not re-litigate)
1. **GYN code lives in a new nwidart module `Modules/Gyn`** (+ FE `src/app/features/gyn/`), mirroring Dental; shared entities stay in Clinic (C1). The module owns its own `routes/api.php` (you may edit it — it is NOT the Clinic hot file). Enabling it needs one line in `/home/moonui3/moon-erp-be/modules_statuses.json` (`"Gyn": true`) — **you may make exactly that one-line addition** (required to run your tests); report it. Autoload needs no edit: `bootstrap/module-autoload.php` (in composer `autoload.files`) auto-detects every `Modules/*/app` folder and registers its PSR-4 prefix (verify `Modules\Gyn\Tests\` resolves; if not, report — do not edit composer.json).
2. **Where every GYN field lives** (the p3 schema is the source; the 51 modules map as below — G1 implements the rows marked G1, later phases implement theirs on the same rules):

| p3 module(s) | Home | Phase |
|---|---|---|
| `profile` — patient identity (name, mrn, dob, age, phone, nid) | `lab_patients` columns — **not seeded** | — |
| `profile` — weight/height/bmi | D1 observations (core vitals) — not seeded | — |
| `profile` — blood/rh | `lab_patients.blood_group` (has Rh) — not seeded; `patient.rh` exposed to show_if/mirror | G1 |
| `profile` — smoking, marital | core answers (`smoking`, `marital_status`, D2a) — not seeded | — |
| `profile` — marriage_no, marriage_years, prev_marriage*, education | L2 answers `clinical_type=gyn_ob`, scope patient, category profile, fill_role reception, in_intake | G1 |
| `profile` — job, referred_by | core profile `occupation`, `referred_from` (create core, `is_active=false`, if missing) | G1 |
| `profile` — husband section (`h_*`) | L2 answers `gyn_spouse_*` (alias the existing obgy codes: `h_name`→`spouse_name`, `h_job`→`spouse_occupation`; `marriage_years`→`marriage_duration_years`); fill_role reception | G1 |
| `alerts` (drug allergy, DVT/PE, thrombophilia/APS, anticoag, transfusion, airway, custom) | D2 `patient_alerts` + conditions with `is_alert` (codes seeded by D2a) — **not seeded as fields**; the shell's shared alerts/conditions cards cover them | — |
| `history` (medical/surgical/family chips, drugs, allergy note, anaesthesia, transfusion) | D2 conditions / `past_surgical_history` / `family_history` / home meds — not seeded; G1 seeds `V.medHx/surgHx/famHx` vocab into `lookup_key` `condition` / `procedure` with `specialty='gyn_ob'` | G1 |
| `obhx` (previous pregnancies repeat + gpal) | **Gyn table `obstetric_history_rows`** (one row per pregnancy) + computed G/P/A/L in the header slot | G1 |
| `menstrual` | L2 answers, module_key `menstrual`; **`lmp` is scope encounter** with `carry_forward=prefill`, `stale_after_days=35` | G1 |
| `complaint` (list + conditional questions) | list = the shared complaint card (`clinic_complaints` + `clinical_lookups` `complaint`, GYN values seeded with codes); conditional questions = L4 answers module_key `complaint`, show_if on `{k:'complaint', has:<code>}` | G1 |
| `exam` | L4 answers module_key `exam`, category exam (bp/pulse/temp **skipped** — vitals card) rendered inside the shared exam card's specialty slot | G1 |
| `imaging` (TVUS/HSG/SIS/MRI/Pap/HPV/colpo/hysteroscopy/labs-by-number) | L4 answers module_key `imaging` — the «الفحوصات المنظّمة» station card `gyn_imaging` | G1 |
| `team` | C1 team referrals — not seeded | — |
| `dx` | shared diagnosis card (`encounter_diagnoses`); GYN dx list seeded into `lookup_key='diagnosis'` (specialty gyn_ob) for favourites | G1 |
| gyn group: fibroid, aub, endometriosis, adenomyosis, polyp, pcos, pelvic_pain, adnexal, onc, cervical, vulval | L4 answers per module_key, module show_if from the registry | **G2** |
| fert: infertility, semen, prev_art, diag_lap | L4 answers | G3 |
| fert: oi, ivf | Gyn tables `treatment_cycles` + `cycle_monitoring` (+ record-scope answers) | G3 |
| op: operation | C1 `clinical_procedures` + record-scope answers + `hospital_operations` templates | G4 |
| fu: discharge / followup | `clinical_documents` + Rx `kind=discharge` / C1 `follow_up_tasks` | G4 |
| ob: pregnancy | Gyn table `pregnancies` (+ fills core `pregnant` derived) | G5 |
| ob: antenatal | observations (D1) + L4 answers; no table | G5 |
| ob: highrisk, vte, labour, vd, cs, postpartum | answers (record scope `pregnancy` for labour/vd/cs/postpartum) + VTE computed + alert | G5 |
| uro: luts, sui, oab, uroexam, voiding, fistula, meshc, urofu | L4 answers (PROMs = `score`) | G6 |
| uro: popq | observation set + `popq` computed; uds = answers + `clinical_documents uds_report` | G6 |
| sex: sexhx, sexexam, sextx | L4 answers `is_private=true` + `gyn.sexual.view` | G7 |

3. **Module definitions vs field definitions.** Modules (key, group, titles, colour, `priv`, `always`, scope, **module-level show_if**) are **code** — the generated JSON shipped in `Modules/Gyn` and served by `GynModuleRegistry` (implements C1b's `Modules\Clinic\Clinical\Contracts\ModuleCatalog`). Fields are **per-company `history_questions` rows** seeded from the same JSON (so the clinic can hide/require them — D0 rule: `is_active/is_required/labels/sort_order` written on create only). A field is visible iff its module is visible AND its own `show_if` passes.
4. **Schema → JSON export** (one-time, reproducible): a Node script `Modules/Gyn/database/data/tools/export-gyn-schema.mjs` reads `p3a…p3g` from the KB path, strips `<script>` tags, evaluates them together in a `node:vm` context (stub `CALC`, `fmtD`, `alertsOf`, `esc`, `num` as no-ops — they are only referenced inside `sum` functions, which are dropped), returns `{GROUPS, CATS, V, MODULES, PROC_LIB}` and writes **`Modules/Gyn/database/data/gyn_schema.json`** (committed; pretty-printed; `"schema_version": "2026-10-20"`). The KB is never read at runtime. Transformations (implement exactly; unit-test the PHP side on the JSON):
   - **alias/skip map first (one rule):** the map below is applied to **every** reference — a field's own code, every local `{k}` and every `{mod:'m.k'}` — *before* any prefixing; the exporter's «fail on unresolved» check runs after it. Local + cross-module aliases: `complaint.list` / local `list` inside `complaint` → `complaint` (context list of complaint codes); `profile.marital` / `marital` → `marital_status` (core); `h_name` → `spouse_name`; `h_job` → `spouse_occupation`; `marriage_years` → `marriage_duration_years`; `job` → `occupation`; `referred_by` → `referred_from`; `profile.rh` → `patient.rh`; `profile.blood`, `rh`, `weight`, `height`, `bmi`, `smoking`, identity fields (`name, mrn, dob, age, phone, nid`) → **skip** (not seeded; a rule referencing a skipped field other than via the aliases above is an export error); exam `bp`, `pulse`, `temp` → skip; `alerts.*` → the `condition` rewrites below; `dx.list` → `diagnosis` (context list of encounter diagnosis codes — register it with the `values` hook if any rule needs it). Every whole module that decision 2 marks «not seeded» (`alerts`, `history`, `team`, `dx`, identity parts of `profile`) produces no fields.
   - **codes:** field `gyn_{module}_{k}`; repeat child `gyn_{module}_{rep}_{k}`; > 64 chars → first 57 chars + `_` + 6-char md5.
   - **types:** chips→`multi_select`; seg→`single_select` + `meta.render='segmented'`; sel→`single_select` + `meta.render='dropdown'`; num→`numeric`; text→`text`; area→`textarea`; bool→`boolean`; date→`date`; calc→`computed`; repeat→`repeat` (parent row, `meta.title`) with children carrying `repeat_group` = the parent code (≤32; hash-trim the same way); table→`table` (`meta.cols`); score→`score` (`meta.items`); popq→`popq`; figo→`figo`; media→ **not a field** (record `module.media=true`; the attachments card filters by `module_key`).
   - **meta:** `w`→`meta.w`, `u`→`unit`, `rep.title`→`meta.title`, calc args → `meta.ref`, module `color`→ module JSON only.
   - **options → codes:** every option string gets a slug code (lowercase ASCII, non-alnum→`_`, trimmed, ≤64, de-duplicated within its list with `_2`…). Option lists **identical by reference to a `V.<name>` array** → shared catalog `lookup_key = gyn.v.<name>` (snake-case the name, e.g. `gyn.v.tvs_ut`); other `chips`, and `sel` with > 7 options → `lookup_key = gyn.<module>.<k>` (≤48, hash-trim); `seg`, and `sel` with ≤ 7 options → per-question `history_answer_options` rows (value = code, labels = the English string). `bool` has no options. Labels: p3 option strings are English medical terms → `value_en` = the string, `value_ar` = the same string (owner decision G6: English medical with Arabic headings).
   - **labels:** `label_ar` = `f.l`; `label_en` = `f.en` ?? humanised `k`; section title → `section` (≤64). `sort_order` = running index ×10.
   - **show_if rewrite (hard rule — the PHP `ShowIfEvaluator` treats `{mod:…}` as "module open" and E1e's `ValidShowIf` requires `k` on value leaves):** `{k:'x', op}` → `{k:'gyn_<module>_x'}` (inside a repeat → the sibling child code, evaluated per row); `{mod:'m.k', op}` → `{k:'gyn_m_k', op}` — **except** core refs through this alias table: `complaint.list` → `{k:'complaint'}`, `alerts.dvt_pe eq Yes` → `{k:'condition', has:'dvt_pe'}`, `alerts.anticoag eq Yes` → `{k:'condition', has:'anticoagulant'}`, `alerts.thrombophilia` (any non-No) → `{any:[{k:'condition',has:'thrombophilia'},{k:'condition',has:'aps'}]}`, `profile.*` spouse/marital refs → their aliased codes; operand values → option **codes**; for boolean targets `eq:'Yes'`→`eq:true`, `eq:'No'`→`eq:false`, `{truthy}`→`{truthy:true}`; `{cat:'x'}` unchanged. A leaf with no operator → `{k, truthy:true}` (p4b returns `!!val`). Module-level rules get the same rewrite. The exporter **fails** on any ref it cannot resolve and prints it.
   - **computed:** `calc:'x'` → `computed_key` = `gyn_x` for GYN calcs (`gyn_gpal`, `gyn_utvol`, `gyn_afc`, `gyn_count`, `gyn_mirror`, `gyn_rmi`, `gyn_rotterdam`, `gyn_rasrm`, `gyn_bmi_profile` …, ≤32), with `count:myoma` → `gyn_count` + `meta.ref`=repeat group code, `from:a.b` → `gyn_mirror` + `meta.ref`=`gyn_a_b` (or `patient.rh` for `profile.rh`), `popq:pre` → `gyn_popq` + `meta.ref`; core `age`/`bmi` → existing keys. `udsReport`/`muiNote` → not computed: `meta.hint` / summary type (G6).
   - **modules JSON:** `{key, group, title_ar, title_en, color, priv, always, scope (patient|encounter|record), show_if (rewritten), feature (from group: fert→fertility, ob→obstetrics, uro→urogyn, sex→sexual, op→operations; module onc→oncology; core/gyn/fu→null), media, sections:[{title, codes:[…]}]}`; plus `groups`, `cats` (group → [{code, en}]), `lookups` (lookup_key → [{code, en}]), `proc_lib` (for G4).
5. **Categories** = 4 encounter-scope `multi_select` definitions `gyn_cat_gyn`, `gyn_cat_ob`, `gyn_cat_uro`, `gyn_cat_sex` (lookup_keys `gyn.category.gyn|ob|uro|sex`, `meta.category_group`, `gyn_cat_sex` `is_private=true`), `fill_role=doctor`, required for sign only when the gyn_cats card is in the layout (soft warning, not 422). The `fert` tab of the prototype picks from the `gyn` list (`infertility`, `tubal`…). `ShowIfContext.categories` = union of these answers for the encounter ∪ the open episode's `categories` (C1) — registered by the Gyn provider via `ShowIfContextBuilder::extend('categories', …)`.
6. **Features `gyn.feature.{fertility, obstetrics, urogyn, sexual, oncology, operations}`** live in the S1 store `clinic.visit_layouts['gyn_ob'].features` (default all true) — **no separate setting definitions**. BE reads them through `Modules/Gyn/app/Support/GynFeatures::for(companyId)`; a feature off hides its group's categories (lookup rows filtered), modules (registry), cards (FE `feature`), summary types, and makes the BE refuse writes to that group's fields (422 `gyn_feature_off`). Category `infertility` belongs to fertility; other gyn-group categories never hide with fertility (deviation from the prototype's `GYN_FERT_CATS`, which also hid pcos/endometriosis — clinically wrong; report it).
7. **`obstetric_history_rows`** columns are the p3 `obhx.preg` repeat fields as typed columns (year, ga_weeks, outcome code, mode_detail, cs_indication, loss_mx, birth_weight_g, sex, neonatal, alive, pph, pph_cause/pph_tx/pph_products json arrays of codes, sepsis, sepsis_source json, sepsis_timing_days, sepsis_organism, sepsis_mx json, sepsis_outcome, transfusion, other_complications json, place, note, sort_order). JSON arrays of codes are allowed (they are not CSV). G/P/A/L is computed from rows (port `CALC.gpal` exactly; the «ongoing pregnancy» term is added by G5 from `pregnancies`).
8. **Values are codes everywhere** (D2 decision 2). Summaries resolve codes → `value_en`.

## Shared-file rules
- Orchestrator-owned — REPORT, don't edit: `Modules/Clinic/routes/api.php`, `RolePermissionSeeder.php`, `ClinicSettingDefinitionSeeder.php`, the installer seeder list. Allowed one-liner: `modules_statuses.json` (decision 1).
- Hot files you own during G1 (serial; nobody else edits them meanwhile), minimal additive edits only: `Modules/Clinic/app/Services/HistoryService.php` (repeat-aware computed map + feature-off refusal hook, G1a §8), `Modules/Clinic/app/Support/ShowIfEvaluator.php` (ONLY if a test vector proves a semantic gap after the rewrite — report it instead if possible), `Modules/Clinic/app/Services/ClinicProfilePackService.php` (one guarded call, G1a §7), `Modules/Clinic/app/Models/ClinicComplaint.php` (+`code`), `Modules/Clinic/app/Support/ComputedRegistry.php` (static `extend()` only).
- FE: S1 files may be edited only to register GYN cards + the `gyn_ob` default layout + the `VisitClinicalType` value (G1b §1); i18n targeted keys under `GYN.*` only, **no `I18N_VERSION` bump**.
- Pick-don't-type: every multi-option list renders as selected chips + «＋ اختيار» picker (prototype rule «opts > 6 → picker»), never long open chip lists; segmented only for ≤ 7.

---

## G1a — BE (migrations `2026_11_03_192000`–`192099`, in `Modules/Gyn/database/migrations` unless stated)

### Do
1. **Scaffold `Modules/Gyn`** (module.json alias `gyn`, composer.json, `GynServiceProvider` + `RouteServiceProvider` mirroring Dental, `routes/api.php` with prefix/middleware exactly like Dental's, `lang/{ar,en}/gyn.php`, `tests/Feature`). The provider's `boot()` registers: `GynModuleRegistry` as the `ModuleCatalog` binding for `gyn_ob`; `ClinicalTypeRegistry::register('gyn_ob', …)` (summary types + their module lists from the prototype `GYN_SUM`, follow-up rules unchanged from C1); `ShowIfContextBuilder::extend('categories', …)` and `extend('values', …)` (adds `complaint` = codes of the encounter's linked `clinic_complaints`, `patient.rh` = '+'|'-' from `blood_group`); `PatientHeaderExtensions::register('gyn_ob', …)` (§5); `SummaryTemplateRegistry` templates (§9); `ComputedRegistry` keys (§8) — HistoryService `new`s its helpers (it does `new ShowIfEvaluator`), so add a static `ComputedRegistry::extend(string $key, callable $fn)` (static array merged into `$handlers` in the constructor; additive edit to that Clinic file, which you own during G1) and call it from the Gyn provider.
2. **Export** (decision 4): write `tools/export-gyn-schema.mjs`, run it (`node Modules/Gyn/database/data/tools/export-gyn-schema.mjs`), commit `gyn_schema.json`. Print and report the counts (modules, fields per module, lookup keys, option rows, show_if rules rewritten, unresolved = must be 0).
3. **Migration `192000_create_obstetric_history_rows_table`** (decision 7; + company_id, patient_id, created_by/updated_by, timestamps, softDeletes, idx (company_id, patient_id)). Model `ObstetricHistoryRow` (BaseModel, casts, `LogsActivity`).
4. **Migration `Modules/Clinic/database/migrations/2026_11_03_192001_add_code_to_clinic_complaints.php`**: `clinic_complaints.code` string(64) nullable + idx (company_id, code).
5. **`GynFieldPackSeeder`** (`Modules/Gyn/database/seeders/`): `run(int $companyId, array $moduleKeys)` reads the JSON; for each field: `HistoryQuestion::firstOrNew(company_id, code)`; structural columns always (input_type, scope, category (`profile` for patient-scope profile fields, `history` for other patient-scope, `exam` for exam/imaging module, `custom` otherwise), clinical_type `gyn_ob` (except aliased core codes, which keep their own type), module_key, repeat_group, lookup_key, show_if, computed_key/is_computed, unit, `meta`, is_private (module `priv`), fill_role, in_intake, carry_forward/stale_after_days); presentation + `is_active=true`/`is_required=false` on create only. Options: `history_answer_options` upsert by (question, value); lookups: `clinical_lookups` upsert by (company, lookup_key, code), `specialty='gyn_ob'`, `display_order` from list order, never deactivating a row the clinic added. GYN complaint list → also `clinic_complaints` (name_ar = name_en = English, `code`) + `lookup_key='complaint'` rows with the same codes. GYN dx list → `lookup_key='diagnosis'` rows (specialty gyn_ob). `V.medHx/surgHx/famHx` → `condition`/`procedure` lookups (specialty gyn_ob, skip codes D2a already seeded). Category lists → `gyn.category.*`. Idempotent: a second run changes nothing (assert in tests).
6. **`meta` column** already exists (D2a `170007`, with fillable/cast/resource) — verify it; do not add it again. If it is missing, STOP and report.
7. **Install triggers**: (a) data migration `192010_install_gyn_pack.php` — for every company with a department `clinical_type='gyn_ob'`, run the seeder for the G1 module set (`profile` subset, `menstrual`, `complaint`, `exam`, `imaging`, categories, vocab) — non-fatal, logged, skipped in unit tests like the permission bridge; (b) artisan command `gyn:install-pack {--company=} {--modules=}`; (c) in `ClinicProfilePackService::apply()` after the `obgy` pack applies: `if (class_exists(GynPackInstaller::class)) app(GynPackInstaller::class)->install($companyId);` (guarded one-liner).
8. **Engine extensions** (in `Modules/Gyn/app/Support/GynComputed.php`, registered in §1): `gyn_gpal` (from `obstetric_history_rows`, port `CALC.gpal` incl. term/preterm split, E/CS counts, «alive» rule), `gyn_utvol` (L×W×AP×0.523, rounded), `gyn_afc` (R+L), `gyn_count` (rows in `meta.ref` repeat group), `gyn_mirror` (value of `meta.ref` in the same encounter → patient scope fallback; `patient.rh` special), `age`/`bmi` reuse core. Handlers receive the code→value map; write a `GynModuleValues::local(string $moduleKey, array $codeValues): array` helper (strips `gyn_<module>_`, groups repeat children into `[repeat_k => [ [k=>v], … ]]`) so ported formulas read like p4a. **HistoryService** computed-on-save (E1e) must pass repeat rows: extend its code→value map so a repeat child code maps to the list of row values keyed by `repeat_index` (additive; keep scalar behaviour for non-repeat codes) and pass `meta` (for `meta.ref`) — read E1e's private compute method first. **Computed results may be text with a tone** (p4a returns strings/objects: `gpal` «G3 P2 A1 L2», `rotterdam` «2/3 criteria — …» warn, `rmi` «312 (≥200 — oncology referral)» bad): handlers return a scalar OR `['value' => int|float|null, 'text' => string, 'tone' => ''|'warn'|'bad']`; the derived write stores `answer_value` = text ?? (string) value, `value_num` = numeric value or null, `value_json` = the array, `source='derived'`, and never runs `validateNumeric` on a derived value (E1e validates computed answers as numeric when a *client* sends one — keep that for client input, which should be rejected/ignored for computed questions anyway). Also in HistoryService: before saving an answer whose question has `clinical_type='gyn_ob'`, call an optional hook `FieldWriteGuard` (Clinic contract, resolved from the container if bound) — Gyn binds it to refuse fields of a feature that is off (decision 6) and private (sex) fields without `gyn.sexual.edit`.
9. **Summaries** (C1b registry): PHP templates for module keys `profile` (gyn part: marriage + spouse), `obhx`, `menstrual`, `complaint`, `exam`, `imaging` — port the p3a `sum` functions **line by line** into `Modules/Gyn/app/Summary/*Template.php` (English output identical to the JS for the same inputs; use the p4c scenario values — `p4c-scenarios.js` — as test fixtures). The `clinic` summary for gyn_ob = core module list (`profile, alerts, history, obhx, menstrual, complaint, exam, imaging, team, dx`) + every visible non-core module (G2 adds their templates; the generic template covers them until then). Alerts/history/team/dx lines come from the D2/C1 shared data (alerts: format like `alertsOf()`).
10. **G/P/A/L in the header**: `PatientHeaderExtensions::register('gyn_ob', fn → ['gpal' => 'G3 P2 A1 L2 · 1 CS', 'obhx_rows' => n])`; also fill the legacy `pregnancy_gpa` key the FE model already has. Snapshot: register the same value into the sign snapshot `facts.gpal` if D2b exposed a hook; else report.
11. **API (`Modules/Gyn/routes/api.php`, you own it)** — prefix `api/gyn` (mirror Dental's prefixing): `GET schema?modules=` (module registry filtered by features + permissions: priv modules only with `gyn.sexual.view`; includes per-field definitions for the company: id, code, labels, input_type, meta, options/lookup_key, show_if, is_required, is_active), `GET patients/{patient}/obstetric-history`, `POST …`, `PUT obstetric-history/{row}`, `DELETE obstetric-history/{row}` (soft), `PUT obstetric-history/reorder`, `GET features` (resolved `gyn.feature.*`). Field values themselves use the existing Clinic endpoints (encounter/patient history upsert, record fields) — no GYN value endpoint.
12. **Permissions + roles** (`GrantGynPermissionsSeeder` + bridge migration `192020_gyn_permission_bridge`, pattern of `GrantClinicDevicePermissionsSeeder`): `gyn.file.view`, `gyn.file.edit`, `gyn.obhx.manage`, `gyn.sexual.view`, `gyn.sexual.edit` (G3–G7 add their own). Seed roles (firstOrCreate, never strip existing grants): `gyn_consultant`, `gyn_doctor` (all gyn.* + the clinic doctor set: copy the permissions of the existing clinic doctor role — find it via `ClinicRoleAssignment`/`ClinicRoleController`), `gyn_team_member` (file.view/edit, no sexual), `gyn_nurse` (file.view + vitals/history record, no sexual), `gyn_sonographer` (file.view + edit of module `imaging` only — enforce via the FieldWriteGuard: sonographer role may write only `module_key=imaging`), reception = existing reception role (no gyn.*). Defaults: `gyn.sexual.*` only to consultant/doctor (design: «الافتراضي الطبيبة المعالجة بس»). **Two gates on private GYN fields, granted together:** E1e refuses `is_private` saves/reads without `clinic.fields.private`, and the `FieldWriteGuard` refuses sex-group fields without `gyn.sexual.edit` — so every role granted `gyn.sexual.view|edit` is also granted `clinic.fields.private` by this seeder (and no role gets `gyn.sexual.*` without it). State this in the report as a design note. REPORT the lines for `RolePermissionSeeder` + installer list.
13. **First-visit step (no wizard yet — D3 builds it):** set `in_intake=true` + `fill_role` on the profile/spouse (reception) and menstrual (nurse) fields; `PatientProfileFieldController?clinical_type=gyn_ob` already returns the gyn L2 section (verify; if it filters by category=profile only, menstrual must be category `history` and surfaced through `GET gyn/patients/{p}/intake-status` = `{missing: [codes], obhx_rows: n, complete: bool}` from required+in_intake gyn fields — add that endpoint).

### Tests — `Modules/Gyn/tests/Feature/G1aGynFoundationTest.php` (prefix `g1a_`) + `Modules/Gyn/tests/Unit/G1aSchemaExportTest.php`
JSON: 51 modules, 0 unresolved refs, every field code ≤64 and unique, every lookup_key ≤48, every rewritten show_if passes `ValidShowIf`; seeder idempotent (second run: 0 inserts/updates) and keeps a clinic-hidden field hidden; complaint conditional field `gyn_complaint_hmb_pads` hidden unless the encounter has the `heavy_menstrual_bleeding` complaint (via `ShowIfContextBuilder` + evaluator); boolean rewrite works (`gyn_profile_prev_marriage_years` shown only when `gyn_profile_prev_marriage=true`); category answer `aub` in `gyn_cat_gyn` → `ShowIfContext.categories` contains `aub`, and an open episode's categories are unioned; G/P/A/L of the p4c scenario rows equals the JS output; utvol/afc/count/mirror computed on save (source derived); urogyn feature off → uro categories absent from the lookup list and a write to a uro field 422; sexual field write without `gyn.sexual.edit` 403/422; sonographer can write `imaging` fields only; obstetric-history CRUD + reorder; clinic summary for a scenario encounter contains the ported menstrual/complaint/exam/imaging lines; header `specialty.gpal` present for a gyn_ob call and absent for general. Run also `C1aEntitiesTest`, `C1bSummaryRecordTest`, `D2aFactsTest`, `E1eAnswersTest`, `HistoryEngineTest`, `PlatformE1HistoryEngineTest`, the existing obgy/profile-pack tests (grep `ProfilePack`) — keep green.

### Out of scope (G1a)
G2 modules' seeding + templates + their computed (rotterdam/rmi/rasrm/bmi_profile), cycles/pregnancies/procedure templates (G3–G5), sexual-health content (G7), intake wizard (D3), FE.

---

## G1b — FE (`src/app/features/gyn/` new + registered shell cards; no migrations)

**Fable design consult is MANDATORY** (the GYN client's daily screen): before building, send Fable (`model: "fable"`) screenshots of the prototype GYN specialty (`../visit-shell-prototype.html` → choose «نسا وتوليد»; helper in `S1-visit-shell-fe.md` §Spec 4) for the categories card, module host (collapsed modules with summary line, first open, «اتفتح من الاختيار» tag, private dark card), obstetric-history card, imaging station, and the GYN summary card; ask for the production spec (density, chip vs segmented thresholds, repeat-row anatomy, FIGO/mobile behaviour, how the header shows G/P/A/L next to alerts). 2 rounds max; record decisions in the report. Ask again only for a state the prototype lacks.

### Do
1. **Shell registration (only S1 edits allowed):** `visit-layout.model.ts` — `VisitClinicalType` gets `'gyn_ob'` (BE value; the prototype/S1 used `'gyn'` — replace it, grep usages); `features/clinic/clinical-types/clinical-types.ts` has no `gyn_ob` entry (only general/nutrition/dental) — add one (`key:'gyn_ob'`, tabs: consultation) so `clinicalTypeOf('gyn_ob')` stops falling back; `card-catalog.ts` — cards `gyn_cats` (required, S/M), `gyn_modules` (required, M/F), `gyn_imaging` (station, M/F), `gyn_obhx` (L1, S/M/F), `gyn_summary` (M/F), `gyn_first_visit` (S/M), plus one addable card per non-core module from the schema endpoint (`gyn_<key>`, `feature` from the module, `priv` flag) — generated at runtime from `GET api/gyn/schema` and merged into the catalog by a small `registerSpecCards()` function (add it to `card-catalog.ts`); `DEFAULT_LAYOUTS.gyn_ob` = the prototype `gyn` layout with D2/C1 shared cards: vitals → complaint → history → gyn_cats → gyn_modules → gyn_imaging → exam → diagnosis → lab → rad → rx (props `{subjectEnabled:true}`) → documents → team → followup, and `timeline` + `gyn_summary` + `gyn_obhx` (check with Fable whether obhx sits in the start rail or as a station). Wire the card bodies in the shell `@switch`. `consultation.component.ts` redirect: also redirect `gyn_ob` departments when `visit_shell_enabled` (one condition).
2. **Engine (TS mirrors, `src/app/features/gyn/engine/`, reusable by Clinic later):** `show-if.ts` — exact mirror of PHP `ShowIfEvaluator` (same fail-open choices, same pinned edge decisions in its docblock) + per-row evaluation for repeat children; `computed.ts` — `bmi, age, utvol, afc, count, mirror, gpal` (same rounding as PHP); a shared vector file `Modules/Clinic/tests/Fixtures/show_if_cases.json` (create it in G1b if G1a didn't; `{rule, values, categories, expected}` ≥ 30 cases incl. the rewritten GYN shapes) — the TS mirror must pass it: write `engine/show-if.cases.check.ts` and run it once with `npx tsx` (if unavailable, transpile with `npx tsc --outDir` to the scratchpad and run with node); report the result. There is no FE test runner (`skipTests`), so do not add spec files to the build.
3. **Field form** `gyn-module-form.component.ts` (`[definitions] [values] [locked] [context]` → `valueChange`): renders a module's sections with the engine-v2 types (single_select segmented/dropdown, multi_select via the S1 `clinical-picker-box` (lookup) — **never a long open chip list**, numeric with unit, text, textarea, date, boolean yes/no, computed read-only with tone (warn/bad), repeat rows add/remove/reorder with per-row show_if, table grid). Hidden fields (show_if false, `is_active=false`, client `fields` rule visible=false) are not rendered; required ones show `*`. Autosave (debounced) through the existing encounter/patient history upsert services (`repeat_index`, `value_json`, codes).
4. **Cards:** `gyn-category-picker.component.ts` (4 group lines → picker per group on `gyn.category.<group>`; locked groups struck through with the feature name, per the prototype; sex group 🔒 only with `gyn.sexual.view`); `gyn-module-host.component.ts` (lists visible modules = registry show_if over the context built from current answers + categories + complaint codes; collapsed with a summary line, first open, «اتفتح من الاختيار» tag; core modules NOT rendered here); `gyn-imaging-card` (module `imaging` form); `gyn-obhx-card` (rows table + add/edit dialog with the p3 fields, G/P/A/L live from `computed.ts`, saves to the Gyn API); `gyn-first-visit-card` (intake-status: missing items with jump links; opens the spouse/menstrual forms inline); exam card slot: the shared exam card gets `gyn-module-form` for `module_key=exam` below its picker **through a specialty-slot input** on `visit-exam-card` (add one optional input `specExtension` — the only edit to that S1 card); complaint card same (`module_key=complaint` conditional questions under the picker).
5. **GYN summary card** `gyn-summary-card.component.ts`: extends C1c's summary card with the GYN types (clinic / fertility / operative / antenatal / delivery / discharge / follow-up / UDS / urogyn / couple — only those whose feature is on), preview from the BE, private-exclusion note (prototype text), «حفظ وطباعة». Live preview while typing is NOT required (BE preview button).
6. **Header slot:** pass `specialty.gpal` to D2c's `patient-alert-strip` specialty slot for gyn_ob encounters (styled per Fable).
7. **Max lab grid (shared, not GYN code):** in `features/clinic/encounter/orders/lab-orders.component.*` add a «شبكة» view toggle that renders the full LIS catalog through `shared/catalog-grid-picker` grouped by category (build the categories from the existing `lab-catalog.util.ts`), favourites lane first, «حدد الفئة» per lane — mirror how `rad-orders` uses the grid; keep the current search view as the default and remember the doctor's choice in `localStorage` (try/catch). Order ≠ execution: the grid only adds order lines exactly like the search path does. P3 owns `rad-orders` — do not touch it.
8. i18n targeted keys under `GYN.*` (+ `LAB_ORDERS.GRID_*` for item 7), ar + en, no `I18N_VERSION` bump. `npx ng build --base-href /app/` green. No deploy.

### Out of scope (G1b)
G2 module-specific widgets (FIGO picker, RMI suggestion), cycles/pregnancy/POP-Q widgets, the intake wizard, nutrition/dental layouts, BE.

---

## Report back (each WP)
Files created/edited; export counts (G1a); test output with pass counts; TS show_if vector result (G1b); route lines (Clinic — if any), permission names, role seeds, installer-list line, the `modules_statuses.json` one-liner; CHANGELOG EN/AR; files needed but not touched; Fable decisions (G1b); any p3 field you could not place by the table above.
