# RonanRx: Technical Specification (technical.md)

**Date:** 2026-07-30 (v5)

**What this file is.** Mechanism only: tables, code paths, config and flags, schedules, and interfaces for each card on the system map. One home per fact: binding rules live in business.md, delivery status lives in spec/status.json, dated implementation evidence lives in as-built.md (which also pins the audited git refs). Sections marked TBD have no mechanism yet; that is intentional (founder ruling, 2026-07-22).

**Pricing.** All pricing rules and evidence live in business.md and as-built.md; this file only records the mechanisms (RxPricing, the catalog, billing account routing) where they appear on their owning cards.

**Changelog**

- **v5: 2026-07-30.** Reverified the deployed Flue onboarding path, its JSONB progress record, current completion predicate, optional chat-ID behavior, serial secure-completion gates, and known state-handoff gap.
- **v4: 2026-07-22.** Mechanism-only rewrite of every card section (same pass business.md received): status narration removed, canonical card names adopted in headings, empty sections say TBD, pricing cross-cutting block replaced by pointers.
- **v3: 2026-07-21.** Current-ref correction; Flue cutover documented.
- **v2: 2026-07-19.** Founder-response reconciliation.
- **v1: 2026-07-17.** Initial technical specification.

---

## Platform stack summary

- **App:** `ronanrx-core`: Rails 8.1 monolith, Ruby 4.0.
- **Database:** Postgres 17 (Cloud SQL instance `ronanrx-prod-pg17`). Schema is `db/structure.sql`.
- **Runtime:** GCP Cloud Run services `ronanrx-web`, `ronanrx-worker`, `ronanrx-migrate`. Isolated Cloud Run sidecar for Function Health imports (`Dockerfile.function-health-runner`, internal ingress, own service account, no Rails secrets).
- **Jobs:** Solid Queue; recurring schedule declared in `config/recurring.yml` (see "Recurring job schedule" at the end of this doc).
- **PII:** ActiveRecord encrypted columns (names, phones, DOB, addresses, clinical free text) with lookup-hash shadow columns (`*_lookup_hash`) for querying (e.g. `public_intakes.email_lookup_hash`/`phone_lookup_hash`). EHR tables additionally enforce encryption at the Postgres level via the `ehr_active_record_encrypted_envelope` CHECK constraint (rejects unencrypted PHI writes).
- **Enums:** string-backed Rails enums throughout (e.g. `Prescription` status `draft/valid_rx/dispensed/cancelled`; `Shipment` `ready_check/scheduled/dispatched/in_transit/delivered/exception`).
- **Activity & audit:** `events` (append-only activity log, polymorphic `eventable` with a type allowlist in `app/models/event.rb`) + `AuditLog`: exactly **11 hard-fail gates** (`AuditLog::HARD_FAIL_GATES`, `app/models/audit_log.rb:21-33`) and **24 policy gates** (`POLICY_GATES`, lines 41-66); append-only with HMAC signing on appointment chart-record payloads. The 11 hard-fail gates: `state_license_coverage_verified`, `ai_never_makes_final_prescribing_decision`, `doctor_approval_required_before_pharmacy`, `compounding_requires_patient_specific_prescription`, `pharmacist_release_required`, `shipment_requires_pharmacist_release`, `formula_version_requires_pharmacist_or_doctor_approval`, `ehr_prescription_requires_signed_encounter`, `compounded_dose_not_near_fda_strength`, `governed_artifact_requires_owning_role_signature`, `dea_schedule_controls_prescribing` (the last is **vestigial**: name only; enforcement removed 2026-07-14 with free-text prescribing; `test/services/workflows/hard_fail_gate_test.rb` = 38 tests).
- **Agents:** the 2026-07-30 deployment check found both production and staging routing onboarding turns to the focused Flue service. Production web/worker run core `2a346e1`; staging web/worker run `608c9d9`; the deployed agent images are `prod-goalthread6` and `goalthread6`. The live onboarding model is `gpt-5.5` with reasoning disabled. The remaining Ruby real-LLM agents use the BaseAgent OpenAI path. Full Ruby class census from the earlier audit remains in §9.8 and is separate from deployed routing.
- **Workflows:** `app/services/workflows/` + `app/services/ehr/workflows/`: guarded transition objects that raise `InvalidTransition` and write gate audit rows.
- **Git flow:** staging-first; production deploy asserts release SHA via `DeploymentReleaseCanaryJob`.
- **Boundary:** EHR zone (`app/models/ehr/`, `app/services/ehr/`, `/ehr` routes) crossed only via the `Ehr::Integration` facade; `bin/architecture-check` (advisory, core→EHR) + `test/architecture/ehr_boundary_test.rb` (CI, EHR→core).

---

## Section 0: Agent Ground Rules

### Cam's Crack Commandments

No mechanism: these are posture rules for agents, not software. The binding list lives in business.md Section 0.

---

## Section 1: Patient Entry & Onboarding

### 1.1 iMessage (Linq)

**Data:**
- `secure_links`: `code` (22-char alphanumeric, `SecureLink::CODE_LENGTH = 22`), `token`, `intake_response_id`, `purpose` (default `completion`; values `completion`, `id_upload`), `linq_conversation_id`, `expires_at`.
- `linq_conversations`: `chat_id`, `contact_key`, `flow_state`, `answers` jsonb, `intake_state`, `intake_version` (default 6), `completed_version`, `opt_out`, `phone_binding_digest`, `intake_response_id`, `human_takeover_actor_id`, `human_takeover_started_at`.
- `linq_events`: unique index `index_linq_events_on_message_id` for inbound replay dedupe.

**Code paths:**
- Models: `app/models/secure_link.rb` (`issue_or_reuse_completion!`); `app/lib/signup_magic_link.rb` (signed `MessageVerifier` token, per-issuance nonce on `intake_responses.portal_token_nonce`).
- Services: `app/services/linq/` (`webhook.rb`, `flow.rb`, `signup_forward.rb`, `templates.rb`, `send_guard.rb`, `serialized_dispatch.rb`, `lines.rb`); `app/services/secure_link_card.rb` (SIGN-stage OG card, 1200x630 PNG rendered via libvips); `app/services/signup_completion_links/resend.rb`.
- Controllers: `app/controllers/api/v1/linq_controller.rb`; `app/controllers/portal/completions_controller.rb`; `app/controllers/portal/short_links_controller.rb`; `app/controllers/portal/id_uploads_controller.rb`.
- Jobs: `LinqReplyJob` (send-guarded outbound); `LinqContactCardJob`.

**Config:**
- `SignupMagicLink::EXPIRES_IN = 24.hours`; `SignupMagicLink::PORTAL_MIN_EDIT_WINDOW = 5.minutes` floor.
- Advertised intake line from `LINQ_LINES`, falls back to `+15125632172` (`Linq::Lines::DEFAULT`).
- Inbound body cap `Linq::Webhook::MAX_BYTES = 32_768`.
- Outbound auth `LINQ_API_TOKEN`; inbound Standard-Webhooks signature `LINQ_WEBHOOK_SECRET`.

**Interfaces:**
- Routes: `post api/v1/linq/inbound` (back-compat `api/linq/inbound`); `get p/:code` and `get p/:code/card.png`; `portal/signups/:token`; `portal/complete/:token` wizard (`show`, `update_email`, `sign_packet`, `update_payment`, `capture_id`, `update_schedule`); `portal/id/:token`; `get /start` 301-redirects to `/`.
- Linq partner API for iMessage/SMS transport; GCS (`storage.googleapis.com/ronanrx-public-assets`) for static PAY/BOOK stage preview cards.

**Notes:**
- Triggers: signature-verified inbound webhook drives the conversation state machine; intake reaching COMPLETE mints or reuses the completion link; ops-console manual resend via `signup_completion_links/resend`.
- Inbound dedupe in `LinqController#inbound`: `LinqEvent.exists?(message_id:)` fast path plus advisory-locked recheck, backed by the `linq_events` unique index.

### 1.2 Onboarding Agent

**Data:**
- Rails is system of record for `linq_conversations`, `linq_events`, patient and intake records, secure completion links, opt-out, safety holds, and staff takeover.
- Conversation key derived from the Linq chat identifier (`RrxAgents::ConversationKey.build`) so retries and completion checks address the same agent record.
- Flue persists one versioned JSONB document per conversation in `agent.onboarding_records.data`. The document contains `patientGoals[]`, `desiredMeds[]`, `state`, `currentMedications[]`, `allergies`, `medicalConditions[]`, height/weight and `biometricsAddressed`, identity candidate state, attachments, interactions, and updates.

**Code paths:**
- Rails ingress and policy: `Api::V1::LinqController`, `Linq::ConversationalIntake`, emergency and dosing branches, human-takeover controls.
- `Linq::AgentTurnJob` (`app/jobs/linq/agent_turn_job.rb`) calls `RrxAgents::TurnClient`, forwarding accepted text and up to five JPEG or PNG images; `RrxAgents::OnboardingRecord` and `RrxAgents::CompleteOnboarding` read completion state and create the completion-link flow.
- `app/services/rrx_agents/`: `turn_client.rb`, `onboarding_record.rb`, `complete_onboarding.rb`, `conversation_key.rb`, `identity_token.rb`.
- `rrx_agents/src/tools/onboarding.ts` defines and upserts the JSONB progress document. Rails projects the record through `app/services/agent_intake/handoff.rb` into the canonical `IntakeResponse`.
- Delivery via `LinqReplyJob.enqueue_conversational` (rechecks newer inbound, opt-out, safety holds, takeover, dedupe, SendGuard, retries).

**Config:**
- `Linq::AgentTurnJob::MAX_AGENT_IMAGES = 5`; each forwarded image is capped at 10 MiB and must be JPEG or PNG.
- `RRX_AGENTS_BASE_URL`; `RRX_AGENTS_USE_IAM` selects a Google OIDC identity token (hosted) or the static `RRX_AGENTS_TOKEN` bearer (local dev).
- Current Flue completion requires all of `patientGoals`, `currentMedications`, `allergies`, and `medicalConditions`, plus either a captured height/weight or a truthy `biometricsAddressed`.
- `state`, `desiredMeds`, and chat ID do not currently gate completion.

**Interfaces:**
- Rails calls the rrx-agents run-to-completion route `POST /api/agents/onboarding/:key?wait=result` in both deployed environments.

**Notes:**
- The ID is offered once in chat after the clinical questions. Refusal is acknowledged without pressure or a repeat ask, then the agent continues to the height/weight beat. The response is generative; no exact "That's okay" literal is guaranteed.
- `Linq::AgentTurnJob` has no `retry_on`. Timeout or hard failure logs and alerts but does not currently send the deterministic patient-facing fallback previously described by this spec.

### 1.3 Intake 1: Basics

**Data:**
- `intake_responses`: `answers` jsonb, `status` (draft/submitted), `patient_program_enrollment_id`, `consent_id`, `id_document_id`, `portal_token_nonce`, `portal_token_consumed_at`, `signup_session_token`, `source`, `transcript`, `completed_step_index`.
- `public_intakes`: `name`, `email_lookup_hash`, `phone_lookup_hash`, `state`, `programs` jsonb, `qualifiers`, `serviceable`, `status`, `outcome`, `verification_*`, `patient_id`.
- `leads`: B2B partnership form (`company`, `message`, `pdf_requested`, `source` default `partnership_form`); not patient-funnel data.
- `patients`: encrypted name/email/phone/dob/address, `sex`, `state`, `state_of_record`, `clinic_id`, `user_id`, `stripe_customer_id`.
- `dead_letter_intakes`: failed intake ingest for replay; `funnel_counters`: `key`, `day`, `count`.
- `ehr_contraindication_answers`: DB CHECK `chk_ehr_contra_answers_source_provenance` requires each row carry a `source` (`agent_chat`, `call_transcript`, `portal`, `doctor_entry`) with matching `verification` (`patient_reported` or `clinician_verified`) and a provenance pointer (`linq_event_id`, `pre_doctor_intake_id`, `source_artifact_id`, `recorded_by_id`).
- Funnel goal stored in `intake_responses.answers['goal']`.
- The current Flue JSONB record is partial-progress storage, not a loose JSON file. Its answer-level audit history remains in the same versioned record until Rails handoff.

**Code paths:**
- `app/services/linq/flow.rb`: `INTAKE_STEPS`, `CURRENT_INTAKE_VERSION = 6`, `REQUIRED_STEPS_BY_VERSION`, `compute_missing`.
- `app/services/linq/signup_forward.rb`: `FIELD_MAP` (age, weight, height, conditions, medications, allergies, goal, state, email to intake answers), `sync_patient_from_answer` (name, state), `write_vitals_to_chart`, `find_or_bootstrap` (phone advisory lock), `bootstrap_chat_signup`.
- `app/services/public_intakes/upsert.rb`, `patient_linker.rb`, `pull_from_bucket.rb`; `app/services/onboarding/serviceability.rb` (state gate).
- `app/services/analytics/funnel_milestone.rb`.
- Controllers: `app/controllers/api/intake_controller.rb`; `app/controllers/api/v1/signups_controller.rb`.
- Jobs: `PublicIntakePullJob`, `PublicIntakeSweepJob`.

**Config:**
- v6 required steps: goal, first_name, conditions, medications, allergies (goal-first); version persisted per conversation so deploys do not move in-flight users.
- Empty clinical list forwards as `None reported`; deferred capture forwards `Linq::SignupForward::DEFERRED_CLINICAL_SENTINEL = "Pending secure intake"`.
- Phone normalization: `signup_phone` requires 10+ digits; `normalize_e164` must match `+\d{8,15}`.
- `LINQ_FORWARD_SIGNUP` (default `true`) gates in-process answer forwarding per chat turn.
- Funnel milestone `demo_signup_submitted` emitted once per intake, idempotency key `signup:submit:<id>`, deduped via `analytics_idempotency_keys` (`Analytics::FunnelMilestone`).
- Approved target handoff minimum is `service_state AND (patient_goal OR desired_medication)`. This target is not deployed. Current Flue still uses the stricter clinical completion floor documented in §1.2.
- Flue captures `fields.state`, but `RrxAgents::OnboardingRecord#basics` does not currently project it and `Linq::AgentTurnJob` passes `patient.state`; a new patient can therefore lose the SMS state during handoff.
- No `missing_for_later` manifest exists today, and `/p` does not insert a missing-basics form. The governed Intake 2 question inventory and prefill path are the target continuation surface.

**Schedules:**
- `PublicIntakePullJob` every 15 minutes; `PublicIntakeSweepJob` daily 03:00 America/Chicago (`config/recurring.yml`).

**Interfaces:**
- Routes: `post api/intake` and `post api/submit` (unauthenticated lead capture, never creates a patient account); `post api/v1/intakes`; `api/v1/signups` create with member `answer` and `submit` and collection `by-phone/:e164` (constraint `+\d{8,15}`); `admin/public_intakes`; `admin/leads`.
- GCS leads bucket via `LeadsBucketClient`; server-side PostHog funnel analytics (`PosthogCaptureJob`).

**Notes:**
- `bootstrap_chat_signup` hard-codes seeded clinic slug `voss-precision-health`, program slug `weight`, and referring doctor `amelia.voss@vossprecisionhealth.com`; `find_by!` raises `RecordNotFound` when those seeds are absent.
- New chat-signup patients start in state TX via `Identity::StateOfRecord` source `system_default` until real state is captured.

### 1.4 Sign Docs / Agreement Packet

**Data:**
- `consents` (db/structure.sql:1911): `patient_id`, `kind`, `status`, `seal_state`, `signed_at`, `document_version`, `metadata` jsonb, `phone_binding_digest`, `signature_hash`, `expires_at`, `revoked_at`, `supersedes_id`, `signed_by_id`.
- `provider_agreement_acceptances` (:6228): `provider_id`, `packet_id`, `packet_version`, `content_sha256`, `sealed_pdf_locator`, `signer_name`, `attestations` jsonb.
- `authorization_tokens` (:1679); `uploaded_documents` (:7548) rows with source `'agreement_packet'`.

**Code paths:**
- `app/services/agreement_packet_page.rb`: self-contained sign page with a Consent, Pay, Schedule header.
- `app/services/text_authorizations/contract.rb`, `packet_renderer.rb`, `pdf_renderer.rb`.
- `app/services/waiver_store.rb` (`GCS_WAIVERS_BUCKET`): create and get at request time; list and delete reserved for the reconcile service account.
- `app/services/provider_agreement_packet/cover_fields.rb`: provider twin, carries `provider_fee_cents` and `PLATFORM_FEE_CENTS = 1000`.
- `app/services/workflows/seal_text_authorization.rb` via `SealTextAuthorizationJob`.
- `app/controllers/portal/completions_controller.rb#sign_packet`: `packet_signature_complete?` returns 422 on a partial signature and writes no consent or PDF; `#persist_packet_consent` writes one idempotent consent per IntakeResponse; PDF returned via send_data.
- `app/controllers/authorizations_controller.rb`, `app/controllers/waivers_controller.rb`.
- `ProviderAgreementPacketCleanupJob`: enqueued with `CLEANUP_DELAY` from `Workflows::CreateProviderFromInvite`.

**Config:**
- `config/text_authorization/agreement_packet_contract_v1.yml`: `packet_version 2026.07.09.2`, 7 ordinals.

**Schedules:**
- `TextAuthorizationReconcileJob` every 15 min (`config/recurring.yml text_authorization_reconcile`): self-heals missed seals.
- `AuthorizationTokenReapJob` daily 04:00 CT (`authorization_token_reap`).

**Interfaces:**
- `post portal/complete/:token/sign_packet` (config/routes.rb:181).
- `connect/consents/:kind` show and sign (:222-223); `provider/onboard/sign` (:233).
- Auth host `auth.ronanrx.com` (`AuthHostConstraint`, :550): `/a/:token` sign surface, `/w/:token` waiver retrieval and integrity.
- GCS waivers and agreement-packet buckets (ADC service-account auth).

**Constraints:**
- Packet is 7 documents (ordinals 1-7): RonanRx Patient Platform & Marketplace Agreement; Provider Election & Care-Relationship Addendum; Telehealth Consent & Consent to Treat; Payment & Billing / Subscription Terms; Privacy, HIPAA & Records-Sharing Authorization; Pharmacy & Compounding / Medication Disclosure; Mobile Messaging & AI Consent, Authorization & Communications Preferences.
- Completeness gate requires e-sign agreed, signer name, a real typed or drawn signature, and every ordinal signed, else 422 with no consent or PDF.
- Ordinal 7 rendered non-skippable (`skippable: false`, app/views/portal/completions/show.html.erb:311).
- One IntakeResponse signs one packet; re-sign requires a fresh intake after reset.
- Waiver GCS access is create, get, and delete with no list permission.

### 1.5 Membership Pay

**Data:**
- `patient_program_enrollments` (db/structure.sql:5817): `patient_id`, `program_id`, `status` (default `'draft'`), `stripe_subscription_id`, `subscription_status`, `referring_doctor_id`, `enrolled_at`.
- `patients.stripe_customer_id`: unique partial index `index_patients_on_stripe_customer_id` (WHERE stripe_customer_id IS NOT NULL).
- `prescription_payments` (:6056): separate second-account medication charge table (see Prescription Pay).

**Code paths:**
- `app/services/onboarding/activate_subscription.rb`: `Stripe::Subscription.create` with coupon `first_month_free`, idempotency key `ronanrx_sub_enrollment_<id>_<setup_intent>`, row lock.
- `app/services/onboarding/prepare_payment.rb`: Stripe Customer plus reusable SetupIntent (card with Apple Pay and Google Pay wallets; Stripe Link disabled on this account).
- `app/services/onboarding/payment_eligibility.rb`, `cancel_subscription.rb`, `completion_state.rb` (payment-gating state machine).
- `app/services/payments/mode.rb`; `app/services/ops/integration/payment_diagnostics.rb`.
- `app/lib/billing.rb`: `COUPON_ID = 'first_month_free'`, `price_id` reads `STRIPE_PRICE_ID`.
- `app/controllers/portal/completions_controller.rb#update_payment`, `#update_schedule`.
- `app/controllers/webhooks/stripe_controller.rb`: signature-verified, idempotent on event id (cached 3 days); `setup_intent.succeeded` backstop; `customer.subscription.updated` and `customer.subscription.deleted` sync `subscription_status`.

