|
| 1 | +# agents.md — RCPCH Audit Engine: Agent Orientation |
| 2 | + |
| 3 | +> This file is intended as a growing reference for AI agents and developers working on this codebase. It will be expanded over time to cover each major area of the project. |
| 4 | +
|
| 5 | +--- |
| 6 | + |
| 7 | +## Project Overview |
| 8 | + |
| 9 | +**rcpch-audit-engine** is the backend and web application for **Epilepsy12**, a national clinical audit run by the Royal College of Paediatrics and Child Health (RCPCH). It collects, validates, and reports on epilepsy care for children across England and Wales. |
| 10 | + |
| 11 | +- **Framework**: Django (Python) |
| 12 | +- **Database**: PostgreSQL with the PostGIS extension (spatial queries) |
| 13 | +- **Deployment target**: Azure Container Apps |
| 14 | +- **Container registry**: Azure Container Registry (ACR) |
| 15 | +- **Reverse proxy**: Caddy (handles HTTPS) |
| 16 | +- **Task queue**: Celery (celerybeat for scheduled tasks) |
| 17 | +- **Primary app**: `epilepsy12/` — all audit domain logic lives here |
| 18 | +- **Documentation**: MkDocs site, served via a separate Docker Compose service and built into the image at deploy time |
| 19 | +- **Full docs**: https://e12.rcpch.ac.uk/docs |
| 20 | + |
| 21 | +The main Django project config is in `rcpch-audit-engine/` (the inner directory), including `settings.py`, `urls.py`, `logging_settings.py`, and `build_info.py`. |
| 22 | + |
| 23 | +--- |
| 24 | + |
| 25 | +## The `s/` Scripts Directory |
| 26 | + |
| 27 | +All developer and CI operations are driven by short shell scripts in `s/`. These exist to reduce typing, reduce errors, and ensure consistency. Scripts are plain bash; make them executable with `chmod +x s/<script>` if needed. |
| 28 | + |
| 29 | +| Script | Purpose | |
| 30 | +|---|---| |
| 31 | +| `s/up` | `docker compose up` — starts all services (caddy, django, postgis, mkdocs) | |
| 32 | +| `s/down` | `docker compose down` — stops services, does **not** destroy volumes or images | |
| 33 | +| `s/rebuild` | Destroys containers and images then calls `s/up` (runs `s/remove-containers-and-images` then `s/up`) | |
| 34 | +| `s/remove-containers-and-images` | Removes local containers and images without touching volumes | |
| 35 | +| `s/DELETE-LOCAL-DATA` | **Destructive** — prompts for confirmation, then runs `docker compose down -v --rmi local` removing volumes too. Never run on live/production. | |
| 36 | +| `s/start-dev` | Django entrypoint for development: `collectstatic`, `migrate`, seed groups/permissions, create dev users, then `runserver` | |
| 37 | +| `s/start-prod` | Django entrypoint for production | |
| 38 | +| `s/start-test` | Django entrypoint used during test runs: `collectstatic` then sleeps (keeps container alive for pytest) | |
| 39 | +| `s/seed` | Seeds 200 cases and registrations into a running django container via `manage.py seed` | |
| 40 | +| `s/test` | Runs `pytest -v` inside the running django container; passes all extra args through (e.g. `-m slow`) | |
| 41 | +| `s/pr-check` | Used in CI on PRs: spins up compose with `start-test`, runs `not slow` then `slow` test markers, tears down | |
| 42 | +| `s/ci` | Full deployment pipeline script (see CI section below) | |
| 43 | +| `s/logs` | Tails all compose service logs with timestamps | |
| 44 | +| `s/psql` | Opens a psql shell inside the postgis container | |
| 45 | +| `s/django-shell` | Opens a Django shell inside the django container | |
| 46 | +| `s/create-superuser` | Creates a Django superuser inside the running container | |
| 47 | +| `s/get-build-info` | Writes git metadata (hash, branch, etc.) to `build_info.json` for the build info page | |
| 48 | +| `s/push-envs-github-secret` | Pushes environment secrets to GitHub Actions secrets | |
| 49 | +| `s/trust-caddy-ca` | Trusts Caddy's local CA certificate for local HTTPS development | |
| 50 | +| `s/restart` | Restarts compose services | |
| 51 | + |
| 52 | +The `DJANGO_START_COMMAND` environment variable controls which start script the django container runs. It defaults to `s/start-dev`; CI overrides it to `s/start-test` when running tests. |
| 53 | + |
| 54 | +--- |
| 55 | + |
| 56 | +## CI / Deployment Pipeline |
| 57 | + |
| 58 | +### GitHub Actions Workflows |
| 59 | + |
| 60 | +| Workflow file | Trigger | Purpose | |
| 61 | +|---|---|---| |
| 62 | +| `run-docker-compose-test-on-pr.yml` | PR to any branch | Runs the full pytest suite via `s/pr-check` | |
| 63 | +| `deploy.yml` | Push to `live` branch | Full build, test, and deploy to Azure via `s/ci` | |
| 64 | +| `staging_e12-staging-web-app-service.yml` | (see file) | Staging App Service deployment | |
| 65 | +| `auto-add-issues-to-project.yml` | Issue events | Automatically adds issues to the GitHub Project board | |
| 66 | + |
| 67 | +### The `s/ci` Deployment Script (called by `deploy.yml`) |
| 68 | + |
| 69 | +This is the authoritative deploy sequence executed on every push to `live`: |
| 70 | + |
| 71 | +1. **Login to Azure ACR** — `az acr login` |
| 72 | +2. **Download `.env` from Azure File Share** — production secrets are stored in Azure Storage, not in the repo |
| 73 | +3. **Burn in build info** — `s/get-build-info` writes git metadata to `build_info.json` |
| 74 | +4. **Build the Docker image** — `docker compose build` |
| 75 | +5. **Build the MkDocs documentation** — runs inside the image; docs are embedded into the static files |
| 76 | +6. **Rebuild the image** — a second build to embed the freshly built docs |
| 77 | +7. **Tag and push to ACR** — tagged with the Git SHA: `<registry>.azurecr.io/e12-django:<SHA>` |
| 78 | +8. **Run tests** — `s/test -m 'not slow'` then `s/test -m 'slow'` against a local Postgres container |
| 79 | +9. **Deploy to staging** — `az containerapp revision copy` creates a new revision on the staging Container App |
| 80 | +10. **Deploy to production** — same command targets the live Container App |
| 81 | + |
| 82 | +> Note: the image is pushed to ACR **before** tests run, so that an emergency deploy is possible from a known-good SHA even if tests are mid-flight. |
| 83 | +
|
| 84 | +### Authentication to Azure |
| 85 | + |
| 86 | +The GitHub Actions workflow uses OIDC (`id-token: write` permission) with Azure federated credentials — no long-lived secrets for the Azure login itself. Remaining secrets (registry name, resource group, app names, storage account, etc.) are stored as GitHub Actions secrets and injected as environment variables into `s/ci`. |
| 87 | + |
| 88 | +--- |
| 89 | + |
| 90 | +## Docker Compose Services |
| 91 | + |
| 92 | +| Service | Image / Build | Role | |
| 93 | +|---|---|---| |
| 94 | +| `caddy` | `caddy` (official) | Reverse proxy, TLS termination, serves static docs | |
| 95 | +| `django` | `e12-django:built` (local build) | Main Django application | |
| 96 | +| `postgis` | `postgis/postgis:15-3.3` | PostgreSQL + PostGIS | |
| 97 | +| `mkdocs` | `e12-django:built` | Builds and optionally serves the MkDocs documentation | |
| 98 | + |
| 99 | +All services share environment from `envs/.env` (not committed to git). Two named volumes are used: `caddy-data` and `postgis-data`. |
| 100 | + |
| 101 | +--- |
| 102 | + |
| 103 | +## IMD Calculation — Design Notes |
| 104 | + |
| 105 | +### Background |
| 106 | + |
| 107 | +The **Index of Multiple Deprivation (IMD)** quintile is stored on `Case.index_of_multiple_deprivation_quintile`. The correct IMD year to use depends on the patient's **cohort**: |
| 108 | + |
| 109 | +- Cohort < 8 → 2019 IMD (England 2019 / Wales 2019, based on 2011 LSOA boundaries) |
| 110 | +- Cohort ≥ 8 → 2025 IMD (England 2025, based on 2021 LSOA boundaries; Wales still 2019) |
| 111 | + |
| 112 | +The cohort is stored on `Registration` and is derived from `Registration.first_paediatric_assessment_date`. The RCPCH Census Platform API was updated to **v2**, which now accepts a `year` parameter (`2019` or `2025`) in the IMD endpoint. |
| 113 | + |
| 114 | +### The problem with putting IMD in `Case.save()` |
| 115 | + |
| 116 | +`Case` and `Registration` have a 1-to-1 relation, but they can be created in either order. If IMD is calculated inside `Case.save()`, the Registration (and therefore the cohort) may not exist yet on first save, making it impossible to know the correct year. Workarounds inside `save()` grow complexity and can cause `ValueError` when filtering on an unsaved instance. |
| 117 | + |
| 118 | +### Current design: signal-driven utility |
| 119 | + |
| 120 | +IMD is calculated in a **single utility function** and triggered by **`post_save` signals** on both models: |
| 121 | + |
| 122 | +``` |
| 123 | +epilepsy12/general_functions/index_multiple_deprivation.py |
| 124 | + └── recalculate_imd_for_case(case) |
| 125 | + - no-op if postcode missing or unknown |
| 126 | + - no-op if Registration or cohort not yet available |
| 127 | + - derives imd_year from registration.cohort |
| 128 | + - calls imd_for_postcode(postcode, year=imd_year) once |
| 129 | + - persists via queryset .update() to avoid re-triggering Case.save() |
| 130 | +``` |
| 131 | + |
| 132 | +``` |
| 133 | +epilepsy12/signals.py |
| 134 | + ├── pre_save / post_save on Case |
| 135 | + │ → fires recalculate_imd_for_case when postcode changes |
| 136 | + └── pre_save / post_save on Registration |
| 137 | + → fires recalculate_imd_for_case when first_paediatric_assessment_date changes |
| 138 | +``` |
| 139 | + |
| 140 | +`Case.save()` itself only normalises the postcode (strip spaces/dashes, uppercase) and updates geolocation coordinates (`location_wgs84`, `location_bng`). It sets `index_of_multiple_deprivation_quintile = None` when postcode switches to an unknown/placeholder value. |
| 141 | + |
| 142 | +### Bulk recalculation |
| 143 | + |
| 144 | +To recalculate IMD for existing records (e.g. after a cohort boundary change or API update): |
| 145 | + |
| 146 | +```bash |
| 147 | +s/recalculate-imd # all cohorts |
| 148 | +s/recalculate-imd 6 # cohort 6 only |
| 149 | +``` |
| 150 | + |
| 151 | +This wraps `manage.py recalculate_imd --all` / `--cohort N` (`epilepsy12/management/commands/recalculate_imd.py`). |
| 152 | + |
| 153 | +### Key constants |
| 154 | + |
| 155 | +| Setting | Location | |
| 156 | +|---|---| |
| 157 | +| `RCPCH_CENSUS_PLATFORM_URL` | `settings.py` / `.env` | |
| 158 | +| `RCPCH_CENSUS_PLATFORM_TOKEN` | `settings.py` / `.env` | |
| 159 | +| `UNKNOWN_POSTCODES_NO_SPACES` | `epilepsy12/constants/postcodes.py` | |
| 160 | + |
| 161 | +--- |
| 162 | + |
| 163 | +## Areas to Expand |
| 164 | + |
| 165 | +The following sections will be added over time: |
| 166 | + |
| 167 | +- `epilepsy12/` app structure (models, views, forms, KPIs, migrations) |
| 168 | +- `epilepsy12/models_folder/` — domain model breakdown |
| 169 | +- `epilepsy12/views/` — view organisation and HTMX patterns |
| 170 | +- `epilepsy12/constants/` — audit constants and clinical coding |
| 171 | +- `epilepsy12/management/commands/` — custom management commands including `seed` |
| 172 | +- `epilepsy12/tests/` — test structure and pytest markers |
| 173 | +- `epilepsy12/common_view_functions/` — shared view logic |
| 174 | +- KPI calculation logic (`kpi.py`, `organisational_audit.py`) |
| 175 | +- Permissions and decorator patterns |
| 176 | +- Celery / celerybeat scheduled tasks |
| 177 | +- REST API (serializers, DRF) |
| 178 | +- Template and HTMX patterns |
0 commit comments