# D2 — structured patient facts (alerts · conditions · home meds · shared pregnancy/smoking/marital · clinical notes), audit, stamp-at-sign, the ONE patient header

Read `_COMMON.md` first. D2 is **three serial WPs** — D2a (BE schema + write services) → D2b (BE readers + sign stamping + header API) → D2c (FE header + cards). Each section below is a complete brief for its WP; read the shared parts (Spec, Decisions, Shared-file rules) plus your own section.

**Depends on:** D1 ✅ (observations), P1 ✅ (Rx v2: `prescriptions.kind/subject/dispense_status`, `prescription_items.drug_name/sentence/item_status/clinic_drug_id`), E1e ✅ (answers `repeat_index/value_json/source`, `LogsActivity` on `PatientHistoryAnswer`, private-field rules, `ValidShowIf`). D2b needs D2a ✅; D2c needs D2b ✅ and S1 ✅.

## Spec (read before coding)
- `../design-data-layers.md` §2 (L1 row «المحتوى/تعديل-Audit/فين بيظهر»), §4 field-placement table rows **الحساسية، الأمراض المزمنة، العمليات، العائلي، الأدوية، التدخين، الحالة الاجتماعية، الحمل/الرضاعة، الدورة الشهرية، ملاحظات المريض، medical_history json**, §6 rule (3) «حقيقة بتهم كل العيادات ممنوع تتعرّف في باقة», §7 steps 4, 5, 6, 7, 9, 11, and the D2 row of §8 (acceptance).
- `../design.md` §4.5 (patient_alerts, `clinic.alerts.manage`, header in every screen incl. inpatient file) and §4.7 row «ملاحظات/addenda».
- `../design-gyn-integration.md` §2 rows `patient_alerts` (GYN codes) and `clinical_notes` (+`is_private`/`module_key`/record morph).
- `../design-visit-shell.md` §4 (sticky header always shows alerts), §5, §2.6 (pick-don't-type pickers).
- `../master-plan.md` D2 row (acceptance: «بياخد سيولة» in dental shows red in the GYN header · an outside drug shows in «الأدوية الحالية» · an old signed visit shows what it saw · a private note is invisible to anyone but its author).
- GYN source for alert semantics: `/home/moonui3/public_html/knowledge-base/plans/gyn-obstetrics-module-src/p3a-schema-core.js` module `alerts` (lines ~114–124) and `p4a-engine-calc.js` `alertsOf()` (line 8: tone per alert).

## Decisions already made (do not re-litigate)
1. **One catalog = `clinical_lookups`** (E1c), not `clinic_history_options`. data-layers §4 put `is_alert/alert_severity/alert_types` on `clinic_history_options`; the owner directive «one lookup_key catalog» overrides it. D2a adds the alert columns to `clinical_lookups`, copies the `clinic_history_options` rows into it (`lookup_key` = `condition` | `relation` | `procedure`), and leaves `clinic_history_options` + its CRUD untouched as a one-release read fallback.
2. **`clinical_lookups.code`** (new, string(64), nullable, unique per (company_id, lookup_key, code) when not null) is the **stable value stored in answers / structured rows** for every lookup-backed field. Seeded rows get a slug code; an inline-added row gets `code = 'c'.$id` (set right after insert). Labels are resolved from the catalog at read time (summary, header, print). Never store a CSV; multi-values are JSON arrays of codes (`value_json`) or one row per value.
3. **`patient_alerts` columns = the union of design.md §4.5 and data-layers §4** (they diverge): `kind` (allergy | condition | drug | note | nka | custom), `code`, `label`, `severity` (info | warn | danger), `reaction`, `source` (manual | history_answer | condition | drug_alert | import | derived), `ref_type`/`ref_id`, `active`, `set_by`. «لا يوجد حساسية» is an explicit `kind=nka` row (intake minimum, data-layers §5.1).
4. **Conditions live in `past_medical_history`** (+ `status`, `is_chronic`, `severity`, `lookup_id`, `source`) — no new table. A condition whose catalog row `is_alert=true` also surfaces in the header as an alert **at read time** (D2b) — do not write a duplicate `patient_alerts` row for it.
5. **Home meds = `prescriptions.kind='home'`** (no `patient_medications` table). Home rows have no visit: `prescriptions.encounter_id` becomes nullable; `dispense_status='not_applicable'`; written by a dedicated service, never through `IssuePrescription` (which requires an editable encounter).
6. **Pregnancy is a visit fact**, core definition `pregnant` / `breastfeeding` (`scope=encounter`, `clinical_type=NULL`, `show_if {"k":"patient.gender","eq":"female"}`, `carry_forward=prefill`, `stale_after_days=30`). `smoking` stays the core answer. `marital_status` becomes core (`clinical_type=NULL`, `is_active=false` by default for companies that do not have it yet). The nut_/dent_ duplicates are **migrated then deactivated** (`is_active=false`), not deleted, this release.
7. **`clinical_notes`** is immutable after create: edits are new rows with `kind=addendum`, `parent_id` → original. `is_private=true` ⇒ visible to the author only (never to admins, never in summary/export/print). `module_key` + `record_type/record_id` are present from day one (G-phases attach notes to a module/procedure/cycle).
8. **Audit:** Spatie `LogsActivity` on every new/extended structured model (mirror exactly how E1e wired `PatientHistoryAnswer`): `PatientAlert`, `PastMedicalHistory`, `PastSurgicalHistory`, `FamilyHistory`, `ClinicalNote` (logOnly the clinical columns, logOnlyDirty, dontSubmitEmptyLogs).
9. **Stamp at sign** = extend the existing `Listeners/SnapshotEncounterHistory` payload (same immutability guard). A signed visit renders from its snapshot, never from live tables.
10. **The header is ONE data contract** (`GET /clinic/patients/{p}/header`, the existing `PatientReadService::summaryFor`) consumed by `app-clinic-patient-header` (all screens) and `app-visit-header` (shell). Specialty additions (G/P/A/L, EDD/GA) plug in through a registry slot, so G1/G5 never edit `PatientReadService` again.
11. **`show_if` context refs `patient.gender`, `patient.age`, `encounter.kind`, `encounter.clinical_type`** are produced by ONE builder (`Support/ShowIfContextBuilder`, D2a). HistoryService required-enforcement and (later) the summary/G-modules use it; nobody builds a context by hand again.

## Shared-file rules (all three WPs)
- `Modules/Clinic/routes/api.php` and `Modules/Core/database/seeders/RolePermissionSeeder.php` are **orchestrator-owned**: do NOT edit. Put your route lines and permission names in the report, verbatim, grouped as they should be pasted (route lines next to the existing `patients/{patient}/past-medical-history` block). Write the HTTP tests against the route names you report; until the orchestrator pastes the lines, run your service-level tests and list which HTTP tests are pending the paste (the orchestrator runs them after pasting). Never register routes from a test file or a service provider as a workaround.
- `Modules/Clinic/database/seeders/ClinicSettingDefinitionSeeder.php` is orchestrator-owned: read settings with a code default; report the definition rows you need.
- `HistoryService.php` is a hot file: only D2a edits it during D2 (see D2a §6). `Encounter.php`, `consultation.component.*` are not D2 files.
- FE i18n: targeted key additions to `src/assets/i18n/ar.json` and `en.json` under `PATIENT_HEADER.*` / `PATIENT_FACTS.*` only; **do NOT bump `I18N_VERSION`** (the orchestrator bumps at deploy). This supersedes the bump instruction in older briefs.
- Never write CSV into one column; every pick is a catalog code.

---

## D2a — BE schema + write services (migrations `2026_11_03_170000`–`170099`)

### Do
1. **Migration `170000_create_patient_alerts_table`**: `patient_alerts` (id, company_id, patient_id, kind string(12), code string(64) nullable, label string(191), severity string(8) default 'warn', reaction string(191) nullable, source string(16) default 'manual', ref_type string(32) nullable, ref_id unsignedBigInteger nullable, active bool default true, set_by nullable, set_at timestamp nullable, resolved_at/resolved_by nullable, note text nullable, timestamps, softDeletes; idx (company_id, patient_id, active), (ref_type, ref_id)). Guarded `hasTable`.
2. **Migration `170001_extend_structured_history_tables`**: `past_medical_history` + `lookup_id` nullable, `status` string(12) default 'active' (active | resolved | remission), `is_chronic` bool default true, `severity` string(8) nullable, `source` string(16) default 'manual', `recorded_by` nullable; `past_surgical_history` + `lookup_id`, `source`, `recorded_by`, `performed_on` date nullable, `place` string(191) nullable; `family_history` + `lookup_id`, `source`, `recorded_by`. All `hasColumn`-guarded. (`since_when` already exists on PMH/PSH — keep.)
3. **Migration `170002_add_alert_columns_to_clinical_lookups`**: `clinical_lookups` + `code` string(64) nullable, `is_alert` bool default false, `alert_severity` string(8) nullable, `alert_kinds` json nullable (which alert kinds it raises; null = condition alert everywhere), `meta` json nullable; unique (company_id, lookup_key, code) — MySQL treats NULLs as distinct, so inline rows without code are fine. Then the **data step in the same file** (idempotent, per company): copy every `clinic_history_options` row → `clinical_lookups` (`lookup_key` = its `type`, `value_ar` = `value`, `value_en` = `value`, `code` = slug(value) de-duplicated with a numeric suffix, `display_order` = sort_order) unless a row with the same (company, lookup_key, value_ar) exists; and seed the **alert-condition codes** into `lookup_key='condition'` with `is_alert=true` + severity: `anticoagulant` (danger, «بياخد سيولة» / On anticoagulation), `bisphosphonate` (warn), `bleeding_disorder` (danger), `dvt_pe` (danger, DVT / PE سابق), `thrombophilia` (danger), `aps` (danger, Antiphospholipid syndrome), `transfusion_significant` (warn, نقل دم مهم سابقاً), `difficult_airway` (warn, Difficult airway / مضاعفات تخدير), `diabetes` (info), `hypertension` (info), `asthma` (info), `epilepsy` (warn), `cardiac` (warn). Labels ar/en from the GYN `alerts` module + the dental pack labels (`ClinicProfilePacks::dentalQuestions()` alert codes) — read both. Also seed allergen codes into `lookup_key='allergen'`: penicillin, cephalosporin, nsaids, sulfa, latex, iodine_contrast, local_anaesthetic, general_anaesthetic, food (from GYN `drug_allergy_list` + dental). Only for companies that have at least one `history_questions` row.
4. **Migration `170003_make_prescriptions_encounter_nullable`**: `prescriptions.encounter_id` → nullable (`->change()`, guarded; drop/re-add any FK the same way P1 handled `clinic_drug_alerts`). Read `2026_11_03_120000_add_rx_v2_columns_to_prescriptions.php` first.
5. **Migration `170004_create_clinical_notes_table`**: `clinical_notes` (id, company_id, patient_id, encounter_id nullable, kind string(12) (sticky | case | nursing | progress | addendum | note), parent_id nullable, body text, is_private bool default false, module_key string(32) nullable, record_type string(24) nullable, record_id nullable, clinical_type string(24) nullable, author_id, pinned bool default false, timestamps, softDeletes; idx (company_id, patient_id), (encounter_id), (record_type, record_id)).
5b. **Migration `170007_add_meta_to_history_questions`**: `history_questions.meta` json nullable (guarded) — design-gyn §4 promised it in E1 and it was never added; D2a needs it for `meta.condition_code` / `meta.alert_code` (§10c) and G1 for render/width/ref. Add `meta` to `HistoryQuestion` fillable + cast `'meta' => 'array'` and one line exposing it in `HistoryQuestionResource` (+ accept it as `nullable|array` in the Store/Update requests).
6. **Migration `170005_core_shared_fact_definitions`** (data, idempotent per company, only companies that already have core history questions): upsert core definitions — `pregnant` (boolean, scope encounter, category history, section women, clinical_type NULL, carry_forward prefill, stale_after_days 30, show_if `{"k":"patient.gender","eq":"female"}`, fill_role nurse), `breastfeeding` (same), `menstrual_pattern` (single_select, scope patient, show_if gender female, options regular|irregular|post_menopause|amenorrhea — check data-layers §4 row «الدورة الشهرية»), `marital_status` (if it exists for the company set `clinical_type=NULL`; if not, create it core with `is_active=false`). **Before creating, check for an existing row with the same code** (the nutrition/dental packs may already own `pregnant` / `marital_status` under the bare code — read `ClinicProfilePacks` helpers `$alert/$history/$profile` to see whether they prefix codes); upgrade in place, never duplicate. Update `ClinicProfilePacks::GYN_OB_CODES` nothing — marital_status is already treated as core there; but make `ClinicProfilePacks::nutritionQuestions()` / `dentalQuestions()` stop seeding their own pregnant/smoker/marital duplicates (they must reference the core codes). Then **backfill** (same file, idempotent): `nut_smoker=true` → core `smoking='current'` (only when the patient has no smoking answer); `nut_pregnant`/`dent_pregnant` true → no backfill (it is a visit fact now; just deactivate the old questions); `nut_marital_status` → `marital_status` (when empty). Deactivate (`is_active=false`) `nut_smoker`, `nut_pregnant`, `nut_breastfeeding`, `nut_marital_status`, `dent_pregnant`. Use the real codes you find.
7. **Migration `170006_backfill_patient_alerts_and_conditions`** (idempotent, guarded, per company; skip a row if an identical (patient, kind, code|label, source='import') exists):
   - allergies: `lab_patients.medical_history.allergies|allergy` (json list/string) + the core `allergies` text answer (split on `,`/`،`/newline, trim) → `kind=allergy`, `source=import`, code = allergen code when the text matches one (case-insensitive ar/en), else label only; dental allergy flags answered `true` (section `allergies`, boolean) → allergy with the matching allergen code.
   - `lab_patients.important_note` non-empty → `kind=note`, severity warn, `source=import`.
   - booleans `diabetes`, `hypertension`, and every `dent_*` chronic flag answered `true` (section `chronic`) → `past_medical_history` row with `lookup_id`/`condition_code` = the catalog code (`dent_anticoagulant` → `anticoagulant`, map the rest by reading the dental pack), `source=import`.
   - `medical_history.chronic|conditions` json → PMH rows (label-matched to catalog, else `condition_code`=slug).
   - `medical_history.medications|meds` + `nut_supplements` + `nut_external_medications` text → ONE `prescriptions` row `kind=home` per patient with one item per line (`drug_name`=text, `sentence`=text, `item_status=active`), only if the patient has no home prescription yet.
   - Nutrition food allergies: if table `nutrition_patient_food_prefs` exists, rows with kind/allergen → `kind=allergy`, `ref_type='nutrition_food'`, `ref_id`=row id. (moonui is paused; moonui3 owns this edit — read the table first.)
   - Do NOT delete or deactivate the source questions in this migration except `allergies` (core text) — set it `storage='alert'` and keep it active one release (D2b stops reading it).
8. **Models** (`Modules/Clinic/app/Models`, BaseModel/TenantAware like the neighbours): `PatientAlert` (casts, scopes `active()`, `forPatient()`), `ClinicalNote` (scopes `visibleTo(User)` = not private OR author_id = user), extend `PastMedicalHistory`/`PastSurgicalHistory`/`FamilyHistory` fillable/casts + `lookup()` belongsTo `ClinicalLookup`; `ClinicalLookup` fillable/casts for the new columns; all five get `LogsActivity` (decision 8). `Prescription` model: nothing beyond what P1 did — if `encounter()` is typed non-null somewhere, make it nullable-safe (read, report).
9. **Services** (`Modules/Clinic/app/Services/Facts/`, new folder):
   - `PatientAlertService`: `add(patientId, array data, userId)`, `resolve(alertId, userId)`, `setNoKnownAllergy(patientId, userId)` (creates `nka`, and deactivates nothing — but adding a real allergy auto-resolves an active `nka`), `activeFor(patientId)` (alerts ∪ derived condition-alerts from PMH rows whose lookup `is_alert` — returned with `source='condition'` and `ref_type='past_medical_history'`, sorted danger→warn→info).
   - `PatientConditionService`: `add(patientId, lookupId|code|label, status, sinceWhen, note, source, userId)` (writes PMH with both `lookup_id` and `condition_code`=code for back-compat), `update`, `resolve`, `list(patientId, includeResolved)`; chips toggle helper `syncChips(patientId, codes[], userId)` for `storage=condition` definitions.
   - `HomeMedicationService`: `add(patientId, items[{drug_name|clinic_drug_id|drug_id, sentence?, dose?, frequency_code?, started_on?}], userId)` → one `prescriptions` row kind=home (reuse the patient's open home prescription if one exists — append items), `stop(itemId, userId)` (`item_status=stopped`, `stopped_at/by`), `activeFor(patientId)` = active items across kinds `home`, `discharge`, and `outpatient` issued within `clinic.current_meds.window_days` (default 90; read the setting with default, report the definition) whose `item_status=active` and `ends_at` null or ≥ today.
   - `ClinicalNoteService`: `add(...)`, `addendum(noteId, body, userId)` (only author or `clinic.notes.manage`), `listFor(patientId|encounterId|record, User)` honouring `visibleTo`. Writing to a signed encounter → only `kind=addendum` allowed (call `Encounter::assertEditable()` for non-addendum kinds with an encounter_id).
   - `Support/ShowIfContextBuilder`: `forEncounter(Encounter, array extraValues = [])` and `forPatient(Patient, array extraValues = [])` → `ShowIfContext` with values: every patient-scope answer of the patient + (for an encounter) its encounter-scope answers, **keyed by question code and decoded by input_type** (boolean → real bool from 'true'/'false'; multi_select/repeat/table → array from `value_json` ?? json_decode(answer_value); numeric/computed → float; date → 'Y-m-d' string), plus `patient.gender` (lowercase enum value), `patient.age` (int), `encounter.kind`, `encounter.clinical_type` (the department's), and two fact lists every specialty may reference: `condition` (codes of the patient's active PMH rows) and `allergen` (codes of active allergy alerts) — G-module rules like «previous DVT» are rewritten to `{"k":"condition","has":"dvt_pe"}`. `categories` = [] and `modules` = [] for now, with two documented extension hooks: `protected function categories(...)` (G1 fills from category answers, C1 from open episodes) — implement as a small registry `ShowIfContextBuilder::extend(string $hook, callable $fn)` with hooks `values` (fn returns extra code => value pairs, merged) and `categories` (fn returns codes, unioned), each called with (`?Encounter`, `Patient`) inside try/catch, so C1/G1 register without editing the class body.
10. **HistoryService (you are the only D2 agent on it):** (a) `assertRequiredProfileFieldsPresent` uses `ShowIfContextBuilder` merged with the submitted values (submitted wins) instead of `showIfContextFromAnswers` — keep the old private method only if still used elsewhere; (b) **lookup-aware choice validation**: in `validateSingleSelect` / `validateMultiSelect`, when `$question->lookup_key` is set, validate each value against the active `clinical_lookups` codes of that key for the company (`code` or, for rows with null code, `'c'.id`) instead of `HistoryAnswerOption`; unknown value → the existing `history_value_option` 422; (c) **storage routing**: when a saved question has `storage='condition'` and input boolean/multi_select, ALSO call `PatientConditionService::syncChips` (boolean true → add the condition whose code is `meta.condition_code` ?? the question's `code`; false → resolve it; multi_select → the selected codes); `storage='alert'` similarly via `PatientAlertService` (code = `meta.alert_code` ?? the question's `code`); `storage='home_rx'` → no-op + 422 `history_use_home_meds` (the FE must use the meds card). Keep the answer row as well (back-compat, one release). Read the whole file first; E1e changed the save loop.
11. **Controllers/Requests/Resources** (new files): `PatientAlertController` (index active+resolved?flag, store, resolve, nka), `PatientConditionController` (index, store, update, resolve) — PMH CRUD in `PatientHistoryController` stays as is; `HomeMedicationController` (index active, store, stop item); `ClinicalNoteController` (index by patient / encounter / record, store, addendum). FormRequests validate: kind/severity/status enums (create `Enums/AlertKind`, `AlertSeverity`, `ConditionStatus`, `ClinicalNoteKind`), lookup ids belong to company + right lookup_key, label ≤191, body ≤ 20000. Resources expose resolved labels (lookup value_ar/en) + `can_edit`. Every write: tenant check `assertSameCompany` pattern used by `PatientHistoryController` (read it and mirror).
12. **Permissions** (REPORT, don't edit the seeder): `clinic.alerts.view`, `clinic.alerts.manage`, `clinic.conditions.manage` (fallback: reuse `clinic.history.record` — say which you used in routes), `clinic.home_meds.manage`, `clinic.notes.view`, `clinic.notes.create`, `clinic.notes.manage`. Also ship `Modules/Clinic/database/seeders/GrantClinicFactsPermissionsSeeder.php` + bridge migration `170010_clinic_facts_permission_bridge.php` mirroring `2026_10_09_130003_clinic_device_permission_bridge.php` + `GrantClinicDevicePermissionsSeeder` (grant to owner/admin + roles that hold `clinic.history.record`; nurses holding `clinic.vitals.record` get alerts.view/manage + home_meds.manage + notes.view/create). Report the installer-list line for the orchestrator.
13. **Proposed route lines to report** (prefix as the file does): `GET/POST patients/{patient}/alerts`, `POST patients/{patient}/alerts/nka`, `POST alerts/{alert}/resolve`; `GET/POST patients/{patient}/conditions`, `PATCH conditions/{condition}`, `POST conditions/{condition}/resolve`; `GET/POST patients/{patient}/home-meds`, `POST home-meds/items/{item}/stop`; `GET patients/{patient}/clinical-notes`, `GET encounters/{encounter}/clinical-notes`, `POST patients/{patient}/clinical-notes`, `POST clinical-notes/{note}/addendum`.
14. Lang keys (`Modules/Clinic/lang/{ar,en}/errors.php` + `clinic.php`): enum labels + new 422 messages. CHANGELOG: don't edit — report 2 lines EN + AR.

### Tests — `Modules/Clinic/tests/Feature/D2aFactsTest.php` (prefix helpers `d2a_`)
alert add/resolve; adding a real allergy resolves `nka`; a condition with catalog `is_alert` appears in `activeFor` as danger without a `patient_alerts` row (acceptance: `anticoagulant` added from a dental-context call shows in the alert list of any context); home med add → `activeFor` includes it, stop removes it; outpatient issued Rx within window shows, older doesn't; private note invisible to another user, visible to author; addendum on a signed encounter ok, a plain note 422; lookup-backed single/multi select accepts catalog codes and 422s an unknown one; storage=condition boolean answer creates/resolves the PMH row; required profile field with `show_if patient.gender=female` is not enforced for a male patient (builder); backfill migration: medical_history allergies + `allergies` text + `important_note` + `diabetes=true` produce the right rows and a second run creates nothing; clinic_history_options rows copied once; activity log row written on alert update. Run also `HistoryEngineTest`, `HistoryMemoryTest`, `PlatformE1HistoryEngineTest`, `E1eAnswersTest`, `P1PrescriptionV2Test`, `IssuePrescriptionTest` and keep green.

### Out of scope (D2a)
Readers/header/snapshot (D2b), FE (D2c), intake wizard (D3), clinic_history_options removal, deleting any old question.

---

## D2b — BE readers + stamp at sign + header API (migrations `2026_11_03_171000`–`171099`, only if you need one — you probably don't)

### Do
1. **`PatientReadService::summaryFor`** (read the whole file): `allergies` → from `PatientAlertService::activeFor` kinds allergy/nka only (keep `pluckList(medical_history)` as a fallback ONLY when the patient has zero alert rows — one release); `chronic` → `PatientConditionService::list(active)` labels (keep the boolean-answer source as fallback only when PMH is empty); `current_meds` → `HomeMedicationService::activeFor` (drug label + sentence + kind), **not** "latest Rx"; `important_note` → the active `kind=note` alert text (fallback column). **Add** (additive keys, old keys keep their shapes): `alerts: [{id, kind, code, label, severity, reaction, source, ref_type, ref_id, set_at}]`, `conditions: [{id, code, label, status, since_when, is_alert, severity}]`, `meds: [{item_id, prescription_id, kind, drug_name, sentence, started_at}]`, `facts: {pregnant: {value, observed_at, stale}|null, breastfeeding: …, smoking: string|null, marital_status: string|null}` (pregnant/breastfeeding = latest encounter answer within `stale_after_days`, gender female only), `specialty: {}`.
2. **Specialty header slot:** `Modules/Clinic/app/Support/PatientHeaderExtensions.php` — static registry `register(string $clinicalType, callable(Patient, ?Encounter): array)`; `summaryFor(Patient $p, ?string $clinicalType = null, ?Encounter $e = null)` fills `specialty` from the registered callable (try/catch, never throws). The header endpoint accepts `?clinical_type=&encounter_id=` (read `PatientController`/the controller that serves `patients/{patient}/header` — find it with grep — and pass them through; validate clinical_type ∈ `ClinicalType::values()`). G1 registers `gyn_ob` (G/P/A/L), G5 adds EDD/GA.
3. **`SnapshotEncounterHistory`** (keep the raw-DB style + the conditional `whereNull` write): add to the snapshot `alerts` (active `patient_alerts` + derived condition alerts, as `activeFor` returns them — call the service; it must not depend on request context: pass company/patient ids explicitly), `conditions` (PMH active rows with labels), `surgical` (PSH rows), `family` (family_history rows), `meds` (`activeFor` items), `facts` (smoking/marital/pregnant as in §1) and bump `snapshot_version: 2`. Existing keys `captured_at/patient/encounter` unchanged.
4. **Signed-visit reads:** wherever a signed encounter's history/header is rendered from the snapshot today (grep `history_snapshot` readers — `EncounterHistoryController`, `VisitHistoryService`, `EncounterResource`), surface the new snapshot keys as `snapshot.alerts/conditions/meds` so the FE can show «ما رآه الطبيب وقت التوقيع». Add `GET encounters/{encounter}/snapshot` only if no existing endpoint returns the snapshot (report the route line).
5. **Other readers switched** (read each; keep behaviour otherwise): `ClinicDashboardService`/queue patient chips that show allergies; `Modules/Hospital` admission file header if it calls `summaryFor` (it should just get alerts for free — verify); prescription interaction warnings that read allergies (grep `allergiesList`, `allergies` in `Modules/Clinic/app` and `Modules/Hospital/app`). Do NOT touch Nutrition/Dental FE.
6. `medical_history` json: stop writing it from `PatientController` store/update? — NO, leave writes (owner screens still send it) — only reads move. Note it in the report.

### Tests — `Modules/Clinic/tests/Feature/D2bHeaderSnapshotTest.php` (prefix `d2b_`)
header shows alert from a dental-context condition in a gyn_ob call (acceptance #1); home med in `current_meds` and `meds` (acceptance #2); sign an encounter → change an allergy → the encounter snapshot still holds the old alert (acceptance #3); `specialty` slot calls a registered callable and survives a throwing one; pregnant fact null for male, stale after 30 days; old `allergies`/`chronic` shape still a list of strings. Run also every `*PatientHeader*`, `*PatientRead*`, `*Snapshot*`, `VisitHistoryTest`, `D2aFactsTest` in `Modules/Clinic/tests` and keep green.

### Out of scope (D2b)
FE, intake (D3), dropping `medical_history`, the GYN G/P/A/L callable itself (G1).

---

## D2c — FE: the unified patient header + fact cards (no migrations)

**Fable design consult is MANDATORY** before you build the header (it is on every clinical screen): send Fable (`model: "fable"`) the current `app-visit-header` + `app-clinic-patient-header` screenshots and the prototype header (`../visit-shell-prototype.html`, open in headless Chromium — helper in `S1-visit-shell-fe.md` §Spec 4) and ask for: alert pill tones/order (danger/warn/info + `nka` neutral), overflow on mobile (max 3 pills + «+N»), the meds/conditions strip, the specialty slot (G/P/A/L, EDD/GA), and the «snapshot at sign» state for signed visits. Keep the consult to 1–2 rounds; record its decisions in your report.

### Do (FE repo `/home/moonui3/public_html/moon-erp`)
1. `features/clinic/services/clinic-patient.service.ts`: extend `ClinicPatientHeaderSummary` with `alerts`, `conditions`, `meds`, `facts`, `specialty` (typed interfaces, all optional for back-compat); `getHeader(patientId, {clinicalType?, encounterId?})`; new `ClinicPatientFactsService` (same folder) for the D2a endpoints (alerts, conditions, home meds, notes) using the existing api-service pattern of the clinic services.
2. **One header rendering:** move the alert/condition/meds/specialty rendering into a presentational `features/clinic/shared/patient-alert-strip/patient-alert-strip.component.ts` (inputs: `alerts`, `conditions`, `meds`, `facts`, `specialty`, `compact`, `snapshotAt?`). Use it inside BOTH `shared/clinic-patient-header/clinic-patient-header.component.*` (keep all existing inputs/outputs and behaviour; only replace the allergies/chronic/meds markup) and `visit-shell/visit-header.component.ts` (replace its local `alerts()` computation; keep its inputs). RTL, tones from Fable, `role="list"`, contrast ≥4.5:1, pills keyboard-focusable with a tooltip showing source («من عيادة الأسنان · 3 مارس»).
3. **Fact cards (pick-don't-type, using the S1 shared `src/app/shared/components/clinical-picker/` box + dialog — do not fork it):**
   - `features/clinic/shared/patient-facts/alerts-card.component.ts`: active alerts list, «＋ حساسية» (picker on `lookup_key=allergen` + reaction + severity), «لا يوجد حساسية» one-click (`nka`), resolve with confirm, «＋ تنبيه» custom.
   - `conditions-card.component.ts`: chips of active conditions (picker on `lookup_key=condition`, ★ favourites, inline add), status toggle active/resolved, since_when; alert-flagged codes show a red dot.
   - `home-meds-card.component.ts`: «الأدوية الحالية» list (kind badge: home / خروج / روشتة), add outside drug (drug search reuse: `GET /clinic/drugs/search` from P1 — free-text allowed → `drug_name`), stop item.
   - `clinical-notes-card.component.ts`: sticky notes + addendum thread, private toggle with 🔒 and the hint «لا يراها غيرك ولا تظهر في الملخص».
   Register them as shell cards ONLY by adding entries to `visit-shell/card-catalog.ts` (`alerts`, `conditions`, `home_meds` kind shared layer 1; reuse the existing `notes` id for the notes card — first read `visit-shell/cards/visit-notes-card.component.ts` and what it saves today: keep that path working (if it writes encounter notes/plan text, show that field above the clinical-notes thread) rather than silently replacing it, and report what you did) and the shell's card `@switch` — no other S1 file edits. They take `[patientId] [encounterId] [locked]` and emit a `changed` event the header listens to (refresh token).
4. Patient file page (`features/clinic/patients/…` — the file that hosts `app-clinic-patient-header` for the patient file; grep) gets the four cards in its existing medical-history area **only if** it has a slot for them without restructuring; otherwise report and leave it.
5. Signed visit: when the encounter is signed, the header strip renders `snapshot.alerts/conditions/meds` with a small «وقت التوقيع {date}» chip and a toggle «الحالي» to see live data.
6. i18n `PATIENT_HEADER.*`, `PATIENT_FACTS.*` targeted edits in ar.json + en.json; no I18N_VERSION bump.
7. `npx ng build --base-href /app/` green. Do NOT deploy.

### Out of scope (D2c)
Intake wizard (D3), nutrition/dental screens (S2), GYN specialty slot content (G1), BE.

---

## Report back (each WP)
Files created/edited; test output with pass counts; route lines + permission names + setting definitions to paste; CHANGELOG lines EN/AR; any file you needed but did not touch; Fable decisions (D2c).