**Config:**
- `config/initializers/stripe.rb`: `Stripe.api_key = ENV['STRIPE_SECRET_KEY']` (global).
- `config/initializers/payments.rb`: `PAYMENT_MODE`, default `embedded`.
- `lib/tasks/stripe.rake`: creates "RonanRx Membership" product, price `unit_amount 3900`, lookup_key `ronanrx_membership_monthly`, coupon `first_month_free` (100% off once), promo code `YC`.
- Active membership price lives in Stripe (`STRIPE_PRICE_ID`); the repo hard-codes 3900 cents only in the rake task.

**Interfaces:**
- `post stripe/webhook` (config/routes.rb:114); `post rx/stripe/webhook` (:118, separate Rx account).
- `post portal/complete/:token/payment` (:182).
- patient `prescription_payments` member actions (:253).
- ops `post api/internal/ops/v1 payments/membership/diagnose` (:82).
- Stripe membership account (global api key); Apple Pay and Google Pay via Payment Element.

**Constraints:**
- `PAYMENT_COMPLETE_STATUSES = %w[active trialing]`; `PAYMENT_CONFIRMING_STATUSES = %w[incomplete pending processing]` (completion_state.rb:5-6).
- Secure completion is serial: ID verification, then the agreement packet, then payment, then video scheduling or async review. `Portal::CompletionsController` hard-gates every later step on the earlier one.
- Membership copy hard-coded at completion_state.rb:295: "Your first month is free, then $39/month, cancel anytime. A doctor still reviews everything before any prescription is issued."

### 1.6 Conversion Agent

**Data:**
- `health_history_nudges` (db/structure.sql:4434): `patient_id`, `nudge_number`, `completion_percent`, `dedupe_key`, `delivery_mode` (default `'dry_run'`), `dispatched_at`, `enqueued_at`.
- `weigh_in_nudges` (:7870); `pre_doctor_intake_reminders` (:6013).

**Code paths:**
- `app/services/signup_completion_links/resend.rb`: admin-triggered, reuses a fresh SecureLink, delivered through send-guarded `LinqReplyJob`.
- `app/services/health_history/nudge_dispatcher.rb`, `nudge_notifier.rb`: cohort of active enrollment with a Linq thread below 100% profile completion.
- `app/services/weigh_in/nudge_dispatcher.rb`.
- `app/services/pre_doctor_intake_reminders/eligibility.rb`, `notifier.rb`, dispatched via `app/services/ops/commands/pre_doctor_intake_reminder.rb`.
- `app/services/rx_payments/payment_link_recovery.rb`: staff-only, post-prescription.

**Schedules:**
- `HealthHistoryNudgeJob` daily 10:00 CT (`config/recurring.yml health_history_nudge`).
- `WeighInNudgeJob` Mondays 10:00 CT (`weigh_in_nudge`).
- `RxPaymentLinkRecoveryDispatchJob` every minute (`rx_payment_link_recovery_dispatch`): delivers staff-requested Rx pay-link recoveries.
- `RxCheckoutReconcileJob` every 15 min (`rx_checkout_reconcile`).

**Interfaces:**
- ops `post resend_completion_link` on conversations (config/routes.rb:452).
- `post api/ops/v1 intake-reminders/candidates` (:88).
- Linq delivers all nudges over the existing Linq thread.

**Constraints:**
- `HealthHistoryNudge::MAX_SENDS = 3` lifetime, `THROTTLE_WINDOW = 72.hours`, skipped at 100% completion (app/models/health_history_nudge.rb:17-19).
- `WeighInNudge::THROTTLE_WINDOW = 72.hours`; skipped when a scale measurement exists within 7 days.
- `PreDoctorIntakeReminder::MAX_SENDS = 3`, `THROTTLE_WINDOW = 24.hours`.
- `RxPayments::PaymentLinkRecovery::REQUEST_WINDOW = 7.days`; staff-only actor gate.

**Notes:**
- Nudge copy is fixed `Linq::Templates` delivered through send-guarded `LinqReplyJob`.

### 1.7 ID Verification

**Data:**
- `web_id_uploads` (db/structure.sql:7744): `intake_response_id`, `state` (CHECK pending|verified|failed|needs_review), `reason`, `attempt_no` (CHECK > 0), `content_sha256`, `origin` (CHECK = 'portal'), `document_id`, `linq_conversation_id`, `purge_object_key`/`purge_generation`/`purge_document_id`.
- `secure_links.purpose = 'id_upload'` (`SecureLink::PURPOSES = %w[completion id_upload]`, `CODE_LENGTH = 22`, app/models/secure_link.rb:12,16).
- `uploaded_documents` ID doc rows; `ehr_encounters.identity_verified` (default false) and `identity_verification_method` (db/structure.sql:2638-2639).

**Code paths:**
- `app/services/onboarding/web_id_photo_capture.rb`: portal upload, front image only.
- `app/services/linq/id_auto_promote.rb`: mid-chat license photo bootstraps Patient and IntakeResponse and promotes to GCS; `build_or_extend_document` can attach a back image via `Identity::IdDocumentStore#attach_back!`.
- `app/services/identity/id_document_extractor.rb`: OCR via `SOURCE = "gemini_id_ocr"`, `media_for` reads front only, `Identity::GeminiConfig`.
- `app/services/identity/id_document_store.rb` (`GCS_ID_DOCUMENTS_BUCKET`, no-list): `object_key_back`/`generation_back` storage fields.
- `app/models/linq_conversation.rb#id_on_file?` returns true once `intake.waiver_signature_captured?`, stopping the onboarding agent's ID request.
- `app/controllers/portal/completions_controller.rb#step_accessible?`: ID is the first hard gate; the agreement packet, payment, and scheduling/async choice unlock serially after it.
- `app/services/ehr/workflows/sign_encounter.rb#advise_telehealth`: evaluates `identity_verified`/`identity_verification_method` as appointment completeness and logs a warning without blocking signing.
- `WebIdOcrJob`: enqueued on upload.

**Schedules:**
- `WebIdUploadRecoveryJob` every 15 min (`config/recurring.yml web_id_upload_recovery`).
- `WebIdUploadPurgeJob` daily 04:45 CT (`web_id_upload_purge`).

**Interfaces:**
- `post portal/complete/:token/id` (config/routes.rb:183).
- `portal/id/:token` show, create, status (:187-189).
- ops `get id_document` on conversations (:459).
- Google Gemini for ID OCR; GCS ID-documents bucket.

**Constraints:**
- `web_id_uploads` CHECKs: `state` in pending|verified|failed|needs_review; `attempt_no > 0`; `origin = 'portal'`; failed requires reason; needs_review requires document and reason; verified requires document.
- Purge columns and `WebIdUploadPurgeJob` cap retention.

**Notes:**
- Visit-side attestation is a manual checkbox rendered as "Identity verified for this appointment" (app/views/ehr/encounters/_telehealth_fact_fields.html.erb:50-52).

---

## Section 2: Doctor Matching & Scheduling

### 2.1 Dr. Picker

**Data:**

- `users`: `role` ("doctor"), `status`, `clinic_id`, `license_number`, `license_state`, `google_meet_user_id` (unique partial index), `setup_link_nonce` (unique partial index). `scheduling_display_name` and `scheduling_slug` are computed model methods on `User`, not columns.
- `ehr_provider_state_licenses`: `provider_id`, `provider_credential_id`, `state`, `license_number`, `status`, `expires_on`.
- `ehr_provider_credentials`; `clinics`: `name`, `slug`, `status`, `settings`, `white_label_pharmacy_name`.
- No `specialty` column exists in any table; "specialty" is free text only in provider invite/agreement cover fields (`app/services/provider_invites/create.rb`).

**Code paths:**

- `app/services/onboarding/doctor_roster.rb`: active clinic doctors, filtered to those with an active, unexpired state license in the patient's state backed by an active, unexpired credential, ordered round-robin by fewest upcoming booked visits then user id.
- `app/services/onboarding/schedule_doctor.rb`: doctor descriptors; defaults to a legacy synthetic identity (name "Dr. Payton Reiter", slug `dr-amelia-voss`, key `voss`, no backing `User` row).
- `app/controllers/portal/completions_controller.rb`: `assign_schedule_doctors`, `resolve_selected_doctor_slots`, `doctor_choice_allowed?`; view "Choose your doctor" group in `app/views/portal/completions/show.html.erb`.
- `app/services/doctor_signups/register.rb` (`MIN_PASSWORD_LENGTH = 12`); `app/services/doctor_setup/reconcile.rb`; `Ehr::Integration::Providers.provision!` (`app/services/ehr/integration/providers.rb`).

**Config:**

- `DOCTOR_SIGNUP_CODE`: gates doctor self-signup; unset fails closed (route redirects to staff sign-in), checked in `app/controllers/doctor_signups_controller.rb`.

**Interfaces:**

- Routes: `GET/POST doctors/sign_up`; portal completion step `?doctor=<key>`.

**Notes:**

- Round-robin default: fewest upcoming booked visits first, tie-broken by user id.
- State-license filter falls back to all active clinic doctors when none are licensed in the patient's state (`doctor_roster.rb`).
- Doctor choice is offered only on first onboarding; later flows pin the previously chosen doctor, then referring doctor, then clinic `primary_doctor`.
- Doctor self-signup creates credential and state-license rows inactive; prescribing refused until a human activates them.
- Deterministic service; no LLM in doctor selection.

### 2.2 Booking

**Data:**

- `appointments`: `patient_program_enrollment_id`, `patient_id`, `doctor_id`, `slot_id`, `starts_at`, `ends_at`, `doctor_name`, `doctor_slug`, `timezone`, `status` (default `booked`; enum booked/cancelled/completed), `source`, `metadata` jsonb, `visit_token`. Partial unique indexes `idx_appointments_one_booked_per_enrollment` and `idx_appointments_one_booked_per_slot` (both `WHERE status='booked'`) enforce one booked visit per enrollment and one booking per slot.
- `appointment_messages`: `appointment_id`, `kind`, `dedupe_key`, `delivery_mode` (default `dry_run`), `dispatched_at`, `enqueued_at`, `scheduled_start_at`.
- `doctor_calendar_feeds`: `doctor_slug`, `doctor_name`, `token` (`TOKEN_LENGTH = 32`, `revoke!`, `rotate!`), `revoked_at`.

**Code paths:**

- `app/services/onboarding/schedule_slots.rb`, `book_appointment.rb`, `cancel_appointment.rb`; `app/services/ops/integration/appointment_gateway.rb`.
- `app/controllers/appointments/visits_controller.rb`; `app/controllers/doctor/calendar_feeds_controller.rb`; `app/controllers/ehr/schedule_controller.rb` with `app/services/ehr/integration/schedule.rb`.
- `app/services/calendar/ics.rb`: RFC 5545 builder carrying a PHI-free "RonanRx visit" title and the gated visit link only.
- Jobs: `AppointmentConfirmationJob` on new booking; `AppointmentMeetRoomJob` on booking and reschedule when `MeetConfig.enabled?` (`app/lib/meet_config.rb`).

**Config:**

- Slots (`schedule_slots.rb`): `TIMEZONE = America/Chicago`, `MIN_LEAD_DAYS = 3`, `LOOKAHEAD_DAYS = 14`, `LIMIT = 60`, `DURATION = 20.minutes`, hard-coded `ALLOWED_WINDOWS` Mon-Fri grid.
- `ADHOC_WINDOWS`: one-off Central-time slots for the legacy doctor, exempt from `MIN_LEAD_DAYS`.
- Slot ids namespaced per doctor: `doc<id>-YYYYMMDD-HHMM-ct` (legacy `voss-...`).
- `Appointment::JOIN_OPENS_BEFORE = 10.minutes`, `Appointment::JOIN_GRACE_AFTER = 10.minutes`.

**Interfaces:**

- Routes: `GET v/:token`, `POST v/:token/join`, `GET v/:token/calendar`; `GET feeds/doctor/:token/calendar`; `GET dashboard/appointment`; ops `cancel_appointment`/`rebook_appointment`; `GET /ehr/schedule` with brief/open_visit/join.
- Linq SMS confirmation into the existing thread; patient .ics and doctor ICS subscription feed; Google Calendar template URL (`app/services/appointments/links.rb` `google_calendar_url` targeting `calendar.google.com/calendar/render`).

**Notes:**

- Cancel is soft (status set to cancelled, row retained) and clears `intake.answers['scheduling']` patient-wide (`cancel_appointment.rb`).
- Ops rebook may assign any active doctor cross-clinic; EHR care-team pre-seed (`Ehr::Integration::CareTeam.ensure_provider!`, EhrConfig-gated) grants chart access.
- Deterministic services and jobs; no LLM.

### 2.3 Intake 2: Pre-Appointment

**Data:**

- `ehr_pre_doctor_intakes`: `patient_id`, `intake_response_id`, `linq_conversation_id`, `governed_artifact_version_id`, `status` (default `collecting`), `activation_mode`, `plan_fingerprint`, `cursor_question_id` (encrypted), `selected_tracks`, `progress`, `revision_number`, `origin` (default `sms`; CHECK origin='sms' iff linq_conversation_id present), `current_plan_revision_id`, `current_packet_id`, `ready_at`, `abandoned_at`.
- `ehr_pre_doctor_intake_answers`, `_plan_revisions`, `_prompts`; `_packets`: `plan_revision_id`, `governed_artifact_version_id`, `packet_revision`, `status` (default `ready_for_clinician_review`), `payload` (encrypted jsonb), `payload_digest`; `_section_submissions`: `section` (CHECK in A/B/C), `idempotency_key_digest`, `canonical_payload_digest`, `result_snapshot` (carries digest and section CHECKs, no encrypted-envelope CHECK).
- `pre_doctor_intake_reminders`: `appointment_id`, `patient_id`, `ehr_pre_doctor_intake_id`, `linq_conversation_id`, `requested_by_id`, `nudge_number` (CHECK > 0), `dedupe_key`, `delivery_mode` (CHECK dry_run|send), `dispatched_at`, `enqueued_at`. Unique index on (appointment_id, scheduled_start_at, nudge_number).

**Code paths:**

- `app/services/ehr/pre_doctor_intakes/`: planner.rb (`RULESET_VERSION = 1`), activation.rb, questions.rb, packet_builder.rb, record_answer.rb, record_portal_section.rb, start_or_resume.rb, start_for_patient.rb, portal_eligibility.rb, readiness.rb, manual_takeover.rb, automatic_staff_handoff.rb.
- `app/services/linq/pre_doctor_intake_router.rb`: rides `LinqReplyJob`, calls `Ehr::PreDoctorIntakes::Activation.call`.
- `app/controllers/patient/pre_doctor_intakes_controller.rb`.
- `app/services/pre_doctor_intake_reminders/eligibility.rb`, `notifier.rb`; `app/controllers/api/ops/v1/intake_reminders_controller.rb`; `Ops::Integration::IntakeReminderGateway` (`app/services/ops/integration/intake_reminder_gateway.rb`); `Ops::Commands::PreDoctorIntakeReminder`.

**Config:**

- `GLP1_INTAKE_MODE` activation modes (`activation.rb`): `synthetic_rehearsal` (staging and synthetic patients only, governed schema_version == 2), `production_shadow` (keys only, no questions asked), `real_patient` (requires governed schema_version == 2); any other value falls to `manual_fallback` (no questions asked, no answers written).
- `GLP1_INTAKE_KILL_SWITCH`: hard-off.
- `LINQ_PRE_DOCTOR_INTAKE_REMINDER_MODE=send` else dry_run (`send_guard.rb`).
- Reminder caps: `PreDoctorIntakeReminder::MAX_SENDS = 3` per appointment and start time; `THROTTLE_WINDOW = 24.hours` per patient.

**Interfaces:**

- Linq SMS/iMessage transport for prompts and answers; GovernedArtifact versions pin the question set.
- Routes: patient `resource :pre_doctor_intake` (show, update); `POST api/ops/v1 intake-reminders/candidates`.

**Notes:**

- Deterministic planner over a governed, versioned question artifact; portal section flow A/B/C; completed packets surface in the doctor's pre-call brief.
- Reminders are ops/human-triggered; no recurring dispatch job in config/recurring.yml.
- Reminder blockers (`eligibility.rb`): appointment_not_current, intake_ready, staff_review_required, portal_unavailable, text_consent_missing (signed `text_messaging` consent required), linq_thread_missing, patient_opted_out, phone_binding_mismatch.
- SMS commands (`pre_doctor_intake_router.rb`): `manual review|staff help|human help` for manual takeover; `correct <question_id>: <value>` for a correction.
- Intake reminders enqueue via `LinqReplyJob.enqueue_exempt` (`notifier.rb`) and deliver during an assistant pause or human takeover; intake question prompts remain takeover-suppressible.

### 2.4 Appointment Reminders

**Data:**

- `appointment_messages`: `kind` (confirmation | reminder_24h | reminder_1h), `dedupe_key`, `delivery_mode` (default `dry_run`), `scheduled_start_at`, `dispatched_at`, `enqueued_at`. Unique per (appointment, kind, scheduled_start_at).

**Code paths:**

- `app/jobs/appointment_reminder_job.rb` (calls `Appointments::ReminderDispatcher.call`); `app/jobs/appointment_confirmation_job.rb`.
- `app/services/appointments/reminder_dispatcher.rb`, `notifier.rb`, `links.rb`.
- `Linq::Templates`, `Linq::SendGuard`; transport `LinqReplyJob`.

**Schedules:**

- `AppointmentReminderJob` runs every 15 minutes (`config/recurring.yml appointment_reminders`).

**Config:**

- Send mode: dry_run unless `LINQ_TEXTFLOW_AUTOREPLY_MODE=send`; appointment templates are members of `TEXTFLOW_TEMPLATE_IDS` and routed by `SendGuard.reply_mode_for_template` to `textflow_reply_mode`, not to `LINQ_WELCOME_AUTOREPLY_MODE`. Templates: `appointment_confirmation_v1`, `appointment_reminder_24h_v1`, `appointment_reminder_1h_v1` (`linq/templates.rb`).
- Link base from `APP_BASE_URL` (default `https://ronanrx.com`); `SendGuard` permits only `LINQ_SECURE_LINK_HOST` and its subdomains (default `ronanrx.com`), so the two settings must agree (`links.rb`, `send_guard.rb`).

**Interfaces:**

- Linq partner API (SMS/iMessage outbound). Link carried: `GET v/:token`. Confirmation carries `calendar_url` (.ics); reminders carry `visit_url`.

**Notes:**

- Two windows, T-24h and T-1h, deduped independently; the 24h window range includes the 1h window (`reminder_dispatcher.rb`).
- Live re-check before send: cancelled or rescheduled-out-of-window visits skip with no ledger row, so a later valid window can still fire.
- Takeover-exempt: `Appointments::Notifier` enqueues via `LinqReplyJob.enqueue_exempt` under the `TAKEOVER_CONVERSATIONAL`/`TAKEOVER_EXEMPT` policy contract (`linq_reply_job.rb`); confirmations and both reminders deliver during an assistant pause or human takeover.
- Deterministic recurring sweep with fixed templates; no LLM.

### 2.5 Pre-Brief

**Data:**

- `artifacts` kind `physician_brief`: payload keys goals, relevant_history, active_medications, allergies, lab_trends, risk_flags, missing_data, confidence, human_approval_required, reasoning_steps.
- Reads `ehr_medications`, `ehr_allergies`, `ehr_patient_profiles`, `ehr_pre_doctor_intake_packets`.

**Code paths:**

- Deterministic read model `app/services/ehr/integration/pre_call_brief.rb`, rendered by `app/controllers/ehr/schedule_controller.rb#brief`.
- Real-LLM `app/services/agents/patient_summary.rb` (`real_claude!`, `clinical!`, artifact `physician_brief`), shown on `app/controllers/doctor/cases_controller.rb` via `@brief_artifact`; `app/helpers/clinical_briefs_helper.rb`.
- `app/services/workflows/submit_for_doctor_review.rb` produces the `physician_brief` artifact.
- `IntakeResponse#to_brief_payload` (`app/models/intake_response.rb`) pivots intake answers into a `physician_brief` payload; consumed by `Ehr::Agents::NoteDraft` and `Ehr::Agents::RxSuggestions`.
- Doctor calendar feed: `app/models/doctor_calendar_feed.rb`, `app/controllers/doctor/calendar_feeds_controller.rb`, `app/services/calendar/ics.rb`.

**Config:**

- OpenAI via `BaseAgent#llm_call`; model default gpt-5.5 (`OPENAI_MODEL`).

**Interfaces:**

- `GET /ehr/schedule/appointments/:appointment_id/brief` (`schedule_appointment_brief`).

**Notes:**

- `PreCallBrief` caps: 10 most-recent active medications, 10 allergies, 6 must-ask questions, 5 listed conditions/symptoms.
- `PatientSummary` context caps: first 20 imported labs, last 14 wearable vitals, 10 recent events; output sets `human_approval_required = true`.
- Doctor ICS calendar payload is PHI-free: times, generic title, and the authenticated `/v` visit-token link only; brief content requires an authenticated doctor session.

### 2.6 Appointment

**Data:**

- `meet_spaces`: `appointment_id`, `space_name`, `meeting_uri`, `meeting_code`, `status` (default `pending`), `failure_reason`, `subscription_name`, `subscription_expires_at`, `doctor_joined_at`, `patient_joined_at`, `conference_started_at`, `conference_ended_at`, `active_conference_record`, `transcript_ingest_enqueued_at`, `access_locked_at`.
- `ehr_video_sessions`: `encounter_id`, `vendor` (default `linq`; `google_meet` rows written by ingest), `external_ref` (unique conference record), `status`, `started_at`, `ended_at`, `recording_ref`, `transcript_ref`, `failure_reason`.
- `consents` kind `recording` (metadata.appointment_id, superseded_at chain; sources `patient_visit_page` / `doctor_prejoin`).

**Code paths:**

- `app/services/appointments/meet_room.rb` (`ensure!` creates a Meet REST v2 space, transcription on), `meet_sweep.rb`, `recording_consent.rb`.
- `app/services/google_workspace/meet_client.rb` (`create_space!`, `patch_artifact_config!`, conferenceRecords/participants/transcripts), `credentials.rb`, `http.rb`, `speaker_attribution.rb`; `app/lib/meet_config.rb`.
- `app/services/workflows/authorize_appointment_join.rb` (sole Meet-URL release path), `app/services/workflows/record_recording_consent.rb`, `app/services/ehr/integration/recording_consents.rb`.
- Transcript-to-note: `MeetVisitTranscriptIngestJob` -> `Ehr::Workflows::IngestVisitTranscript` (fetch conference/participants/transcript, `GoogleWorkspace::SpeakerAttribution` matches the doctor via `users.google_meet_user_id`, claims `Ehr::VideoSession`, opens the chart record via `Ehr::Workflows::OpenAppointmentVisit`, `Ehr::TelehealthPrefill`) -> `Ehr::Agents::NoteDraft` (artifact `note_draft`, prompt `ehr_note_draft.v3`), advisory `Ehr::Agents::RxSuggestions` and `Ehr::Agents::IcdSuggestions`.

**Schedules:**

- `AppointmentMeetRoomJob` (retry_on `GoogleWorkspace::Http::Error`, 8 attempts).
- `AppointmentMeetSweepJob` every 15 minutes.
- `AppointmentMeetTranscriptSweepJob` every 2 minutes (args [10]).
- `NoteDriftSamplingJob` Monday 07:00 America/Chicago.

**Config:**

- `MeetConfig.enabled?` requires `MEET_PER_VISIT_ENABLED`, `MEET_AGENT_EMAIL`, `MEET_DWD_SIGNING_SERVICE_ACCOUNT`; with flags off, falls back to shared room `RONANRX_VISIT_MEET_URL` (validated against `meet.google.com` xxx-xxxx-xxx).
- `GOOGLE_EVENTS_TOPIC` / `GOOGLE_EVENTS_PUSH_SA_EMAIL` / `GOOGLE_EVENTS_PUSH_AUDIENCE` / `GOOGLE_EVENTS_SUBSCRIPTION` reserved and inert; transcript capture is sweep-only polling.

**Interfaces:**

- Google Meet REST API v2 (spaces, conferenceRecords, participants, transcripts) via Workspace domain-wide delegation; GCS PhiDocumentStore for transcript artifacts.
- Routes: `GET /v/:token`, `POST /v/:token/recording_consent`, `POST /v/:token/join`, `GET /v/:token/calendar`; `POST /ehr/schedule/appointments/:appointment_id/recording_consent`, `.../join`.

**Notes:**

- Join window `JOIN_OPENS_BEFORE = 10.minutes` before start to `JOIN_GRACE_AFTER = 10.minutes` after end (`app/models/appointment.rb`).
- Room accessType OPEN, `autoTranscriptionGeneration` ON; `autoRecordingGeneration: "OFF"` hard-coded both modes (`meet_client.rb`), smart notes off; consent state patches transcription per visit at join.
- Drafting skipped when the conference falls outside the appointment window (`GRACE_WINDOW_BEFORE = 2.hours`, `GRACE_WINDOW_AFTER = 4.hours`) or the chart record is already signed.
- `NoteDraft` transcript cap `TRANSCRIPT_MAX_CHARS = 60_000` (20k head / 40k tail truncation); note types SOAP and hpi_mdm.
- Stalled drafts expire via `DEFAULT_DRAFT_WATCHDOG_WINDOW_MINUTES = 10` -> `Ehr::Integration::NoteDraftArtifacts.expire_stalled!`.
- Sweep timers (`meet_sweep.rb`): INGEST_GRACE 2min, FAST_WINDOW 30min, PENDING_RETRY_AFTER 10min, ENQUEUE_DEDUP_WINDOW 30min, WARN_AFTER 24h, ESCALATE_AFTER 14d, EXPIRE_AFTER 30d, STRAGGLER_AFTER 1h, NO_SHOW_AFTER 1h, BACKSTOP_LOCK_AFTER 24h, BACKFILL_WINDOW 7d, SUBSCRIPTION_LOOKBACK 48h.
- `Ehr::Workflows::SignEncounter` guards `actor.doctor?`; `IssuePrescription`/`ConfirmPrescription` require the appointment signer.

### 2.7 Visit Observation

**Data:**

- Reuse the appointment's `meet_spaces` and `ehr_video_sessions` records for the immutable Meet space, conference, recording, transcript, and EHR encounter chain.
- Store image bytes through the existing EHR artifact and PHI document abstractions after a schema census in the core Rails repository. Do not use local files or user downloads as durable storage.
- Each screenshot artifact needs patient, appointment, encounter, assigned-doctor, Meet space, conference, recording resource, transcript interval, resolved speaking source, source timestamp, appointment offset, extraction version, target bucket, MIME type, dimensions, checksum, object reference, quality results, visual-subject status (`unverified` or `doctor_confirmed`), confirming doctor and confirmation time when applicable, lifecycle status, and creation time.
- Screenshot status is `active`, `deleted_undoable`, or `deleted`. A final deletion retains only a content-free tombstone with image id, prior checksum, appointment and encounter references, deleting doctor, and deletion time.
- The observation draft stores active, doctor-confirmed source image ids, the real sample count, allowed visual domains, limitation text, prompt version, and `requires_doctor_review: true`.

**Artifact ingest:**

- §2.6 turns native Meet recording on and owns the raw recording and transcription artifacts. Visit Observation begins only after every known recording session reaches `FILE_GENERATED`.
- Extend `AppointmentMeetTranscriptSweepJob` to enumerate every conference recording resource, fetch each organizer-owned `driveDestination.file` through authorized Drive access, and enqueue one idempotent extraction run over the ordered recording set.
- Resolve exactly one non-doctor, non-phone patient speaking source from Meet participant metadata and transcript attribution. Combine rejoins from that resolved source. Display name alone is never sufficient; ambiguous speaker attribution produces zero screenshots. This resolution identifies candidate times only and never establishes who or what is visible in a composite recording frame.
- An encrypted temporary-media worker uses `ffmpeg` to decode source frames. Successful and expired-job cleanup removes temporary recordings and frames after durable EHR writes finish.

**Deterministic selection:**

1. Build chronological patient-attributed transcript intervals inside the authorized appointment and generated-recording windows.
2. Merge adjacent intervals from the same resolved patient when the gap is at most three seconds.
3. Trim 1.5 seconds from each merged interval's start and 0.5 seconds from its end, then discard empty windows.
4. Concatenate the remaining windows into one cumulative patient-speaking timeline.
5. Place seven targets at `total_eligible_speech * (2i + 1) / 14` for `i = 0..6`, then map each target back to its recording resource and source timestamp.
6. Require at least 20 seconds of wall-clock separation when visit duration permits it.
7. For a failed target, probe `0, +1, -1, +2, -2, +4, -4` seconds while staying inside the same patient-attributed interval.
8. Never duplicate a frame or substitute doctor or unknown-participant time to reach seven.

**Quality and drafting:**

- A candidate frame must decode, meet configured resolution, remain inside its attributed interval, avoid black, blank, frozen, and transition states, and have a checksum not already attached to the encounter. It is attached automatically with visual-subject status `unverified`.
- Launch uses no face detector, face embedding, identity match, facial recognition, emotion, intoxication, or demographic classifier.
- The assigned doctor can confirm one image or the whole reviewed set as showing the patient. Presented content, layout-only frames, and any image the doctor cannot confirm remain excluded from model inputs; confirmation records the doctor and time and is revocable by deletion.
- `Ehr::Agents::NoteDraft` receives only active, doctor-confirmed images with image-level citations and may draft only visible, time-bound observations allowed by §2.7. The prompt records the real confirmed-image count and sampled-image limitations.
- Image-derived draft text remains unsigned and doctor-editable. The existing Clinical Note doctor edit, signature, immutability, and amendment gates in §3.1 remain authoritative.

**Deletion and idempotency:**

- Delete-one and delete-all hide images from the active chart, exports, and model inputs in the deletion transaction, show a short Undo action, then purge the exact stored object generation and visual-analysis payload.
- An applicable legal hold may defer byte purge through restricted retention but never restores the image to ordinary chart display, exports, or model inputs.
- Before signing, image deletion invalidates dependent draft text. Regenerate only untouched generated text; never overwrite clinician-edited text. After signing, preserve signed note bytes and offer the existing amendment path.
- Idempotency keys cover each recording resource, extraction run, screenshot timestamp plus checksum, active-image draft set, and deletion. A deletion tombstone prevents any sweep, retry, or algorithm upgrade from recreating a deleted image.
- Missing media, incomplete artifacts, ambiguous speaker attribution, an unconfirmed visual subject, fewer than seven usable frames, analysis failure, and deletion produce explicit reason codes and never block the appointment, note editing, note signing, or prescribing.

**Config and external interfaces:**

- Use generally available Google Meet REST recording resources and Google Drive recording download. No Developer Preview API, bot participant, automated browser, or additional Workspace-account participant is part of this path.
- Rollout requires a recording-capable Workspace edition and admin policy plus delegated access for `meetings.space.settings`, Meet artifact reads, and organizer-owned Meet recording content in Drive.

---

## Section 3: EHR & Prescribing

### 3.1 Clinical Note

**Data:**

- Tables: `ehr_encounters`, `ehr_clinical_notes`, `ehr_note_addenda`, `ehr_chart_access_logs`, `ehr_encounter_diagnoses`, `ehr_patient_profiles`, `ehr_problems`, `ehr_allergies`, `ehr_medications`, `ehr_vitals`, `ehr_amendment_requests`, `ehr_legal_holds`, `ehr_disclosure_logs`.
- `ehr_encounters`: `status` (default draft), `modality` (default video), `encountered_at`, `patient_state_at_visit`, `identity_verified`, `signed_at`, `signed_by_id`, `signature_hmac`, `signature_key_version`, `canonical_payload`, `signed_payload_bytes`, `signature_payload_version`, `voided_at`, `void_reason`, `appointment_id`, `import_fingerprint`.
- `ehr_clinical_notes`: `note_type` (default `hpi_mdm`), `subjective`, `objective`, `assessment`, `plan`, `hpi`, `ros`, `exam`, `medical_decision_making`, `risks_benefits`, `instructions`, `follow_up`, `ai_draft_source_ref`, `ai_draft_edited`, `doctor_addendum`.
- `ehr_chart_access_logs`: `actor_type/actor_id`, `target_type/target_id`, `action`, `request_id`, `reason`, `occurred_at` (append-only, readonly once persisted).
- `ehr_note_addenda`: `clinical_note_id`, `author_id`, `body`.

**Code paths:**

- Models: `app/models/ehr/encounter.rb`, `clinical_note.rb`, `note_addendum.rb`, `chart_access_log.rb`, `patient_profile.rb`.
- Workflows (30 under `app/services/ehr/workflows/`): `sign_encounter.rb`, `document_clinical_note.rb`, `amend_encounter.rb`, `void_encounter.rb`, `place_legal_hold.rb`, `release_legal_hold.rb`, `record_disclosure.rb`, `submit_amendment_request.rb`, `review_amendment_request.rb`, and others.
- `app/services/ehr/access/chart_access_policy.rb`; `app/services/ehr/integration.rb` with 26 facade submodules under `app/services/ehr/integration/` (`CORE_CONSTANTS` allowlist is the only sanctioned core-EHR crossing); `app/services/ehr/integration/audit.rb` (`chart_access!` writes `Ehr::ChartAccessLog`); `app/services/ehr/encounter_signature_verifier.rb`.
- Controllers: `app/controllers/ehr/charts_controller.rb`, `encounters_controller.rb`, `chart_prints_controller.rb`, `base_controller.rb` (`before_action :authorize_chart_access_hook`).
- Jobs: `app/jobs/ehr/break_glass_alert_job.rb`.
- Boundary tooling: `bin/architecture-check` (Rule 1 one-way boundary, grandfathered allowlist baseline 2026-07-04), `test/architecture/ehr_boundary_test.rb`.

**Automation:**

- Deterministic workflow services plus 5 real-LLM EHR agents under `app/services/ehr/agents/`: `NoteDraft`, `RxSuggestions`, `DictationCommand`, `IcdSuggestions`, `NoteDrift` (all `real_llm!` BaseAgent subclasses).
- Every chart read/write routes through `authorize_chart_access_hook` -> `Ehr::Integration::Audit.chart_access!`; break-glass POST enqueues `Ehr::BreakGlassAlertJob`; EHR schedule roster access writes a chart-access row per patient shown.

**Config:**

- EHR routes behind `ehr_enabled` constraint (`EhrConfig.enabled?`, dark-launchable per env).
- E-signing is in-house HMAC attestation: key from `EHR_ENCOUNTER_SIGNING_KEY`, versions via `EHR_ENCOUNTER_SIGNING_KEY_VERSIONS`.

**Interfaces:**

- Routes under `scope "/ehr"`: `GET charts/:patient_id` (chart), `GET charts/:patient_id/print`, `POST charts/:patient_id/break_glass`, `POST charts/:patient_id/encounters`, `POST encounters/:id/sign`, `POST encounters/:id/addendum`.
- Note drafting is fed by the Google Meet transcript pipeline (see Doctor Visit).
- Visit Observation (§2.7) is an optional future note-draft input. Only its active, doctor-confirmed image citations and visual-observation text may pass through the same doctor edit and signature path; image deletion never mutates signed note bytes.

**Notes:**

- `SECTION_CAP = 25` rows per chart section (`charts_controller.rb`).
- New clinical notes must be `note_type` `hpi_mdm` (`ClinicalNote#new_notes_are_compact`); soap accepted only via `legacy_import`; `NoteDraft` defaults hpi_mdm.
- `SignEncounter#note_complete?` accepts any one populated `NOTE_CONTENT_FIELDS` field.
- Only the appointment provider may sign: `SignEncounter` guards `actor.doctor?`, `actor.id == record.provider_id`, `actor.reauth_fresh?` (raises `Workflows::InvalidTransition`); `IssuePrescription` requires `actor.doctor?` and the appointment signer.
- Signature payload versions v1-v3 (`CURRENT_PAYLOAD_VERSION` v3, supports multi-Rx `payload[:prescriptions]`); `imported` key version cannot issue prescriptions (`Ehr::Integration::Prescribing`).
- Advisory signals (telehealth facts, primary diagnosis, unsupported AI signal) log warnings and never block signing; break-glass and chart-access audit fail closed (raise `NotAuthorized` if the audit row cannot be written).
- `ehr_prescription_correction_requests` enforces DB CHECK `ehr_active_record_encrypted_envelope`, rejecting unencrypted PHI at Postgres.

### 3.2 Prescription Generation

Complete signed-appointment-record to prescription pipeline; the record type is `Ehr::Encounter`.

**Data:**
- `prescriptions`: `medication_name`, `strength`, `directions`, `form`, `route`, `frequency`, `quantity`, `clinical_rationale`, `status` (enum draft/valid_rx/dispensed/cancelled), `encounter_id`, `signed_by_id`, `idempotency_key` (unique), `issued_at`, `expires_at`, `state_at_issue` (encrypted), `signature_ref`, `patient_snapshot`, `prescriber_snapshot`, `provider_credential_snapshot`, `pharmacy_snapshot`, `structured_sig`, `formula_version_id`, `governed_artifact_version_ids`.
- `ehr_prescription_confirmations`: `encounter_id`, `confirmed_by_id`, `revision`, `fingerprint` (over the prescription fields; any field edit invalidates), `confirmed_at`, `invalidated_at`, `governed_artifact_version_ids`, `row_key`.
- `ehr_prescription_correction_requests`: `original_prescription_id`, `replacement_prescription_id`, `status` (default `pending_doctor_review`), `preparation_digest`, `proposed_medication`/`proposed_strength`/`proposed_sig` (encrypted-envelope CHECK), `reason`, `source_evidence`.
- `ehr_dea_schedules`, `ehr_provider_credentials`, `ehr_provider_state_licenses`, `fda_reference_strengths`, `pharmacy_orders`, `external_fulfillments`.

**Code paths:**
- `app/services/ehr/integration/prescribing.rb`: issue pipeline with signature, serviceability, and credential/license preconditions; `GATE_NAME` `ehr_prescription_requires_signed_encounter`; pg advisory-lock fulfillment handoff.
- `Ehr::Workflows::ConfirmPrescription` (`app/services/ehr/workflows/confirm_prescription.rb`): writes the confirmation row and fingerprint.
- `Ehr::Workflows::SignEncounter` (`app/services/ehr/workflows/sign_encounter.rb`): note completeness, provider == signer, re-auth; writes the signature HMAC and signed payload bytes.
- `Ehr::Workflows::IssuePrescription` (`app/services/ehr/workflows/issue_prescription.rb`): re-verifies HMAC, serviceable state, active `medical_license` credential and patient-state license, dose guardrail; creates a `Prescription` (status `valid_rx`) with idempotency key and snapshots.
- `app/services/ehr/encounter_signature_verifier.rb`.
- `app/services/ehr/integration/dose_guardrail.rb`: `GATE_NAME` `compounded_dose_not_near_fda_strength`; consults `FdaReferenceStrength.signed_off` only; fails closed on unparseable referenced strengths.
- `app/services/pharmacy/fulfillment_routing.rb`: internal lane opens a `PharmacyOrder` (status `review_pending`) and runs three synchronous intake agents (`Agents::FormulationFeasibility`, `Agents::SafetyScreen`, `Agents::PharmacistReview`); external lane opens an `ExternalFulfillment` (status `pending_handoff`) until a human faxes and records `mark_faxed` (`Workflows::MarkFaxedToExternalPharmacy`).
- Read-only DEA catalog: `app/services/ehr/controlled_substances/schedule_resolver.rb`, `Ehr::DeaSchedule`, `Ehr::Workflows::UpsertDeaSchedule`, surfaced in `app/services/ehr/integration/prescription_card.rb`.
- `app/services/ehr/agents/rx_suggestions.rb`: real-LLM transcript-derived Rx draft, sanitized via `Prescribing.sanitize_rx_suggestion`.
- Post-issue pricing via `Workflows::PricePrescription.auto_price` (`app/lib/rx_pricing.rb`), then funnel analytics.

**Interfaces:**
- Routes: `post encounters/:id/rx_suggestion`; `post encounters/:id/confirm_prescription`; `post encounters/:id/issue_prescription`; `get prescriptions/:id/print`; `post prescriptions/:id/mark_faxed`.
- External pharmacy fulfillment is human fax (`mark_faxed`); no e-prescribing network.
- Medication charges use a second Stripe account family (see Prescription Pay).

**Config:**
- `REQUIRED_RX_FIELDS` = `medication_name`, `strength`, `directions`, `form`, `route`, `frequency`, `quantity`, `clinical_rationale` (prescribing.rb:14).
- `RX_SUGGESTION_HISTORY_LIMIT` = 5 prior prescriptions fed to the Rx-suggestion agent (prescribing.rb:24).
- Idempotency key format `ehr:encounter:<id>:prescription:<sha256-16>:v1` (prescribing.rb:428).
- `dose_guardrail` `TEN_PERCENT = Rational(1, 10)`, inclusive band.
- DEA seed covers three substances (tirzepatide and semaglutide `not_controlled`, testosterone Schedule III) via `db/seeds/dea_schedule_classifications.rb`.

**Notes:**
- `prescribing.rb` enforces free-text prescribing: it forbids re-adding a formula/DEA/mapping requirement to confirm/sign/issue (pinned by `prescription_issue_test`).

### 3.3 Prescription Pay

One-time charge pipeline; the patient pays per prescription through hosted Stripe Checkout.

**Data:**
- `prescription_payments`: `prescription_id`, `patient_id`, `priced_by_id`, `amount_cents` (int), `currency` (default `'usd'`), `status` (default `'unpaid'`; enum unpaid/paid), `stripe_checkout_session_id`, `stripe_payment_intent_id`, `stripe_customer_id`, `priced_at`, `paid_at`, `ready_to_pay_notified_at`, `pay_link_nonce`, `billing_account` (default `'tx'`).

**Code paths:**
- `app/lib/rx_pricing.rb`: flat launch price in `PRICES` for the six launch drugs (Tirzepatide, Semaglutide, Metformin, Ezetimibe, Methylcobalamin, Low-dose Naltrexone).
- `app/lib/rx_billing.rb`: second Stripe account family; `fallback_enabled?`; `DEFAULT_ACCOUNT = 'tx'`; `live?` gate.
- `app/models/prescription_payment.rb`: `payable?`, Turbo broadcast on create.
- `app/lib/rx_payment_link.rb`: signed token resolver over `pay_link_nonce`.
- `Workflows::PricePrescription` (`app/services/workflows/price_prescription.rb`; `.auto_price` called from `Workflows::ApproveByDoctor` and `Ehr::Integration::Prescribing`); `Workflows::MarkPrescriptionPaid`.
- `app/services/rx_payments/checkout_session.rb` (Stripe Checkout `mode: 'payment'`, one-time only), `finalize_checkout.rb` (server-side session verification, return-URL and webhook legs), `reconcile_checkout_session.rb`, `payment_link_recovery.rb`, `invalidate_for_correction.rb`.
- Controllers: `app/controllers/rx/payment_links_controller.rb` (no-login tokenized pay page), `app/controllers/patient/prescription_payments_controller.rb`, `app/controllers/admin/prescription_prices_controller.rb` (staff pricing UI).
- `app/controllers/webhooks/rx_stripe_controller.rb`: handles `checkout.session.completed`, `async_payment_succeeded`, `expired`.

**Schedules:**
- `RxCheckoutReconcileJob` every 15 minutes (`WATERMARK = 7.days`, `BATCH_SIZE = 200`) (config/recurring.yml).
- `RxPaymentLinkRecoveryDispatchJob` every minute (config/recurring.yml).
- `RxReadyToPayNotifyJob` enqueued on a payable payment: Linq SMS tap-to-pay link, `retry_on SendFailed wait: 10.minutes attempts: 10`, PHI-free body.

**Interfaces:**
- Routes: `get rx/pay/:token`; `post rx/pay/:token/checkout`; `get rx/pay/:token/complete`; patient `prescription_payments` member `pay` and collection `complete`; `post rx/stripe/webhook`.
- Stripe hosted Checkout on the second account family (tx/ca); Linq SMS for the tap-to-pay link; Turbo Streams to the patient dashboard.

**Config:**
- `DEFAULT_AMOUNT_CENTS = Integer(ENV['RX_FLAT_PRICE_CENTS'].presence || 19_500)` (rx_pricing.rb:15).
- `RX_PAYMENT_FALLBACK`: any value other than `'false'` shows "pharmacy will contact you" instead of Pay (rx_billing.rb:46).
- `RX_READY_TO_PAY_AUTOREPLY_MODE` must equal `'send'` or the job logs instead of sending (rx_ready_to_pay_notify_job.rb:27).
- `PricePrescription::STAFF_ROLES` = doctor, pharmacist, lab_ops, admin.

**Notes:**
- Issue and approval auto-price via `PricePrescription.auto_price`; checkout is Stripe `mode: 'payment'` only, with no subscription mode or recurring price object; the saved membership card cannot cross Stripe accounts, so the patient completes checkout separately.

### 3.4 Prescription Limits (3-month max before data-driven checkup)

**Data:**
- `prescriptions.expires_at` column (readers: `app/services/ops/integration/payment_diagnostics.rb:245`, `app/services/ehr/workflows/prepare_prescription_correction.rb:71`).
- `refill_tasks`: `prescription_id`, `due_date`, `status` (default scheduled; enum scheduled/confirmed/held/completed/cancelled), `hold_reason`.
- `outcome_check_ins`.

**Code paths:**
- `app/services/workflows/confirm_delivery.rb`: creates the first `RefillTask` and first `OutcomeCheckIn` at delivery confirmation.
- `app/services/workflows/confirm_refill.rb` (`Workflows::ConfirmRefill`): checks patient ownership and therapy holds.
- `app/models/refill_task.rb`, `app/models/outcome_check_in.rb`; patient `resources :refill_tasks`.

**Schedules:**
- `OutcomeCheckInJob` daily at `0 9 * * * America/Chicago` (config/recurring.yml).

**Config:**
- First refill task due `Date.current + 28` days (confirm_delivery.rb:22).
- First outcome check-in `Date.current + 2` days, weekly cadence (confirm_delivery.rb:32).

### 3.5 Previous Charts / Records Ingestion

Multi-stage pipeline: patient upload or admin bulk import, sealed cloud storage, LLM field extraction, PDF digestion, named-reviewer promotion into the chart.

**Data:**
- `uploaded_documents`: `patient_id`, `status` (default received), `doc_type`, `sealed_object_key`, `sealed_generation`, `checksum_sha256`, `identity_matched`, `identity_score`, `threshold_used`, `review_state` (default none), `review_flags`, `reviewed_by_id`, `accepted_at`, `metadata`.
- `extracted_clinical_fields`: `uploaded_document_id`, `field_kind`, `name`, `value`, `unit`, `confidence` (numeric(4,3)), `identity_value`, `source_location`.
- `records_pdf_digestion_runs`: `uploaded_document_id`, `status`, `parser_name`/`parser_version`/`parser_options`, `fingerprint`, `page_count`/`element_count`/`chunk_count`/`table_count`, `failure_code`, `output_sha256`. Plus `records_pdf_pages`, `records_pdf_elements`, `records_pdf_chunks`, `source_citations`.

**Code paths:**
- `app/services/records/gemini_patient_record_extractor.rb`: real Gemini LLM over `Net::HTTP` with field-kind normalization.
- `app/services/records/patient_document_extractor.rb`.
- `app/services/records/pdf_digestion/{digest_document, eligibility, parser, open_data_loader_parser, run_state}.rb`.
- `app/services/records/promote_extracted_fields.rb`: writes `ehr_medical_histories` with `code_system` `ronanrx_records_upload`, `source: "imported"`, `recorded_by` actor, and a `SourceCitation`.
- `app/services/records/{core_chart_importer, core_chart_zip_importer, core_chart_bootstrapper, extract_importer, source_citation_resolver, source_page_renderer, source_pdf_store, gcs_import_bundle_downloader}.rb`.
- `app/services/agents/health_record_import.rb` (stub).
- `app/controllers/doctor/uploaded_documents_controller.rb#promote`: `require_role(:doctor, :admin)` with care-team chart-write authorization.

**Schedules:**
- Jobs: `app/jobs/records/extract_uploaded_document_job.rb` (chains to digestion via `PdfDigestion::Eligibility`), `app/jobs/records/digest_pdf_document_job.rb`. Patient upload enqueues extraction; extraction completion enqueues digestion when eligible.

**Interfaces:**
- Routes: patient `uploaded_documents` new/create/index/show and member `:source`; doctor `cases/:id/uploaded_documents` index and `post :promote`; admin `core_chart_imports` new/create; `ehr charts/:patient_id/source_documents/:id`.
- GCS `PhiDocumentStore.records_import` for sealed bytes.
- Gemini API (`GEMINI_API_KEY`, `config/initializers/gemini.rb`; `GEMINI_MODEL` default `gemini-2.5-flash`).

**Config:**
- Gemini batches: `OCR_BATCH_SIZE = 5`, `TEXT_BATCH_PAGE_LIMIT = 12`, `MAX_TEXT_CHARS_PER_BATCH = 18_000` (gemini_patient_record_extractor.rb:48).
- `PROMOTABLE_FIELD_KINDS` = diagnosis, procedure, adverse_event (promote_extracted_fields.rb:4).

**Notes:**
- Admin core-chart import is a manual JSON/ZIP run, idempotent by extract SHA.

### 3.6 EHR Schema Cleanup

TBD. No mechanism exists yet. The target lives in this card's business.md section.

---

## Section 4: Labs

### 4.1 Order Labs

**Data:**

- `lab_orders`: `ordering_doctor_id`, `patient_id`, `panels` jsonb (default `[]`), `rationale`, `status` varchar (default `'draft'`; model enum draft, ordered, result_received, reviewed, abnormal, deferred).
- `lab_results`: `lab_order_id`, `collected_at`, `status` (default `'received'`; model enum received, parsed, abnormal, normal), `values` jsonb (default `[]`).

**Code paths:**

- `app/models/lab_order.rb`, `app/models/lab_result.rb`.
- `app/controllers/lab/orders_controller.rb`: index and show, `before_action require_role(:lab_ops, :admin)`, `PAGE_SIZE = 25`.
- `app/controllers/lab/queue_controller.rb`: index surfaces `lab_orders` (status ordered, result_received) alongside compounding tasks and shipments for lab_ops/admin.
- `app/services/agents/lab_recommendation.rb`: real-LLM agent (`real_claude!`, `clinical!`) that writes a `lab_necessity_memo` artifact and emits a `lab_memo_generated` event; prompt hardcodes `human_approval_required` always true.
- `app/services/workflows/submit_for_doctor_review.rb`: calls `Agents::LabRecommendation`.
- `app/views/doctor/cases/show.html.erb`: renders the memo from `@lab_memo_artifact`.

**Interfaces:**

- Routes: `namespace :lab` `resources :orders, only: %i[index show]`.

### 4.2 Quest Lab API

**Data:**

- `biomarker_crosswalks`: `quest_biomarker_code`, `loinc_code`, `canonical_key`, `display_name`, `default_unit`, `method`, `metadata` jsonb, `review_status` (default `'approved'`).
- `biomarker_review_items`: `quest_biomarker_code`, `observed_name`, `observed_unit`, `observed_method`, `metadata` jsonb, `status` (default `'open'`).

**Code paths:**

- `app/models/biomarker_crosswalk.rb`: `lookup!` by `quest_biomarker_code`, `approved` scope; enqueues a `BiomarkerReviewItem` on a miss.
- `app/models/biomarker_review_item.rb`: `enqueue!` (idempotent open queue) for unmapped codes.
- Consumers: `app/services/lab_import/function_health_importer.rb` (reads `questBiomarkerCode`/`quest_biomarker_code`/`quest_code`), `function_health_extractor.rb`, `function_health_data_file_parser.rb`.
- Seed: `db/seeds/biomarker_crosswalks.rb`.

**Config:**

- Seed uses symbolic `QUEST_*` keys (glucose, hemoglobin A1c, insulin, C-peptide, lipids, ApoB, Lp(a), and more) mapped to LOINC and canonical keys; keys can be replaced with exact Quest codes without importer changes.

**Notes:**

- Quest biomarker codes enter only via Function Health import, which runs on Quest; the crosswalk and review queue are keyed on `quest_biomarker_code`.

### 4.3 Mobile Phlebotomy

TBD. No mechanism exists yet. The target lives in this card's business.md section.

### 4.4 Function Health Lab Import

**Data:**

- `function_health_account_bindings`: `patient_id`, `consent_id`, `external_member_id`, `bound_at`.
- `function_health_resource_sync_states`: `function_health_account_binding_id`, `import_connection_attempt_id`, `resource_name` (CHECK in profile/labs/clinician_notes/personalized_recommendations/biological_age/bmi), `fetched_at`.
- `function_health_source_records`: `resource_type` (CHECK in profile_membership/clinician_note/personalized_recommendation/biological_age/bmi), `source_key_digest`, `content_digest`, `payload_json`, `status` (active/source_deleted), `deleted_at`.
- `imported_lab_panels`: `patient_id`, `consent_id`, `counts` jsonb, `external_ref` jsonb, `import_fingerprint`, `provenance_label` (default `'patient-imported (Function Health), as-reported, not RonanRX-verified'`), `source`, `uploaded_document_id`.
- `imported_lab_results`: `external_draw_id`, `external_ref` jsonb, `imported_lab_panel_id`, `status` (default `'received'`), `values` jsonb.
- `import_connection_attempts`: `source`, `status`, `lease_token`/`lease_expires_at`, `function_health_credential` (encrypted), `credential_expires_at`, `request_ip_hash`.

**Code paths:**

- Workflows (`app/services/workflows/`): `ClaimFunctionHealthHandoff`, `EnqueueFunctionHealthImport`, `StartFunctionHealthImport`, `IngestFunctionHealthBundle`, `FinalizeFunctionHealthImport`, `FailFunctionHealthImport`, `ReapStaleFunctionHealthImport`, `ImportFunctionHealthFile`, `ImportFunctionHealthPdf`, `ImportFunctionHealthDataFile`, `ImportLabResults`, `UpsertFunctionHealthResource`, `VerifyFunctionHealthAccountBinding`, `RenewFunctionHealthHandoffLease`, `MintImportHandoffToken`.
- Services: `app/services/lab_import/{function_health_runner, function_health_importer, function_health_extractor, function_health_data_file_parser, function_health_bundle_limiter, source_pdf_store, source_data_file_store}.rb`; `app/services/function_health/{bundle_contract, bundle_limiter, extension_origins, patient_data_export, profile_parser}.rb`.
- Jobs: `FunctionHealthImportJob`, `FunctionHealthImportReapJob` (queue `default`).
- Runner and extension: `function_health_runner/server.js` (Node wrapping a Go CLI), `Dockerfile.function-health-runner`, `function_health_extension/` (Chrome: `background.js`, `content.js`, `sync.js`), `docs/function-health-runner.md`.
- Triggers: patient file upload (`Patient::ImportedLabsController#create` calls `Workflows::ImportFunctionHealthFile`); browser-extension handoff (short-lived Firebase id token via `MintImportHandoffToken`, Claim, Enqueue into the async job).

**Schedules:**

- `function_health_import_reap` (`FunctionHealthImportReapJob`) every minute in `config/recurring.yml` (production and development blocks); clears stale attempts and encrypted credentials.

**Interfaces:**

- Routes: `POST connect/function_health/live`; patient `resource :function_health_connection` with `POST :handoff`; patient `resources :imported_labs` (index/new/create); `api/v1` `POST function_health_imports/handoff` and `GET function_health_imports/:id` (with OPTIONS).
- Function Health: no official API; patient-mediated one-time export via the bundled Go CLI and Chrome extension.
- GCS `PhiDocumentStore.lab_import` seals uploaded source files.
- Isolated Cloud Run runner over `FUNCTION_HEALTH_RUNNER_URL` (HTTPS required in production), auth via `FUNCTION_HEALTH_RUNNER_AUTH_TOKEN`.

**Config:**

- Consents: `function_health_account_import` (live handoff) or `lab_import` (file upload).
- Credential TTL 5 minutes (`EnqueueFunctionHealthImport::CREDENTIAL_TTL`); handoff lease 5 minutes (`ClaimFunctionHealthHandoff::LEASE_DURATION`).
- Bundle limiter defaults (`LabImport::FunctionHealthBundleLimiter`): 10 MiB input, 2 MiB retained, 50 draws, 5,000 values, 64 KiB/value, 5.0 s; hard ceilings 20 MiB, 5 MiB, 200 draws, 20,000 values, 256 KiB/value, 15.0 s; report id 512 bytes.
- Runner output budgets (`function_health_runner/server.js`): 10 MiB total canonical output; profile 256 KiB; clinician notes and recommendations 2 MiB each; biological age and BMI 128 KiB each.
- Cloud Run runner `function-health-import-runner`: 1 CPU, 1 GiB, concurrency 1, 120 s timeout, 0 min and 3 max instances; dedicated `ronanrx-function-runner` identity scoped to `FUNCTION_HEALTH_RUNNER_AUTH_TOKEN` only.

**Notes:**

- Deterministic throughout, no LLM.
- Authorization guard: `Workflows::UpsertFunctionHealthResource#lock_and_validate_authorization!` requires an active `function_health_account_import` consent, an unconsumed and unexpired handoff token, and a valid lease, else raises `InvalidTransition`; `payload_json` is encrypted at rest.
- Unknown or absent Quest biomarker codes are retained as needs_review rows and enqueue a `BiomarkerReviewItem` via `BiomarkerCrosswalk.lookup!`.

### 4.5 Lab Follow-up

**Data:**

- `ehr_lab_result_reviews`: `lab_result_id`, `imported_lab_result_id` (CHECK exactly one set; partial unique index per result), `review_status` (default `'pending_review'`), `flag` [encrypted, deterministic], `acknowledged_at`, `acknowledged_by_id`, `provider_note` [encrypted], `patient_notified_at`, `critical_override_at`, `critical_override_by_id`, `critical_override_reason` [encrypted], `review_metadata` [encrypted].

**Code paths:**

- `app/services/ehr/integration/labs.rb`: merges ordered and imported results; derives `flag` `'critical'` when any value flag includes `'critical'`; `upsert_review!`, `acknowledge!`, `mark_patient_notified!`, `critical_override!` with guards.
- `Ehr::Workflows::{ReviewLabFact, AcknowledgeLabReview, MarkLabReviewPatientNotified, OverrideCriticalLabReview}`: each authorizes via `Ehr::Access::ChartAccessPolicy` (action `write`, break-glass aware).
- `app/controllers/ehr/lab_reviews_controller.rb` and views; queue rank critical=0, abnormal=1, needs_review=2, acknowledged=3; `PAGE_SIZE = 25`.

**Interfaces:**

- Routes: `GET/POST ehr/lab_reviews`; `POST charts/:patient_id/lab_reviews`; `POST lab_reviews/:id/{acknowledge, patient_notified, critical_override}`.

**Notes:**

- Deterministic workflows only; the clinician acts, the system enforces sequence. Sign-off actor is whoever passes `ChartAccessPolicy` write, recorded in `acknowledged_by_id`/`critical_override_by_id`.
- Critical results require acknowledgement or override before patient notification can be recorded (`guard_patient_notification_allowed!`).
- Critical override requires a reason and applies only to critical-flagged results (`guard_critical_override_allowed!`).
- Patient notification requires a channel (raises `Patient notification channel required` when blank).
- Verdict statuses: pending_review, reviewed, deferred; transition statuses acknowledged, critical_override, patient_notified.
- Optimistic-concurrency guard raises `Lab review changed in another session. Reload...` on stale writes.
- `flag` is derived from imported value flags, not computed by an independent reference-range engine.

---

## Section 5: Pharmacy, Protocols & Fulfillment

### 5.1 Protocols

**Data:**

- `formula_masters` (db/structure.sql): `name`, `slug`, `status`, `therapeutic_class`, `current_version_id`.
- `formula_versions`: `approval_status` (draft/approved/superseded/retracted), `approved_by_id`, `approved_at`, `supersedes_id`, `version_number`, `active_ingredients` jsonb, `excipients` jsonb, `canonical_concentration numeric(12,6)` (CHECK positive or null), `concentration_unit`, `allowed_dose_steps` jsonb, `stability_notes` text (no BUD column). Partial unique index `idx_formula_versions_one_approved_per_master` WHERE approval_status='approved' enforces one approved version per master.
- `formulations`: `pharmacy_order_id`, `formula_version_id`, `feasibility` (default `unknown`), `active_ingredients` jsonb, `excipients` jsonb.
- `programs`: `clinic_id`, `slug`, `status` (default `active`), `template` jsonb.
- `program_formula_mappings`: `program_id`, `visit_type`, `formula_master_id`, `formula_version_id`, `titration_rules` jsonb.
- `fda_reference_strengths`: 9 seeded rows, all semaglutide, `clinical_signoff_status` pending_signoff (db/seed_data/pharmacy/v1/fda_reference_strengths.yml).

**Code paths:**

- Models `app/models/`: `formula_master.rb` (approved_version, drift_count), `formula_version.rb`, `formulation.rb`, `program.rb` (state-aware formula lookup), `program_formula_mapping.rb` (for_program, available_in_state, validate_formula_version_for_state!).
- `Workflows::ApproveFormulaVersion`: hard-fail gate `formula_version_requires_pharmacist_or_doctor_approval`; `formula_master.lock!` inside the transaction for partial-unique-index race safety.
- `Workflows::RetractFormulaVersion`.
- `Agents::CompoundingWorkflow`: deterministic pick_list builder, non-LLM.
- `app/services/formulation_seeder.rb`: `run!` seeds 7 formula profiles (tirzepatide 5mg/mL, semaglutide 2.5mg/mL, progesterone 200mg, GHK-Cu 50mg/2mL, BPC-157 5mg/2mL, testosterone cypionate 200mg/mL, epitalon 10mg/2mL) with masters, approved versions, and inventory lots.
- `Onboarding::Serviceability`: per-state formulary.
- `Ehr::Integration::Prescribing`: consumes ProgramFormulaMapping for the prescription menu; free-text prescribing carries an optional `formula_version_id` and does not require a mapping (prescribing.rb:267-270).
- `Agents::ProtocolRecommendation`: doctor-facing suggestion, `real_claude!` LLM, human_approval_required always true.

**Interfaces:**

- Admin `resources :formulas` index/show (config/routes.rb:421); admin `formula_versions` member `post :approve`, `post :retract` (:430-433).

**Config:**

- `Serviceability::STATE_CONFIG`: TX open formulary (allowed_formula_master_slugs nil, default `tirzepatide-5mg-ml`); CA allowed = `tirzepatide-5mg-ml`, `semaglutide-2-5mg-ml`, default `tirzepatide-5mg-ml`.
- db/seeds.rb: clinic voss-precision-health, 3 programs (weight, energy, longevity).
- db/seeds/program_formula_mappings.rb: 2 mappings (tirzepatide, semaglutide to weight program), `titration_rules` default empty `{}`.
- Seed inventory lots: `18.months.from_now` expiry, 1000 units on hand.

**Notes:**

- Pipeline agents (CompoundingWorkflow, FormulationFeasibility, PharmacistReview, QualityGate) are non-LLM; only ProtocolRecommendation calls real_claude!.

### 5.2 Pharmacy Queue

**Data:**

- `compounding_tasks`: `beyond_use_date` date, `checklist` jsonb, `pick_list` jsonb (substance_name, planned_quantity, planned_unit, role, pick_list_index, fallback), `status` (default `queued`), `technician_id`, `pharmacy_order_id`.
- `lot_consumptions`: `compounding_task_id`, `inventory_lot_id`, `pick_list_index`, `quantity_consumed numeric(12,4)` (CHECK positive), `unit`, `consumed_by_id`, `consumed_at`; unique index `idx_lot_consumptions_one_per_pick_list_row` on (compounding_task_id, pick_list_index).
- `inventory_lots`: `substance_name`, `lot_number`, `coa_reference`, `supplier_name`, `expiration_date`, `quantity_on_hand numeric(12,4)` (CHECK nonnegative), `status` (CHECK available/depleted/quarantined/expired).
- `quality_releases`: `checklist_results` jsonb (label_correct, lot_traceable, documentation_complete, bud_set), `released_by_id`, `released_at`, `signature_hash`, `status` (default `pending`).
- `label_proofs`: `compounding_task_id`, `primary_label_text`, `auxiliary_labels` jsonb, `insert_text`, `status` (default `draft`).
- `pharmacist_reviews`: `pharmacy_order_id`, `pharmacist_id`, `status` (default `pending`), `signed_at`, `signature_hash`, `counseling_notes`, `hold_reason`.
- `pharmacy_orders`: `prescription_id`, `status` (default `intake_pending`), `external_id`.
- `external_fulfillments`: `prescription_id`, `pharmacy_name` NOT NULL, `pharmacy_fax`, `status` (default `pending_handoff`), `faxed_by_id`, `faxed_at`, `metadata` jsonb.
- `shipments`: see Delivery Bot.

**Code paths:**

- `Workflows::StartCompounding`: gate `compounding_requires_patient_specific_prescription`; requires an approved PharmacistReview.
- `Workflows::CompleteCompounding`: lot-coverage guard; creates LabelProof inline.
- `Workflows::ReleaseQa`: gate `pharmacist_release_required`; actor guard permits pharmacist, lab_ops, or admin (release_qa.rb:10); computes lot_traceable; TherapyHolds clearance.
- `Workflows::ConsumeLot`: `inventory_lot.lock!`, substance-match, expiry and quantity guards; wired to pharmacist UI via `app/controllers/pharmacist/compounding_tasks_controller.rb:22`.
- `Workflows::ConfirmShippingReadiness`: creates Shipment and calls `Agents::ShippingCoordination`.
- `Workflows::DispatchShipment`: gate `shipment_requires_pharmacist_release`; TherapyHolds clearance; synthesizes `tracking_number` `VAL-<hex>`.
- `Workflows::ConfirmDelivery`: sets delivered; creates RefillTask due +28 days and OutcomeCheckIn scheduled +2 days.
- `Workflows::OpenPharmacyOrder`: gate `doctor_approval_required_before_pharmacy`; TherapyHolds clearance.
- `Workflows::MarkFaxedToExternalPharmacy`: gate `prescription_faxed_to_external_pharmacy`; wired via `app/controllers/ehr/prescriptions_controller.rb:37`.
- `Workflows::ApprovePharmacistReview` / `Workflows::HoldPharmacistReview`: wired via `app/controllers/pharmacist/orders_controller.rb:26,38`; ApprovePharmacistReview emits a `pharmacist_released` Event.
- `Ehr::Integration::Prescribing#handoff_to_internal_queue!` (prescribing.rb:459): EHR issue path creates PharmacyOrder with status `review_pending` (prescribing.rb:460).
- `app/services/pharmacy/fulfillment_routing.rb`: per-clinic internal vs external routing; `STATE_PHARMACIES` maps TX to Elite Care Pharmacy, CA to Striker Pharmacy (fax nil); external handoff is a printed and faxed Rx via ExternalFulfillment (no fax API).
- `app/services/open_work_queue.rb`: daily-prep reads.
- `lib/tasks/pharmacy.rake`: route_external / route_internal clinic switches.

**Interfaces:**

- `namespace :pharmacist`: orders index/show + `post :approve` / `:hold`; compounding_tasks show + `post :consume_lot` (config/routes.rb:369-382).
- `namespace :lab`: orders, compounding, shipments index/show (read-only) + `daily_prep` (:385-391).
- Admin inventory_lots show (:436).
- Patient orders index/show + `get :preparation` (:246-249).
- Stripe via PrescriptionPayment (adjacent).

**Constants & rules:**

- ConsumeLot guard: lot expiration_date on or after Date.current, quantity positive, substance matches the pick_list row.
- ReleaseQa lot_traceable: every pick_list row must have a LotConsumption; fallback vial-level rows skip. QualityRelease `signature_hash` = SHA256 of actor.id:task.id:timestamp.
- ConfirmShippingReadiness hardcodes carrier FedEx, service_level standard (confirm_shipping_readiness.rb:22-23).
- First RefillTask due +28 days; first OutcomeCheckIn +2 days after delivery.

**Config:**

- `RX_FLAT_PRICE_CENTS` env var, default 19,500 cents (`RxPricing::DEFAULT_AMOUNT_CENTS`, app/lib/rx_pricing.rb:15).

**Notes:**

- Pipeline agents deterministic, non-LLM; Agents::Labeling and QualityGate emit stub payloads.
- The target Pharmacy Queue boundary ends at attributed final release and fulfillment readiness. The 3PL connection belongs to §5.6, and patient shipment communication belongs to §5.4.

### 5.3 Beyond-Use Date

**Data:**

- `compounding_tasks.beyond_use_date` (nullable date; migration db/migrate/20260502190309_create_valinor_schema.rb:287, comment "freshness window through ___").
- `formula_versions.stability_notes` is free text (no bud_days column); `label_proofs` has no BUD column (printed label pulls `task.beyond_use_date`).

**Code paths:**

- `app/services/agents/compounding_workflow.rb:33` emits `"beyond_use_date" => nil` (not derived).
- `app/services/workflows/release_qa.rb:44` and `app/services/agents/quality_gate.rb:12` hardcode `"bud_set" => true` in checklist_results regardless of the stored `beyond_use_date`.
- `app/services/agents/pharmacist_review.rb:197`: static "BUD calculated from compounding date" copy.
- Views: `app/views/lab/compounding/show.html.erb` (warning banner when `beyond_use_date` is within 14 days), `app/views/pharmacist/compounding_tasks/show.html.erb`, `app/views/shared/daily_prep/_print_labels.html.erb` (prints "BUD <date>" or a dash placeholder), `app/views/shared/daily_prep/_open_batches.html.erb`, `app/views/patient/orders/show.html.erb` ("Freshness window through <date>").

**Constants & rules:**

- 14-day BUD-expiry warning threshold (display only, lab/compounding/show).
- BUD is externally supplied; no service or job derives it in software.
- The +28-day value in `Workflows::ConfirmDelivery` is the refill due date, unrelated to BUD.

### 5.4 Delivery Bot

**Data:**

- `shipments`: `carrier`, `tracking_number`, `tracking_events` jsonb (default []), `temperature_trace` jsonb (default []), `service_level`, `cold_chain_required` (default false), `readiness_confirmed` (default false), `scheduled_dispatch_date` date, `dispatched_at`, `delivered_at`, `status` (default `ready_check`; enum ready_check/dispatched/in_transit/delivered/exception). `Shipment::CARRIER_DISPLAY` maps fedex, ups, usps, dhl, ontrac to display names; `service_level` enum includes a `saturday` value.

**Code paths:**

- `app/services/agents/shipping_coordination.rb`: deterministic BaseAgent (artifact `shipment_plan`); hardcodes carrier FedEx, service_level standard, cold_chain false, Monday-to-Thursday `next_dispatch_date`; `human_approval_required => false` (the one pipeline agent that acts without human approval).
- `app/services/workflows/dispatch_shipment.rb`: synthesizes `tracking_number` `VAL-<hex>` and tracking_events [label_created, picked_up_by_carrier].
- `app/services/workflows/confirm_delivery.rb`: appends a 'delivered' event and sets `delivered_at` (patient-confirmed).
- Views: `app/views/patient/dashboard/_fulfillment.html.erb`, `_fulfillment_stepper.html.erb` (six stages: Order placed, In review, Being compounded, Quality released, Shipped, Delivered; two-step external-fax variant Sending to pharmacy, Sent to pharmacy), `app/views/patient/orders/show.html.erb`.

**Interfaces:**

- lab shipments index/show (config/routes.rb:389); patient `get dashboard/fulfillment` (:245).

**Constants & rules:**

- Dispatch Monday to Thursday (shipping_coordination.rb comment); Saturday exists only as an unused `saturday` service_level enum value.
- Delivery triggers RefillTask +28 days and OutcomeCheckIn +2 days (ConfirmDelivery).

**Notes:**

- Delivery Bot has no live 3PL or carrier event source. It does not own packing, sealing, postage, carrier-label printing, or carrier tender; those physical operations belong to the contracted 3PL described in §5.6.

### 5.5 Medication Catalog

**Data:**

- Static medication catalog: `public/agent/medication/*.json` (41 files) plus index `public/agent/medications.json`; entries carry `do_not_infer` rules.
- `suggested_prescriptions` (db/structure.sql:7412): operator-curated pharmacy stock catalog.
- `fda_reference_strengths`: 9 seeded rows, all semaglutide, `clinical_signoff_status` pending_signoff (db/seed_data/pharmacy/v1/fda_reference_strengths.yml).

**Code paths:**

- `Ehr::Integration::PrescriptionCard#suggested_prescription_options` (prescription_card.rb:114): builds the doctor Rx-card dropdown from active SuggestedPrescription rows grouped by drug_name, prefilling medication, strength, form, route, and quantity.
- `app/services/research/medication_catalog.rb` (`Research::MedicationCatalog`): sole application loader of the static catalog (CATALOG_PATH = public/agent/medications.json), used by the research pipeline; catalog is not read by prescribing, compounding, or the SMS conversational agent.

### 5.6 3PL Fulfillment Handoff

**Data:**

- No dedicated 3PL handoff, acknowledgment, retry, or reconciliation record exists.
- `shipments.external_id` is nullable and uniquely indexed when present, but no current code populates it.
- `shipments` contains adjacent carrier, tracking, event, dispatch, delivery, and exception fields; current tracking events are synthetic rather than returned by a 3PL or carrier.
- `pharmacy_orders.external_id` belongs to the pharmacy-order lane and is not evidence of a post-release 3PL handoff.

**Code paths:**

- No 3PL or ShipStation client, outbound production job, positive-acknowledgment handler, inbound webhook or poller, status mapper, or Ops reconciliation workflow exists.
- `Workflows::ConfirmShippingReadiness` creates a Shipment and invokes `Agents::ShippingCoordination`, but it has no verified production caller.
- `Workflows::DispatchShipment` generates a local `VAL-<hex>` tracking number and synthetic label-created and carrier-pickup events.
- `ExternalFulfillment` and `Workflows::MarkFaxedToExternalPharmacy` represent the separate prescription-fax handoff to an external pharmacy. They are not the post-release 3PL shipping connection.

**Interfaces:**

- None live. No authenticated outbound contract, ShipStation or 3PL credential configuration, positive acknowledgment, authoritative inbound event feed, or exception-management surface is represented in the reconciled implementation evidence.

**Notes:**

- A future implementation must preserve the ownership and evidence boundaries in the §5.6 business rules: RonanRx owns the release gate and handoff record, the 3PL owns physical packing and shipping operations, and Delivery Bot consumes authoritative returned events for patient communication.

---

## Section 6: Billing Architecture

### 6.1 Stripe 💳

**Data:**

- `prescription_payments.billing_account` (varchar, default `'tx'`, not null; indexed on `billing_account`).
- `prescriptions.state_at_issue` (text).
- `external_fulfillments`, `pharmacy_orders` (see Internal Fulfillment Network).

**Code paths:**

- `app/lib/rx_billing.rb`: `RxBilling` with `ACCOUNTS = %w[tx ca]`, `DEFAULT_ACCOUNT = "tx"`; `account_for_state` routes CA to `ca` and everything else to `tx`; resolves per-account credentials, never touches the process-global `Stripe.api_key`, and passes a per-request `api_key` via `request_opts`.
- `app/lib/billing.rb`, `config/initializers/stripe.rb`: membership account owns the process-global `Stripe.api_key` (`STRIPE_SECRET_KEY`) and reads `Billing.price_id` from `STRIPE_PRICE_ID`.
- `app/lib/rx_pricing.rb`: flat medication price in integer cents, `DEFAULT_AMOUNT_CENTS = 19_500`, per-drug overrides in `PRICES`.
- `app/services/rx_payments/checkout_session.rb`: hosted Stripe Checkout on the Rx account; `product_name = "Prescription medication (#{pharmacy})"` using the state's pharmacy name (Elite Care for TX, Striker for CA); patient re-enters their card because the signup card lives in the membership account and cannot cross accounts.
- `app/services/pharmacy/fulfillment_routing.rb`: `STATE_PHARMACIES`, `route_for`/`pharmacy_for_state`/`enable_external!`; `internal_compounding` vs `external_pharmacy` modes.
- `app/services/workflows/price_prescription.rb`: pins `billing_account` from `RxBilling.account_for_state(state_at_issue)`; re-pins only when there is no live checkout session.
- `app/services/provider_agreement_packet/cover_fields.rb`: `provider_fee_cents` defaults to `2900`.
- `config/text_authorization/provider_agreement_packet_contract_v1.yml`: contract text stating the Provider Fee is settled through Stripe, separate from a flat Platform Fee.

**Interfaces:**

- Webhooks: `post stripe/webhook` (`webhooks/stripe#receive`, membership); `post rx/stripe/webhook` (`webhooks/rx_stripe#receive`, Rx accounts, tries each account's webhook secret until the signature verifies).
- Two isolated Stripe account families: membership and per-state Rx (`tx`/`ca`).

**Config:**

- `STRIPE_SECRET_KEY`, `STRIPE_PRICE_ID` (membership account).
- `RX_<STATE>_STRIPE_SECRET_KEY`, `RX_<STATE>_STRIPE_PUBLISHABLE_KEY`, `RX_<STATE>_STRIPE_WEBHOOK_SECRET`; legacy `RX_STRIPE_*` falls back for the `tx` account only.
- `RX_PAYMENT_FALLBACK` (default `"true"`; any value but `"false"` suppresses the Pay button in favor of a pharmacy-contact state).
- `RX_FLAT_PRICE_CENTS` overrides the flat medication price.

**Schedules:**

- `rx_checkout_reconcile`: `RxCheckoutReconcileJob` every 15 minutes; iterates `RxBilling::ACCOUNTS`.

**Notes:**

- Serviceable states are TX and CA only (`Onboarding::Serviceability::STATE_CONFIG`); CA formulary restricted to `tirzepatide-5mg-ml` and `semaglutide-2-5mg-ml`.
- `test/lib/rx_billing_test.rb` asserts the per-prescription charge carries its own account key and never the global membership key.

---

## Section 7: Async Care Loop

### 7.1 Follow-Up Appointment

**Data:**

- `outcome_check_ins`: `cadence`, `patient_program_enrollment_id`, `scheduled_at`, `sent_at`, `responded_at`, `score`, `free_text`, `status` (default `'scheduled'`).
- `support_messages` (prompt transport), `events`.

**Code paths:**

- `app/services/check_in_dispatcher.rb`: marks due check-ins sent, writes a portal `SupportMessage` prompt, and self-schedules the next check-in.
- `app/services/agents/outcome_tracking.rb`: deterministic stub.
- `app/services/workflows/confirm_delivery.rb`: seeds the first check-in on delivery.
- `app/controllers/patient/check_ins_controller.rb#update`: records `score`/`free_text`, sets `status` to `'responded'` and `responded_at`, emits an event.
- `app/controllers/admin/check_ins_controller.rb#create`: manual dispatch.
- `app/controllers/admin/outcomes_controller.rb`: aggregate trends.

**Schedules:**

- `outcome_check_in`: `OutcomeCheckInJob`, `0 9 * * * America/Chicago` (`config/recurring.yml`); all logic in `CheckInDispatcher`.

**Interfaces:**

- Routes: patient `check_ins` index/show/update; admin `post run-check-ins` to `check_ins#create`; admin `outcomes` index.

**Constants & rules:**

- `CheckInDispatcher::CADENCE_DAYS`: weekly 7, biweekly 14, monthly 30; `DEFAULT_CADENCE_DAYS = 14`.
- First check-in `Date.current + 2` days after delivery.
- `PROMPT_BODY` hard-coded: "How's it going? Reply 1 (worst) to 5 (best), free text optional. Your doctor and pharmacist will see your reply."
- Score stored only when between 1 and 5.
- Admin outcomes windows: 8-week score trend, 12-week enrollment trend.

### 7.2 Refills

**Data:**

- `refill_tasks`: `prescription_id`, `due_date`, `status` (default `'scheduled'`; enum scheduled/confirmed/held/completed/cancelled), `hold_reason`.
- `therapy_holds`.

**Code paths:**

- `app/services/workflows/confirm_refill.rb`: patient-only confirm, guarded by `TherapyHolds::Guard.with_clearance!(action: :refill)`, emits `patient_confirmed_refill`.
- `app/services/workflows/confirm_delivery.rb`: creates the first `RefillTask` (`due_date = Date.current + 28`) and calls `Agents::Refill`.
- `app/services/agents/refill.rb`: deterministic; `next_refill_date = Date.current + 28`; returns decision `hold` with `human_approval_required = true` when an active therapy hold exists.
- `app/services/therapy_holds/guard.rb`: `allowed?`, `ensure_clear!`, `with_clearance!` keyed by `action`.
- `app/controllers/patient/refill_tasks_controller.rb#update`: confirm or pause (sets `status` `held` with `hold_reason`).
- `app/controllers/admin/drill_downs_controller.rb`: read-only `refill-queue` slug (scheduled tasks).
- `app/services/linq/send_guard.rb`: `Linq::SendGuard` restricts outbound messages to approved templates.

**Interfaces:**

- Routes: patient `refill_tasks` index/show/update.

**Constants & rules:**

- First refill due `Date.current + 28` days after delivery.
- Refill confirmation blocked while an active `TherapyHold` applies.
- Patient confirm flash copy hard-coded: "Refill confirmed. Pharmacy will queue your next compounding run."

### 7.3 Clinical Rounds

**Data:**

- `events`: `action`, `actor_id`/`actor_type`, `eventable_id`/`eventable_type`, `metadata` jsonb, `summary`, `created_at`; `EVENTABLE_TYPES` allowlist in `app/models/event.rb` includes `OutcomeCheckIn`, `RefillTask`, `SupportMessage`, `Shipment`, `QualityRelease`, `LinqConversation`, and others; `actor_type` validated against `ACTOR_TYPES`.
- `patient_graphs`: `patient_id`, `status` (enum draft/current/superseded), jsonb `allergies`, `diagnoses`, `labs`, `medications`, `vitals`, `missing_data`, `timeline` (holds lifestyle notes extracted from intake).

**Code paths:**

- `app/controllers/patient/dashboard_controller.rb`: patient-visible activity feed, `Event.where(action: PATIENT_VISIBLE_ACTIONS)` limited to 8.
- `app/controllers/doctor/cases_controller.rb#show`: per-case snapshot (current `patient_graph`, `physician_brief` artifact, last 14 Oura `wearable_daily_metrics`).
- `app/controllers/admin/outcomes_controller.rb`: aggregate trends (8-week score, 12-week enrollment).
- `app/services/agents/patient_summary.rb`, `app/services/agents/data_normalization.rb`: write `patient_graphs`.
- `app/models/intake_response.rb#extract_lifestyle_timeline`: populates `patient_graphs.timeline`.

### 7.4 Patient Chat

**Data:**

- `linq_conversations`: `human_takeover_actor_id`, `human_takeover_started_at` hold assistant-takeover state.
- `linq_events`, `appointment_messages`, and `support_messages` record path-specific delivery and the patient support inbox.

**Code paths:**

- `Linq::AdminMessages::Send` (`app/services/linq/admin_messages/send.rb`), `Ops::Integration::ConversationMessageGateway`, and `Ops::ConversationActionsController#send_message`: staff free-text send from Ops with opt-out, safety-hold, unsafe-link, idempotency, and audit handling.
- `Ops::Integration::TakeoverGateway` calls `Workflows::StartLinqTakeover` and `Workflows::ReleaseLinqTakeover`; Ops exposes pause and resume controls.
- `LinqReplyJob` separates conversational delivery from takeover-exempt delivery and rechecks conversation state before sending.
- `Linq::SendGuard` (`app/services/linq/send_guard.rb`): opt-out consent check, template and mode delivery policy, and a `reasons[]` failure path applied on every routed send.

**Config:**

- `LinqConversation::HUMAN_TAKEOVER_TTL = 24.hours` (`app/models/linq_conversation.rb`); human takeover suppresses conversational replies until explicit release or TTL expiry.

**Interfaces:**

- Linq is the outbound transport; Rails owns consent, policy, audit, takeover, dedupe, and delivery.

**Notes:**

- `test/architecture/linq_takeover_sender_classification_test.rb` inventories every Linq sender against an owner classification, separating the conversational onboarding lane from the exempt transactional, staff, and reminder lane (appointments notifier, health-history and weigh-in nudges, pre-doctor reminders, logins, admin messages, rx-ready-to-pay), and fails on any unclassified or bypassing path.

### 7.5 Medical Support Chat

**Data:**

- `linq_emergency_holds`: `linq_conversation_id`, `tier` (CHECK 1-2 for auto), `urgency`, `registry_trigger`, `detection_source` (default `governed_registry`), `response_template_id`, `phone_binding_digest`, `status` (active/released), `released_by_id`, `release_reason`.
- `adverse_events`: `tier` (CHECK 1-3), `urgency`, `registry_trigger`, `verbatim_message`, `seriousness` (default `pending_clinical_review`), `product_related_status` (default `undetermined`), `linq_emergency_hold_id`, `dosing_error_incident_id`, `detected_at`.
- `on_call_escalation_schedules`: `adverse_event_id`, `action` (live_call|ops_fallback|acknowledgment_deadline|patient_contact_deadline), `run_at`, `completed_at`.
- `therapy_holds`: `patient_id`, `prescription_id`, `reason`, `reason_binding` (sha256), `application_source` (manual_clinician|linq_emergency_hold|dosing_error_incident), `status`, `release_safety_screen_run_id`, `release_chart_fingerprint`, `release_policy_fingerprint`.
- `red_flag_safety_events`, `adverse_event_escalation_entries`, `medwatch_reports`, `dosing_error_incidents`.

**Code paths:**

- `app/services/linq/emergency_detection.rb` (VERSION `b7_adversarial_v1`; governed emergency registry with static fallback; runs on every inbound SMS) and `app/services/linq/red_flag_detector.rb`.
- `Workflows::LatchLinqEmergencyHold` (`AUTO_HOLD_TIERS = [1, 2]`) and `Workflows::ReleaseLinqEmergencyHold` (roles doctor or pharmacist, same clinic, reason required).
- `Workflows::{RecordAdverseEvent, RecordRedFlagSafetyEvent, RecordAdverseEventEscalation, AssessAdverseEvent, SubmitMedwatchReport, ApplyTherapyHold, ReleaseTherapyHold}`.
- `app/services/on_call/dispatch.rb`: `OnCall::Dispatch::ADAPTERS` registry with a `paging_ready?` guard gating dispatch; `app/services/on_call/config.rb` reads the on-call env vars.
- `app/services/linq/therapy_hold_reconciler.rb`, `app/services/therapy_holds/guard.rb`, `Linq::EmergencyHoldReply`.
- Jobs: `app/jobs/on_call_escalation_job.rb` (AdverseEvent after-create; tier 1 schedules the ladder, tier 3 routes to the care-team queue), `app/jobs/on_call_escalation_sweep_job.rb`, `app/jobs/linq_therapy_hold_reconcile_job.rb`.

**Config:**

- `ON_CALL_TARGETS`, `ON_CALL_TRANSPORT`, `ON_CALL_PAGING_ENABLED` configure paging targets, transport, and enablement (`app/services/on_call/config.rb`).
- Emergency reply template (`app/services/linq/templates.rb`, `EMERGENCY_HOLD`): "This could be an emergency. Call 911 now or go to the nearest emergency room. If you are thinking about harming yourself, call or text 988."

**Schedules:**

- `on_call_escalation_sweep` runs every minute (`config/recurring.yml`), executing due ladder steps.

**Interfaces:**

- `POST /api/v1/linq/inbound`: inbound-SMS entry point.
- `POST /staff/red_flag_safety_events`; `POST /staff/adverse_events/:adverse_event_id/escalations`.
- Linq inbound webhook and emergency reply; on-call paging through `OnCall::Dispatch` adapters.

**Notes:**

- Detection is deterministic governed-registry keyword and trigger matching, not an LLM.
- Tiers 1-2 auto-latch the emergency hold with the 911/988 reply and cascade to `therapy_holds` and `adverse_events`.
- Escalation ladder: tier 1 schedules `live_call` at +5 min and `ops_fallback` at +10 min; other tiers schedule an acknowledgment deadline at +15 min and a patient-contact deadline at +1 hour; tier 3 routes to the care-team queue.
- MedWatch `report_due_at` is `detected_at + 15.days` (`app/services/workflows/record_adverse_event.rb`).
- Therapy-hold release requires `trigger_resolved`, resolution confirmation with a sha256 binding, a release safety-screen run, and chart and policy fingerprints (DB CHECK).

---

## Section 8: App Data & Health Profile

### 8.1 Device Data

**Data:**

- `wearable_connections` and `wearable_daily_metrics` store Oura and Withings connections and normalized daily metrics; metrics are stored as jsonb, with source via provider and `wearable_connection`, time via `measured_on`, and `source_ref`.
- `scale_measurements` stores normalized scale readings.
- Unique index `idx_wearable_daily_metrics_patient_provider_date` on `(patient_id, provider, measured_on)`: one row per patient, provider, and day (`db/structure.sql`).

**Code paths:**

- `app/services/oura/*`: `client`, `oauth`, `webhook`, `webhook_processor` (`SUPPORTED_DATA_TYPES` allowlist ignores unsupported `data_type`), `webhook_subscriptions`, `daily_sync`.
- `app/services/withings/*`: `client`, `oauth`, `sync`, `webhook`, `webhook_subscriptions`.

**Schedules:**

- `oura_daily_sync` (`OuraDailySyncJob`) at 05:00 CT and `withings_daily_sync` (`WithingsSyncJob`) at 05:30 CT (`config/recurring.yml`).

**Interfaces:**

- Withings OAuth and measure API; Oura OAuth and webhooks.

### 8.2 Intake 3: Medical History

**Data:**

- `patient_health_histories`: `questionnaire_version`, `status` (draft/complete), `answers` (encrypted text), `section_progress`, `current_section_key`, `revision_number`, `completed_at`.
- `patient_health_history_revisions` (append-only): `revision_number`, `event`, `changed_section_key`, `answers_snapshot`, `section_progress_snapshot`, `created_by_id`.
- `health_history_nudges`: `delivery_mode` defaults `dry_run`.
- `ehr_medical_histories`: `category`, `entry`, `code`, `code_system`, `status` (default `active`), `source` (default `patient_reported`), `recorded_by_id`, `import_fingerprint`.

**Code paths:**

- `app/services/health_history/question_flow.rb`: declarative wizard ordering; `PRIORITY_QUESTION_KEYS`; `REQUIRED_QUESTION_KEYS = ['biometrics.current_weight_lb']` hard-required before advancing.
- `app/services/health_history/intake_prefill.rb`: prefills goal, biometrics, conditions, medications, and allergies from `IntakeResponse`; `SOURCE_LABEL = "From your secure signup"`.
- `app/services/health_history/nudge_dispatcher.rb` and `nudge_notifier.rb`.
- `app/services/ehr/health_history_projector.rb`: projects completed answers into `Ehr::MedicalHistory` with `source` `patient_reported` and a provenance snapshot (source history id, questionnaire version, revision number, `completed_at`, projection fingerprint), emitting an `ehr_health_history_projected` audit event; `Ehr::HealthHistoryReview` (`app/models/ehr/health_history_review.rb`) is the projection-review record.
- Models: `app/models/patient_health_history.rb` (`DEFAULT_VERSION`, `contract(version)`), `patient_health_history_revision.rb`, `health_history_nudge.rb` (`MAX_SENDS = 3`, `THROTTLE_WINDOW = 72.hours`), `ehr/medical_history.rb`.
- Controllers: `app/controllers/patient/health_histories_controller.rb`; `app/controllers/doctor/health_histories_controller.rb` (triggers `Ehr::Integration::Audit.chart_access!`).

**Schedules:**

- `health_history_nudge` (`HealthHistoryNudgeJob`) daily at 10:00 CT (`config/recurring.yml`).

**Interfaces:**

- Patient routes: `resource :health_history` with `get`/`patch questions/:position` (one-question-per-screen wizard) and `resource :health_profile`; doctor `cases` member `get :health_history`.
- Linq (nudges); EHR chart audit; `IntakeResponse` prefill.

**Notes:**

- Wizard saves create revision snapshots.
- Nudge dispatch runs through the daily sweep with a lifetime cap of 3 sends (`MAX_SENDS`), a 72-hour throttle (`THROTTLE_WINDOW`), skips patients at 100% completion, and is `dry_run` by default.

---

## Section 9: AI / Agent Layer

### 9.1 Agent Router

**Data:**
- `linq_conversations`, `linq_events`: Rails-owned conversation, event, policy, and delivery state.
- Staging Flue keeps its onboarding record separately via `RrxAgents::OnboardingRecord` (staging).

**Code paths:**
- `Api::V1::LinqController#inbound` (app/controllers/api/v1/linq_controller.rb): webhook verification, opt-out, duplicate/reset handling, emergency and dosing policy, human-takeover precedence, and pre-doctor routing; safety and policy branches run before conversational routing.
- `Agents::OnboardingConcierge` (app/services/agents/onboarding_concierge.rb): intake-field proposer, called before staging route resolution.
- Staging: accepted onboarding turns enqueue `Linq::AgentTurnJob` (app/jobs/linq/agent_turn_job.rb), which calls `RrxAgents::TurnClient` and returns the reply through `LinqReplyJob` (staging).
- Rails retains completion-link creation, persistence, SendGuard, dedupe, retry, and outbound delivery.

**Interfaces:**
- Linq is the inbound and outbound messaging transport.
- Staging calls the Flue onboarding service in `rrx_agents/` over HTTP via `RrxAgents::TurnClient`, for onboarding generation only (staging).

**Config:**
- Staging Flue onboarding agent uses Mixlayer model `mixlayer/z-ai/glm-5.2` (rrx_agents/src/agents/onboarding.ts) (staging); Ruby real-LLM agents use the `BaseAgent` OpenAI path (`OPENAI_MODEL`, default gpt-5.5).

**Constraints:**
- test/integration/api/v1/linq_takeover_suppression_test.rb: human-takeover, red-flag, and dosing-safety branches run before and outrank conversational agent routing; no generative reply is enqueued during a hold.
- test/architecture/linq_takeover_sender_classification_test.rb: new Linq send sites must be explicitly classified.

### 9.2 Patient Support

**Data:**
- `support_messages`; `SupportMessage` status enum new/read/replied/escalated (app/models/support_message.rb).

**Code paths:**
- `Agents::PatientSupport` (app/services/agents/patient_support.rb): real-LLM draft-reply agent, artifact `support_response`, escalate flag, deterministic mock fallback; exercised only by test/services/agents/base_agent_test.rb.
- `Patient::SupportMessagesController#create` (app/controllers/patient/support_messages_controller.rb): writes the message and an Event.
- `Onboarding::CancelSubscription` (app/services/onboarding/cancel_subscription.rb): deterministic `Stripe::Subscription.cancel`, invoked for non-serviceable states via `handle_serviceability_hold!`.
- `Onboarding::Serviceability` computes the serviceability shown in the completion wizard; `Onboarding::PaymentEligibility` does not gate payment on it.

**Interfaces:**
- Stripe (membership subscription).

**Constraints:**
- `CancelSubscription#cancelable?` checks only that a non-synthetic Stripe subscription id is present and status is not already cancelled; it does not check signup age, trial state, invoice or payment history, or first-month status.

### 9.3 Retention Agent

TBD. No mechanism exists yet. The target lives in this card's business.md section.

### 9.4 Founder Mode / Dr. Bot

**Code paths:**
- `Linq::FounderMode` (app/services/linq/founder_mode.rb): trigger-gated diagnostic variant of the Linq onboarding intake, gated by `Linq::Capture.founder_trigger?` (app/services/linq/capture.rb:654). Present on main; deleted on origin/staging in the Flue onboarding cutover, with no replacement in the staging Flue onboarding agent.

### 9.5 Medical Research Agent

**Data:**
- `research_documents` (`pmid`, `pmcid`, `doi`, `parse_status`, `parse_tier`, `download_status`, `full_text_source`, `license`, `xml_object_key`, `pdf_object_key`, `markdown_object_key`).
- `research_citations` (`medication_slug`, `medication_name`, `evidence_type`, `rank`, `research_document_id`).
- `source_citations` is a separate chart-provenance system (anchors EHR fields to UploadedDocument pages), not part of this pipeline.

**Code paths:**
- Synthesis: `Agents::ResearchSynthesis` (app/services/agents/research_synthesis.rb), real-LLM `clinical!`, artifact `evidence_packet`, wired via `Workflows::SubmitForDoctorReview` (submit_for_doctor_review.rb:33). Its input is a recommendation id and program name, so citations come from model memory, not the corpus.
- Ingestion services (app/services/research/, 18 files): `pubmed_client.rb`, `full_text_resolver.rb` (PMC JATS-XML, PMC PDF, Unpaywall, Context.dev), `jats_to_markdown.rb`, `llama_parse_client.rb`, `pdf_downloader.rb` (%PDF magic-byte guard), `rate_limiter.rb` (Solid-Cache token bucket), `paper_store.rb` (GCS), `ingest_config.rb`.
- Ingestion jobs (app/jobs/research/, 10 files): `IngestMedicationPapersJob`, `IngestAllMedicationsJob`, `ResolveFullTextJob`, `ProcessDocumentJob`, `ParsePaperJob`, `FinalizeParseJob`, `ReconcileParseJob`.
- rake `research:ingest[slug]`, `research:ingest_all`, `research:confirm_tier[document_id]` (lib/tasks/research.rake).

**Interfaces:**
- Webhook `POST research/llamaparse/callback` to `Api::V1::LlamaParse#callback` (routes.rb:111), authenticated by `LLAMAPARSE_WEBHOOK_TOKEN`.
- PubMed/NCBI E-utilities, PMC OA, Unpaywall, LlamaParse/LlamaCloud, Context.dev, GCS.

**Config:**
- `RESEARCH_INGEST_ENABLED` (default off), `NCBI_API_KEY`, `UNPAYWALL_EMAIL`, `LLAMA_CLOUD_API_KEY`, `CONTEXT_DEV_API_KEY`, `GCS_RESEARCH_BUCKET`.
- `RESEARCH_MAX_RESULTS_PER_MED` (default 25, pubmed_client.rb:171).
- `RESEARCH_PARSE_MAX_TIER` (default `agentic`, parse_paper_job.rb DEFAULT_CEILING); 45-credit `agentic_plus` only via `research:confirm_tier`.

**Schedules:**
- `Research::ReconcileParseJob` every 15 minutes (config/recurring.yml).

**Constraints:**
- Unique index `idx_research_documents_unique_pmid` (one document per pmid); unique index `idx_research_citations_one_per_doc_med` (research_document_id, medication_slug).
- `Workflows::ApproveByDoctor` (approve_by_doctor.rb) gate `ai_never_makes_final_prescribing_decision` plus a hard `actor.role == "doctor"` guard before any Prescription is created; research output is a non-actioning reference artifact.

### 9.6 Health Coach

TBD. No mechanism exists yet. The target lives in this card's business.md section.

### 9.7 Greptile Watchdog

**Data:**
- `observability_findings` (`fingerprint`, `source_rule`, `severity`, `status`, `evidence`, `linked_issue_id`, `linked_pr_url`, `post_ship_metric`, `post_ship_checked_at`).
- `linq_judge_reports` (`report_date`, `status`, `conversations_scanned`, `judged_count`, `truncated_count`, `warmth_avg`, `flag_counts`, `flagged`).
- `governed_artifacts`, `governed_artifact_versions`, `governed_artifact_eval_runs` (`suite_id`, `suite_fingerprint`, `content_sha256`, `detector_source_sha256`, `provider_run_digest`, `passed`, `model`, `case_count`, `provenance_source`, `evidence_digest`).

**Code paths:**
- `bin/architecture-check`: structural drift script comparing code against hard-coded structural allowlists (advisory, manual; exits nonzero only on new drift beyond the grandfathered baseline).
- `NoteDriftSamplingJob` with `Ehr::Integration::NoteDriftSampling`: weekly signed-note-versus-transcript judge.
- `DeploymentReleaseCanaryJob`: post-deploy release-SHA assertion.
- `ObservabilityDigestJob`: `Observability::WorkflowMetrics`, `DeterministicAnalyzer`, optional `LlmRanker` (OpenAI, flag-gated), `SlackDigest`; persists to observability_findings.
- `LinqNightlyJudgeJob` with `Linq::NightlyJudge`: deterministic POISON/SUPERIORITY tripwires plus LLM judge into linq_judge_reports.
- `Observability::SentryScrubber`: every emit site must declare its message in `ALERT_MESSAGES` (sentry_scrubber.rb:21), with per-alert validators in `ALERT_EXTRA_VALIDATORS` (:52); `registered!` (:230) raises at class load for unregistered messages. Raw record IDs egress only as HMAC tokens via `Observability::CorrelationId`.
- `GovernedArtifacts::{OfflineEvalRunner, RecordEvalRun, Registry}`: versioned governed artifacts with hash-chained passing-only eval runs.

**Interfaces:**
- Admin routes: `get observability` (routes.rb:398), `get linq_observability` (:399), `resources :governed_artifacts` with nested `versions` (:422-425).
- OpenAI (nightly judge, observability ranker); Slack (`SlackDigest`); Sentry (`SENTRY_RELEASE` canary, scrubber).

**Schedules:**
- `NoteDriftSamplingJob` Monday 7am America/Chicago (recurring.yml:107).
- `ObservabilityDigestJob` daily 5:30am America/Chicago (:78).
- `LinqNightlyJudgeJob` daily 6am America/Chicago (:103).

**Config:**
- `LINQ_JUDGE_MODEL` (default gpt-5.5), `LINQ_JUDGE_MAX_CONVERSATIONS` (default 200).

**Constraints:**
- `MAX_JUDGED = 200` conversations per night (nightly_judge.rb:14); overflow counted as truncated, never dropped.
- `governed_artifact_eval_runs` CHECK constraints (db/structure.sql): `chk_governed_eval_runs_passing_only` (passed = true), `chk_governed_eval_runs_digests` (five sha256 columns match `^[0-9a-f]{64}$`), `chk_governed_eval_runs_provenance` (provenance_source in offline_runner, migration_backfill).
- `GovernedArtifacts::Registry` review intervals: `review_interval_days` 365 for most artifacts, `commercial_comparator_table` 90, `emergency_triage_registry` 180 (registry.rb).

### 9.8 Agent Census

**Code paths:**
- Ruby agent namespace `app/services/agents/`: `BaseAgent` plus 22 subclasses on main; 21 on origin/staging, where `Agents::LinqConversationalist` is deleted and `Agents::OnboardingConcierge` is retained.
- Focused Flue onboarding agent in `rrx_agents/`, outside the Ruby namespace (staging).
- LLM-bearing systems outside `app/services/agents/`: EHR agents (app/services/ehr/agents/: note_draft, note_drift, icd_suggestions, rx_suggestions, dictation_command), `Linq::NightlyJudge`, observability `LlmRanker`, and `Records::GeminiPatientRecordExtractor`.

**Config:**
- Ruby real-LLM agents use the `BaseAgent` OpenAI path (`OPENAI_MODEL`, default gpt-5.5); the Flue onboarding agent uses Mixlayer `mixlayer/z-ai/glm-5.2` (staging).

**Constraints:**
- Partial machine-checked inventories: test/services/agents/clinical_agent_fallbacks_test.rb (real-LLM agent list) and test/architecture/linq_takeover_sender_classification_test.rb (Linq send-site classification).

---

## Section 10: RonanRX iOS & Scope Boundaries

### 10.1 RonanRX iOS

Mechanism lives in the companion `RonanRX_iOS` repository (not part of `ronanrx-core`).

**Repo:** `RonanRX_iOS`: SwiftUI patient app, iOS 17, XcodeGen, bundle `com.ronanrx.app`.

**Code paths (Swift packages):**

- `RonanDesign`: design tokens and chart, gauge, calendar, and body-map components.
- `RonanCore`: GRDB database and migrations, Keychain auth session, APIClient/MobileAPI, offline SyncEngine (outbox, dead-letter, opaque cursors), ReminderScheduler (72h BGTask lease), and client-side ReconstitutionCalculator and DerivedTitrationStep dosing math.
- `RonanHealth`: HealthKitManager, HealthKitBatchUploader, AnchorStore, SampleMapper.

**Config (project.yml):**

- Entitlements: `com.apple.developer.healthkit`, `com.apple.developer.healthkit.background-delivery`, `group.com.ronanrx.app`.
- Info.plist: `BGTaskSchedulerPermittedIdentifiers` `com.ronanrx.app.reminder-lease`; `UIBackgroundModes` `fetch`.
- HealthKit usage string: "RonanRX reads selected HealthKit data to share it with your care team".

**Interfaces:** Fastlane env vars `ASC_KEY_ID`, `ASC_ISSUER_ID`, `ASC_KEY_CONTENT_BASE64`, `IPA_PATH`.

**Notes:** HealthKit authorization and synchronization follow the App Data Collection card.

### 10.2 The Daily Compound

Out of this suite's scope: separate repository, audited separately.

### 10.3 ronanrx-brain

Out of this suite's scope: separate repository, audited separately.

### 10.4 Elite Care Pharmacy Site

Out of this suite's scope: separate repository, audited separately.

### 10.5 TestFlight Protocol Sandbox

TBD. No mechanism exists yet. The target lives in this card's business.md section.

---

## Section 11: Provider Onboarding

### 11.1 Provider Onboarding

**Data:**

- `provider_invites`: `created_by_user_id`, `clinic_id`, `status` (pending/completed), encrypted `first_name`/`last_name`/`contact_email`/`phone`/`npi`/`dea_number`/`prefill` (with `contact_email_lookup_hash` shadow), `token_nonce`, `token_consumed_at`, `completed_provider_user_id`; `prefill` carries free-text specialty and capture-only malpractice, payout, prescribing-preference, and program fields.
- `provider_agreement_acceptances`: `provider_id`, `packet_id`, `packet_version`, `content_sha256`, `sealed_pdf_locator`, encrypted `signer_name`, `signed_at`, `attestations` jsonb.
- `ehr_provider_credentials`: `provider_id`, `credential_type` (default `medical_license`), `npi`, `dea_number`, `status` (active/inactive/expired/revoked), `issued_on`/`expires_on`; unique partial indexes on `npi` and `dea_number` when present.
- `ehr_provider_state_licenses`: `provider_credential_id`, `provider_id`, `state`, `license_number`, `status`, `issued_on`/`expires_on`; unique partial `idx_ehr_provider_state_one_active_license` on (`provider_id`, `state`) where status is active.
- `clinics`: tenancy root; `name`, `slug` (unique), `status` (active/paused/archived), `settings` jsonb, `white_label_pharmacy_name`.

**Code paths:**

- Invite path: `app/controllers/admin/provider_invites_controller.rb` and `app/controllers/provider/onboarding_controller.rb` call `Workflows::CreateProviderFromInvite`, which records `ProviderAgreementAcceptance` and provisions through `Ehr::Integration::Providers.provision!` with credentials and licenses `status: active`.
- Self-signup path: `DoctorSignupsController` (gated by `DOCTOR_SIGNUP_CODE`, fails closed when unset) calls `DoctorSignups::Register`, which creates the User and writes `ehr_provider_credentials` and `ehr_provider_state_licenses` `status: inactive`.
- Agreement packet (provider twin of Sign Docs): `app/services/provider_agreement_packet/{cover_fields,renderer,render_and_seal}.rb`; contract `config/text_authorization/provider_agreement_packet_contract_v1.yml` (six ordinals, carries `packet_version`).
- `ProviderAgreementPacketCleanupJob`.

**Routes:**

- `get/patch provider/onboard`, `post provider/onboard/exchange`, `post provider/onboard/sign`, `post provider/onboard/passkey_options`.
- `get/post doctors/sign_up`.

**Config:** `DOCTOR_SIGNUP_CODE` deploy-time secret; route redirects to `/staff/sign_in` when unset.

**Constants:**

- `MIN_PASSWORD_LENGTH = 12` in both `DoctorSignups::Register` and `Workflows::CreateProviderFromInvite`.
- `PLATFORM_FEE_CENTS = 1000` ($10 platform fee) and a default provider fee of 2900 cents ($29) in `provider_agreement_packet/cover_fields.rb`, summed into a $39 total monthly fee.
- One active license per (provider, state); NPI and DEA unique when present.

---

## Section 12: Ops Console & Oversight

### 12.1 Ops Console

Two planes share the `ops_*` substrate: the human console at `/ops` and the machine Ops API at `api/ops/v1`.

**Data:**

- `ops_access_logs`: fail-closed PHI access audit via `Ops::Integration::Audit.access!` (insert failure returns 500 and no PHI decrypts); `actor_id`, `action` (allowlist `Ops::AccessLog::ACTIONS`, including `assistant_paused`/`assistant_resumed`/`impersonation_started`/`impersonation_stopped`), `patient_id`, `linq_conversation_id`, `request_id`, `metadata`, `occurred_at`.
- `ops_command_executions`: append-only record of every executed Ops API command; `request_id` (unique), `command`, `input_digest`, `before_summary`/`after_summary`, `result`, `warnings`, `next_actions`.
- `ops_credentials`: scoped bearer credentials for the Ops API; `token_digest` (unique), `scopes` jsonb, `expires_at`/`revoked_at`/`last_used_at`.
- `ops_mutation_intents`: two-phase mutation preflight; single-use confirm token (`token_digest` unique, hex-64 CHECK), `idempotency_digest`/`input_digest` (hex-64 CHECKs), `planned_changes`, `human_approval_required` default true, `expires_at`/`consumed_at`.
- `ops_mutation_receipts`: idempotency receipts; unique (credential, command, idempotency_digest) and unique `mutation_intent_id`, append-only.

**Code paths:**

- Console (guarded by `Ops::Config.enabled?`): dashboard and funnel strip, POST-only search, People directory, conversation detail, completion-link resend, staff send, assistant pause and resume, clinical-field editor, appointment cancel and rebook, ID viewer, and suggested-prescriptions management.
- Impersonation: `Ops::Integration::Impersonation` (`ROLES = %w[doctor patient]`, `LIST_LIMIT = 100`, name lookup via bounded decrypt-and-scan `NAME_SCAN_CAP = 50`); `app/controllers/ops/impersonations_controller.rb` stashes `session[:impersonator]` and sets `session[:staff_auth_method] = "impersonation"`.
- Takeover: `Ops::Integration::TakeoverGateway.pause/.resume` calls `Workflows::Start/ReleaseLinqTakeover` (24h TTL); `Ops::Integration::ConversationMessageGateway#send_message` dispatches, then calls `engage_takeover` only after dispatch and rescues its errors.
- Operator sends: `Linq::AdminMessages::Send` (opt-out, safety-hold, and unsafe-link blocks; caller idempotency key mapped to an `ops_<conversation>_<key>` Linq message id; bypasses the SendGuard body PHI and price scan, URL audit still applies).
- EHR write facades: `Ehr::Integration::ClinicalFields` writes six operator-editable fields (height, weight, allergies with NKDA, conditions, medications, treatment goal) to `Ehr::Vital`/`Allergy`/`MedicalHistory`/`Medication` with source `ops_edit`, `CODE_SYSTEM ronanrx_ops_clinical_edit`, and Event `ops_clinical_field_edited`; additive/upsert-only, never retracts rows. `Ehr::Integration::CareTeam.ensure_provider!` pre-seeds cross-clinic `Ehr::CareTeamMembership` at Ops rebook.

**Routes:**

- Console (`constraints ops_enabled`): `get ops/people`, POST-only `people/search`, `get ops/people/:id`, `post users/:id/impersonate`, top-level `delete /impersonation` (outside the admin gate, reachable while swapped), `resend_completion_link`, `cancel_appointment`, `rebook_appointment`, `id_document`, `suggested_prescriptions`.
- Machine plane `api/ops/v1`: commands plus `post mutations/:command/preflight` and `post mutations/:command/apply`.

**Config:** `Ops::Config.enabled?` kill switch gates the whole console.

**Constants & rules:**

- Impersonation auth method is excluded from `signing_auth_methods` and `privileged_auth_methods`: an operator viewing a doctor can browse the chart but cannot sign or issue an Rx and cannot use the admin security-control plane.
- Every console action writes a fail-closed ops access log.
- Ops API mutations are two-phase: preflight intent, single-use confirm token, idempotency receipt, with `human_approval_required` defaulting to true.

---

## Integration inventory

### In code today

| Integration | Purpose | Auth | Status |
|---|---|---|---|
| Stripe: membership account | $39/mo platform subscription, SetupIntent + Payment Element | `STRIPE_SECRET_KEY` (global api key), `STRIPE_PUBLISHABLE_KEY`, `STRIPE_WEBHOOK_SECRET`, `STRIPE_PRICE_ID` | 🟢 Live in prod since 2026-06-29 (repo-runbook claim: docs/runbooks/stripe-apple-pay-payment-mode.md; live account/price state needs the same external Stripe receipts as the Rx rows) |
| Stripe: Rx `tx` (Elite Care Pharmacy) | Medication Checkout, all states except CA; legacy 19,500-cent fallback | `RX_TX_STRIPE_SECRET_KEY`/`_PUBLISHABLE_KEY`/`_WEBHOOK_SECRET` (legacy `RX_STRIPE_*` fallback) | 🟡 Code-wired; charging dark by default (`RX_PAYMENT_FALLBACK`); external account state is not source-verifiable |
| Stripe: Rx `ca` (True Nano / "Striker Pharmacy") | CA medication Checkout | `RX_CA_STRIPE_SECRET_KEY`/`_PUBLISHABLE_KEY`/`_WEBHOOK_SECRET` | 🟡 Code-wired; external account state is not source-verifiable; vendor naming remains inconsistent in code |
| Linq partner API | SMS/iMessage: intake agent, links, reminders, nudges, pay links | `LINQ_API_TOKEN` outbound; `LINQ_WEBHOOK_SECRET` inbound (Standard Webhooks) | 🟢 Live (primary front door) |
| OpenAI | Ruby BaseAgent agents, EHR agents, NightlyJudge, LlmRanker, governed-artifact evals | `OPENAI_API_KEY`, model `OPENAI_MODEL` (default gpt-5.5) | 🟢 Ruby runtime path; staging Flue onboarding uses Mixlayer instead |
| Google Gemini | ID OCR (`Identity::IdDocumentExtractor`) + records extraction (`Records::GeminiPatientRecordExtractor`) | `GEMINI_API_KEY` (BAA in place) | 🟢 Live |
| Google Meet REST v2 / Workspace DWD | Per-visit rooms, transcripts, participants | Domain-wide delegation: `MEET_AGENT_EMAIL`, `MEET_DWD_SIGNING_SERVICE_ACCOUNT` | 🟢 Built; env-gated (`MEET_PER_VISIT_ENABLED`), shared-room fallback |
| Withings | Scale weight → EHR vitals | OAuth2 (env client secrets), webhook | 🟢 Live (daily 5:30 CT + webhook) |
| Whoop | Strain/sleep/recovery metrics | OAuth2 v2, rotating refresh tokens, webhook | 🔴 Historical f246 implementation is absent from both current core refs |
| Oura | Sleep/activity trends | OAuth2 v2, RFC7009 revoke, webhook | 🟢 Live (daily 5:00 CT) |
| Apple HealthKit (device) | Intended iOS device-push samples → daily rollups | Historical mobile session and batch contract | 🟡 iOS package exists but is not composed; historical Rails ingestion is absent from both current core refs |
| Function Health (+ Cloud Run runner + Chrome extension) | Patient-mediated lab/profile export (Quest-coded results) | Short-lived Firebase token handoff; 5-min encrypted credential TTL; consents | 🟢 Live (file + live paths) |
| PubMed/NCBI E-utilities | Research paper ingestion | `NCBI_API_KEY` | 🟡 Built, `RESEARCH_INGEST_ENABLED` default off |
| LlamaParse / LlamaCloud | PDF→markdown parsing of papers | `LLAMA_CLOUD_API_KEY`; callback `LLAMAPARSE_WEBHOOK_TOKEN` | 🟡 Built, off by default |
| Unpaywall | OA full-text resolution | `UNPAYWALL_EMAIL` | 🟡 Built, off by default |
| Context.dev | Full-text fallback resolver | `CONTEXT_DEV_API_KEY` | 🟡 Built, off by default |
| PostHog | Server-side funnel analytics | `POSTHOG_PROJECT_KEY`; `POSTHOG_API_HOST` (default `https://us.i.posthog.com`) | 🟡 Code-wired; unset project key leaves analytics dark |
| Sentry | Error tracking; release canary tags; scrubber | `SENTRY_RELEASE` | 🟢 Live |
| GCS buckets | Waivers (`GCS_WAIVERS_BUCKET`), ID documents (`GCS_ID_DOCUMENTS_BUCKET`), records import / PhiDocumentStore, research (`GCS_RESEARCH_BUCKET`), leads bucket, OG cards | ADC service accounts; waiver/ID buckets create/get/delete with NO list | 🟢 Live |
| Slack | Observability daily digest | `OBSERVABILITY_SLACK_WEBHOOK_URL` | 🟡 Code-wired; unset webhook skips delivery |
| Google Places autocomplete + Address Validation | Address entry assist + validation (`app/services/address_autocomplete/config.rb`, `app/services/address_autocomplete/google_client.rb`; routes `post address_lookup/suggestions` + `post address_lookup/validate`, config/routes.rb:265-266) | `GOOGLE_ADDRESS_AUTOCOMPLETE_ENABLED` + `GOOGLE_MAPS_API_KEY` | 🟢 Implemented; dark unless both env vars set |
| ICS / Google Calendar template URLs | Patient .ics + doctor feed | Tokenized feed (32-char, revocable) | 🟢 Live |

### Planned integrations not in code

| Target | Intended capability | Code status |
|---|---|---|
| Quest Diagnostics ordering API | Order labs directly | 🔴 Nothing: only `quest_biomarker_code` crosswalk plumbing (§4.2) |
| Concierge phlebotomy vendor | ~$400 at-home draw | 🔴 Nothing; vendor name owed by ops co-founder (§4.3) |
| Real carrier tracking (FedEx API) | Delivery Bot tracking + notifications | 🔴 Nothing: tracking numbers fabricated `VAL-<hex>` (§5.4) |
| ZocDoc-style marketplace | Dr. Picker UX benchmark | 🔴 Nothing: single-clinic roster, no specialty/cost data (§2.1) |

---

## Unwired & legacy review-hold register

Current caller evidence is recorded below, but no permanent deletion is authorized merely because a caller was not found. Search code and history, verify production counts and sample shapes, map dependencies, replacements, and migration, prefer reversible deprecation or export, and obtain product and engineering plus applicable clinical or compliance approval.

| Item | Path | Disposition |
|---|---|---|
| StartCompounding, CompleteCompounding, ReleaseQa, ConfirmShippingReadiness, DispatchShipment, ConfirmDelivery | `app/services/workflows/*.rb` (invoked only from `test/test_helper.rb:165-173` demo journeys) | KEEP/COMPLETE: required §5.2 internal-network infrastructure; add staff UI/jobs, pharmacy-supplied BUD capture, and carrier integration. |
| Workflows::OpenPharmacyOrder | `app/services/workflows/open_pharmacy_order.rb` (EHR issuance creates PharmacyOrder directly, prescribing.rb:461) | REVIEW-HOLD: preserve while reconciling gate accounting, history, and production data. |
| Workflows::ReplyToSupportMessage + CareTeamReplyNudgeJob | Historical f246 paths; absent from both current core refs | REVIEW-HOLD: preserve the intended staff reply and notification behavior for the messaging redesign; do not claim current code substrate. |
| Workflows::StartLinqTakeover / ReleaseLinqTakeover | `app/services/workflows/{start,release}_linq_takeover.rb` | **ACTIVE:** wired via `Ops::Integration::TakeoverGateway` + ops pause/resume routes (config/routes.rb:454-455) and header toggle (§7.4/§12). |
| Workflows::ReleaseLinqEmergencyHold | `app/services/workflows/release_linq_emergency_hold.rb` (doctor/pharmacist-only; no UI) | ACTIVATE: clinician release UI required before emergency-hold volume exists. Keep. |
| Agents::PatientSupport | `app/services/agents/patient_support.rb` (real-LLM, only test caller) | REVIEW-HOLD: map against the newer support lane and messaging redesign before replacement/removal. |
| Agents::HealthRecordImport | `app/services/agents/health_record_import.rb` (stub, zero callers; real path is Records::GeminiPatientRecordExtractor) | REVIEW-HOLD: preserve pending history, production-artifact, and dependency review. |
| Ehr::IntakeReview + `ehr_intake_reviews` | `app/models/ehr/intake_review.rb`; only non-test references are the model, Event eventable allowlist, and PHI manifest | REVIEW-HOLD: verify production rows/samples, history, dependencies, and migration before any deprecation. |
| Ehr::LabImportExportRun subsystem + `ehr_lab_import_export_runs` | `app/models/ehr/lab_import_export_run.rb`; three facade transitions have zero production callers | REVIEW-HOLD: preserve pending production/history review; do not affect active Function Health imports. |
| Workflows::SubmitForDoctorReview | `app/services/workflows/submit_for_doctor_review.rb` (tests only; sole producer of PatientSummary brief + ResearchSynthesis + LabRecommendation artifacts) | ACTIVATE with a production trigger (it is the key to §2.5/§4.1/§9.5) or the three agents it drives stay demo-only. |
| LabOrder write path | `app/models/lab_order.rb` + `lab_orders`/`lab_results` (nothing creates LabOrders; prompt says "Valinor") | KEEP/BUILD: labs are required for pre-appointment care; preserve these records pending final ordering architecture. |
| ehr_state_prescribing_rules table + model | `app/models/ehr/state_prescribing_rule.rb`, db/structure.sql:3906 (no reads; only Event allowlist reference) | REVIEW-HOLD: preserve pending history/data/dependency/migration review. |
| OnCall::Dispatch::ADAPTERS = {} | `app/services/on_call/dispatch.rb` (frozen empty hash; paging_ready? always false) | ACTIVATE: implement at least one paging/call adapter (`ON_CALL_TRANSPORT`): safety-critical gap (§7.5). |
| 10 stub agents (DataNormalization, FormulationFeasibility, HealthRecordImport, Labeling, OperationsDashboard, OutcomeTracking, PatientIntake, PrescriptionCompleteness, QualityGate, SafetyScreen) | `app/services/agents/*.rb` | Review individually: FormulationFeasibility/SafetyScreen run on the live internal-intake path emitting stub payloads (misleading artifacts), and QualityGate will do the same once the dormant compounding chain activates (its only caller is Workflows::CompleteCompounding, §5.3); either implement or remove from those chains. |
| Unmerged worktree | `.worktree-linq-patient-support/` (also `.claude/worktrees/`) | Merge or remove: stale copies pollute grep/codegraph evidence. |
| Retired /start wizard | `get /start => redirect('/')` (routes.rb:20-22) | Keep redirect; wizard code already removed. |

---

## Schema review-hold inventory

**Audit premise:** every spot-checked `ehr_*` column had a consumer in the historical survey: `ehr_patient_profiles` (pregnancy_status 27 refs, lactation 42, substance_use 15, no_current_medications 8, medications_reviewed_at 5, allergies_reviewed_at 11, chart_revision 10), `ehr_medical_histories` (recorded_by_id 8, import_fingerprint 40, code_system 23); all 12 spot-checked `Ehr::` models had consumers. **Do not delete `ehr_*` columns without a current per-column reference and production-data audit.**

Items below are spot-checked, not exhaustive, and not a delete order. Apply the system-wide removal rule above before any deprecation or removal:

| Candidate | Evidence | Recommendation |
|---|---|---|
| `prescriptions.external_id` | 0 app references | REVIEW-HOLD: verify history, production data, integrations, and migration. |
| `prescriptions.expires_at` | No production writer (live issue path omits it; the dormant `attach_ehr_sidecar!` can write it, but no production caller supplies a non-null expiry); at least two readers: `app/services/ops/integration/payment_diagnostics.rb:245` and `app/services/ehr/workflows/prepare_prescription_correction.rb:71` | Do NOT drop: §3.4 (3-month limit) needs it. Start writing it instead. |
| `uploaded_documents.fixture_path` | 0 app references | REVIEW-HOLD: verify history/production data before any migration. |
| `ehr_intake_reviews` | Only model/Event allowlist/PHI manifest plus tests; no production caller found | REVIEW-HOLD: production census/samples, history, dependencies, migration, approvals. |
| `ehr_lab_import_export_runs` | Model/facade transitions exist with zero production callers found | REVIEW-HOLD: preserve pending the same system-wide review. |
| `ehr_state_prescribing_rules` table + `Ehr::StatePrescribingRule` | No consumers found beyond Event allowlist | REVIEW-HOLD: preserve pending the same system-wide review. |
| `prescriptions.doctor_approval_id` | Legacy-but-LIVE: still validated as required unless `issued_from_signed_ehr_encounter?` (prescription.rb:27) | Deprecation candidate, NOT deletable: remove only after the legacy approval path is retired. |
| `lab_orders` / `lab_results` | No write path (§4.1) | KEEP/BUILD: required lab functionality; migrate only if approved architecture replaces them. |
| `programs.template` jsonb | Unused/empty in seeds | REVIEW-HOLD: verify intended program use/history/data before migration. |
| `compounding_tasks.beyond_use_date` | Never written by app code | Do NOT drop: §5.3 displays the pharmacy-supplied value; wire an authorized capture path. |

---

## Database census

The full table-by-table field census lives in **database.md**. It is a frozen historical snapshot of staging @ `f246ed27` with 177 tables. Current main @ `d26d5e1c` and staging @ `c0024874` each contain 168 Rails public tables with identical table lists. The ten mobile and HealthKit tables in the frozen census are absent from both current refs; `suggested_prescriptions` exists on both current refs but post-dates and is absent from the frozen census. Use the census as historical field evidence, not as current branch inventory.

---

## Environment & flag inventory

Consolidated from cluster evidence. "Default" = behavior with the variable unset.

| Env var / flag | Default | Effect |
|---|---|---|
| `LINQ_AGENT_ENABLED` | deployed on | Both production and staging currently route onboarding turns to Flue; reverify the residual flag behavior before changing deployment configuration. |
| `RRX_AGENTS_BASE_URL` / `RRX_AGENTS_USE_IAM` / `RRX_AGENTS_TOKEN` / `RRX_AGENTS_READ_TIMEOUT` | deployed | Rails-to-Flue endpoint, hosted IAM mode or local bearer, and turn read timeout. |
| `LINQ_WELCOME_AUTOREPLY_MODE` | `dry_run` | Must be `send` for welcome SMS to actually deliver. |
| `LINQ_TEXTFLOW_AUTOREPLY_MODE` | `dry_run` | Must be `send` for textflow autoreplies and appointment confirmation + reminder SMS in prod (§2.4). |
| `LINQ_PRE_DOCTOR_INTAKE_REMINDER_MODE` | `dry_run` | `send` enables real pre-doctor intake reminder SMS (send_guard.rb:143-145). |
| `RX_READY_TO_PAY_AUTOREPLY_MODE` | log-only | Must equal `send` or RxReadyToPayNotifyJob logs instead of sending the pay link. |
| `LINQ_FORWARD_SIGNUP` | on | Each captured chat turn forwards answers to the signup pipeline in-process. |
| `LINQ_LINES` | fallback `+15125632172` | Advertised intake line registry. |
| `LINQ_API_TOKEN` / `LINQ_WEBHOOK_SECRET` |: | Linq outbound auth / inbound signature verification. |
| `LINQ_JUDGE_MODEL` | `gpt-5.5` | Nightly judge model. |
| `LINQ_JUDGE_MAX_CONVERSATIONS` | 200 | Nightly judge cost cap (overflow marked truncated). |
| `GLP1_INTAKE_MODE` | `manual_fallback` | Pre-doctor intake activation: `synthetic_rehearsal` / `production_shadow` / `real_patient` (needs governed schema v2); anything else = inactive. |
| `GLP1_INTAKE_KILL_SWITCH` | off | Hard-off for pre-doctor intake regardless of mode. |
| `MEET_PER_VISIT_ENABLED` (+ `MEET_AGENT_EMAIL`, `MEET_DWD_SIGNING_SERVICE_ACCOUNT`) | off | All three required for per-visit Meet rooms; else shared room `RONANRX_VISIT_MEET_URL`. |
| `GOOGLE_EVENTS_*` | inert | Reserved for Workspace Events push; polling only today. |
| `PAYMENT_MODE` | `embedded` | Membership payment surface: embedded Payment Element vs `hosted_checkout`. |
| `STRIPE_SECRET_KEY` / `STRIPE_PUBLISHABLE_KEY` / `STRIPE_WEBHOOK_SECRET` / `STRIPE_PRICE_ID` |: | Membership account keys; price object lives in Stripe. |
| `RX_TX_STRIPE_SECRET_KEY` / `_PUBLISHABLE_KEY` / `_WEBHOOK_SECRET`; `RX_CA_STRIPE_*`; legacy `RX_STRIPE_*` (tx fallback) |: | Per-state Rx account keys, resolved as `RX_#{state}_STRIPE_*` (greps for literal names miss them). |
| `RX_PAYMENT_FALLBACK` | **`true` (on = charging dark)** | Any value but `false` shows "pharmacy will contact you" instead of the Pay button. |
| `RX_FLAT_PRICE_CENTS` | 19_500 | Legacy medication-price fallback override; not target pricing. |
| `EHR_ENABLED` (read by `EhrConfig.enabled?`, app/lib/ehr_config.rb:19; also requires the PHI-ready env flag, Active Record encryption keys, and secret_key_base) | off | Gates the entire `/ehr` route tree (routes.rb:2, 313); dark-launch per environment. |
| `EHR_ENCOUNTER_SIGNING_KEY` / `EHR_ENCOUNTER_SIGNING_KEY_VERSIONS` |: | HMAC e-signing keys; `imported` key version barred from issuing prescriptions. |
| `OPENAI_API_KEY` / `OPENAI_MODEL` | model `gpt-5.5` | All BaseAgent/judge/ranker LLM calls. |
| `VALINOR_DISABLE_REAL_LLM` | off (CI: forced true) | Global real-LLM kill switch. |
| `GEMINI_API_KEY` |: | ID OCR + records extraction. |
| `RESEARCH_INGEST_ENABLED` | **off** | Gates the PubMed ingestion pipeline (manual rake trigger). |
| `RESEARCH_MAX_RESULTS_PER_MED` | 25 | Papers per medication. |
| `RESEARCH_PARSE_MAX_TIER` | `agentic` | LlamaParse spend ceiling; `agentic_plus` (45 credits) only via `rake research:confirm_tier`. |
| `LLAMA_CLOUD_API_KEY` / `LLAMAPARSE_WEBHOOK_TOKEN` / `NCBI_API_KEY` / `UNPAYWALL_EMAIL` / `CONTEXT_DEV_API_KEY` / `GCS_RESEARCH_BUCKET` |: | Research pipeline credentials/buckets. |
| `WHOOP_ENABLED` + `WHOOP_CLIENT_ID` + `WHOOP_CLIENT_SECRET` | historical f246 | Historical configuration; Whoop services are absent from both current core refs. |
| `HEALTHKIT_RAW_RETENTION_DAYS` | historical f246: 180 | Historical raw-sample retention setting; Rails HealthKit sample and retention code is absent from both current core refs. |
| `ON_CALL_TARGETS` / `ON_CALL_TRANSPORT` / `ON_CALL_PAGING_ENABLED` | unset | On-call paging config: moot until an adapter exists (`ADAPTERS = {}`). |
| `FUNCTION_HEALTH_RUNNER_URL` |: | Isolated import runner endpoint. |
| `GCS_WAIVERS_BUCKET` / `GCS_ID_DOCUMENTS_BUCKET` |: | Sealed PHI buckets (no-list). |
| `APP_BASE_URL` / `DEPLOY_ENV` |: | Generate application/pay/appointment link bases; they do not define the SendGuard allowlist. |
| `LINQ_SECURE_LINK_HOST` | `ronanrx.com` | SendGuard secure-link host authority/allowlist. |
| `INTAKE_FORWARD_TOKEN` |: | Bearer for web intake twin (`/api/v1/signups`, `/api/v1/intake_chat`). |
| `DOCTOR_SIGNUP_CODE` | unset ⇒ route 302s | Invite gate for doctor self-signup. |
| `SENTRY_RELEASE` |: | Release canary assertion. |
| `PASSKEYS_ENABLED` | off | Passkey/WebAuthn login gate (read by `PasskeysConfig`, app/lib/passkeys_config.rb:17; also requires `WEBAUTHN_RP_ID`/`WEBAUTHN_ORIGIN`). |
| `GOOGLE_ADDRESS_AUTOCOMPLETE_ENABLED` | off | Enables Google Places address autocomplete + Address Validation (`AddressAutocomplete::Config`, app/services/address_autocomplete/config.rb:11); needs `GOOGLE_MAPS_API_KEY`. |
| `GOOGLE_MAPS_API_KEY` |: | API key for Google Places autocomplete + Address Validation calls (app/services/address_autocomplete/google_client.rb). |
| DB-level dry-run defaults | `dry_run` | `appointment_messages.delivery_mode`, `health_history_nudges.delivery_mode`, `weigh_in_nudges.delivery_mode` all default `'dry_run'` at the DB level; `pre_doctor_intake_reminders.delivery_mode` CHECK `dry_run|send`. Features can be "live" in code but silent in production. |

---

## Recurring job schedule

All current rows come from `config/recurring.yml` unless noted. Timezone America/Chicago (CT) where a clock time is given. Rows marked **historical f246** are absent from both current main and staging and are retained only to explain the frozen census.

| Job | Cadence | Timezone / notes |
|---|---|---|
| `clear_solid_queue_finished_jobs` | every hour at minute 12 | Command entry (`SolidQueue::Job.clear_finished_in_batches`), not a job class |
| `AppointmentReminderJob` (appointment_reminders) | every 15 min | T-24h and T-1h windows |
| `AppointmentMeetSweepJob` | every 15 min | Full Meet lifecycle backstop |
| `AppointmentMeetTranscriptSweepJob` | every 2 min (args [10]) | Fast transcript pass + 10-min note-draft watchdog |
| `OutcomeCheckInJob` | daily `0 9 * * *` | 9:00am CT: check-in dispatch |
| `HealthHistoryNudgeJob` | daily 10:00am | CT |
| `WeighInNudgeJob` | `0 10 * * 1` | Mondays 10:00am CT |
| `OuraDailySyncJob` | daily 5:00am | CT |
| `WhoopDailySyncJob` (historical f246) | daily 5:15am | Absent from both current refs |
| `WithingsSyncJob` | daily 5:30am | CT (+ webhook-triggered) |
| `HealthkitDailyRollupJob` (historical f246) | daily 5:45am | Absent from both current refs |
| `HealthkitSampleRetentionJob` (historical f246) | daily 6:15am | Absent from both current refs |
| `RegimenSyncJob` (historical f246) | `30 2 * * *` | Absent from both current refs |
| `RxCheckoutReconcileJob` | every 15 min | 7-day watermark, batch 200 |
| `RxPaymentLinkRecoveryDispatchJob` | every minute | Staff-requested pay-link recoveries |
| `OnCallEscalationSweepJob` | every minute | Executes due escalation-ladder steps |
| `LinqNightlyJudgeJob` | `0 6 * * *` | 6:00am CT: agent conversation judge |
| `NoteDriftSamplingJob` | `0 7 * * 1` | Mondays 7:00am CT |
| `ObservabilityDigestJob` | daily 5:30am | CT → Slack |
| `FunctionHealthImportReapJob` | every minute | Scheduled in both the production and development recurring blocks (queue `default`); clears stale attempts + encrypted credentials |
| `WebIdUploadRecoveryJob` | every 15 min | Stuck OCR retry |
| `WebIdUploadPurgeJob` | daily 4:45am | CT |
| `PublicIntakePullJob` | every 15 min | GCS leads bucket pull |
| `PublicIntakeSweepJob` | daily 3:00am | CT |
| `TextAuthorizationReconcileJob` | every 15 min | Self-heals missed consent seals |
| `AuthorizationTokenReapJob` | daily 4:00am | CT |
| `LinqRawMediaPurgeJob` | daily 4:30am | CT (`30 4 * * * America/Chicago`) |
| `LoginChallengeReapJob` | every hour at minute 10 | Login-challenge reap |
| `Research::ReconcileParseJob` | every 15 min | Research parse reconcile (pipeline off by default) |
| `DeploymentReleaseCanaryJob` | per deploy | Release-SHA assertion (not recurring.yml) |
| `AppointmentConfirmationJob`, `AppointmentMeetRoomJob`, `LinqReplyJob`, `RxReadyToPayNotifyJob`, `OnCallEscalationJob`, `WebIdOcrJob`, `FunctionHealthImportJob`, `Records::*Job`, `MeetVisitTranscriptIngestJob`, `SealTextAuthorizationJob`, `LinqTherapyHoldReconcileJob`, `Ehr::BreakGlassAlertJob` | event-triggered | Not on the recurring schedule; historical `CareTeamReplyNudgeJob` is absent from both current refs |

**No recurring job exists for:** shipments (no tracking sync), lab review SLA (no pending_review sweep), pre-doctor intake reminders (ops-triggered only), funnel reengagement (§1.6).

---

*End of technical.md. Companions: business.md (behavior), database.md (full field census), and as-built.md (dated implementation evidence). Unresolved external and product items remain explicit. Specification-diff tooling must alert for human review rather than automatically rejecting a merge solely for a missing documentation edit.*
