feature:
Full SOFA Score: Data Layer + Scoring Engine Glasgow Coma Scale: Data Layer + Scoring Engine
This commit is contained in:
@@ -2,11 +2,11 @@
|
||||
|
||||
A production-quality clinical backend built with ASP.NET Core 8, PostgreSQL, Apache Kafka, RabbitMQ, Elasticsearch, Redis, and MinIO. The domain models the observe-alert-acknowledge lifecycle at the center of any clinical monitoring system: patient encounters, continuous vital sign and lab result ingest, real-time sepsis and NEWS2 scoring, and clinician notification with automatic escalation.
|
||||
|
||||
**Implementation status:** Nineteen planned phases are complete — from schema and CRUD through Kafka, Elasticsearch CQRS, sepsis detection, RabbitMQ paging with DLQ escalation, reconciliation jobs, Prometheus/Grafana observability, the MinIO Parquet data lake, clinical data model expansion (patient demographics, encounter enrichment, 12 observation codes), warning alerts and orders, the NEWS2 composite scoring engine, trend detection with alert suppression, qSOFA scoring with sepsis bundle compliance tracking, medication administration with alert correlation annotations, the console replay simulator, the **Vue 3 ward dashboard** (virtual ward, patient detail, alert center, vital sign charts, NEWS2 history, replay controls, alert reasoning), and **clinician feedback mode** (structured alert ratings, feedback summary, JSON/CSV export). See [Implemented Phases](#implemented-phases) for the full breakdown. Guides: [dashboard-guide.md](docs/dashboard-guide.md) (technical), [clinical-testing-guide.md](docs/clinical-testing-guide.md) (doctors & nurses).
|
||||
**Implementation status:** Twenty-one planned phases are complete through Phase 26 — from schema and CRUD through Kafka, Elasticsearch CQRS, sepsis detection, RabbitMQ paging with DLQ escalation, reconciliation jobs, Prometheus/Grafana observability, the MinIO Parquet data lake, clinical data model expansion, warning alerts and orders, the NEWS2 composite scoring engine, trend detection with alert suppression, qSOFA scoring with sepsis bundle compliance tracking, medication administration with alert correlation annotations, the console replay simulator, the **Vue 3 ward dashboard**, clinician feedback mode, **Glasgow Coma Scale (GCS) scoring**, and **SOFA organ-dysfunction scoring with baseline tracking and delta sepsis alerts**. See [Implemented Phases](#implemented-phases) for the full breakdown. Guides: [dashboard-guide.md](docs/dashboard-guide.md) (technical), [clinical-testing-guide.md](docs/clinical-testing-guide.md) (doctors & nurses).
|
||||
|
||||
## Domain Model — How It Maps to a Real Clinical System
|
||||
|
||||
In a hospital, a patient presents for care and an encounter is opened. Bedside monitors and lab systems post observations continuously against that encounter. A rules engine evaluates each observation against configured thresholds and flags abnormal values as clinical alerts. Composite scoring engines (NEWS2, SIRS/sepsis, qSOFA) aggregate multiple vitals into acuity scores. When sepsis is suspected (via SIRS or qSOFA), a four-element treatment bundle is automatically created with a one-hour compliance deadline. Clinicians acknowledge and resolve alerts. If a critical alert goes unacknowledged for five minutes, the system escalates to the on-call backup. All events flow through Kafka so the Elasticsearch dashboard, scoring engines, and data lake writer consume the same stream independently.
|
||||
In a hospital, a patient presents for care and an encounter is opened. Bedside monitors and lab systems post observations continuously against that encounter. A rules engine evaluates each observation against configured thresholds and flags abnormal values as clinical alerts. Composite scoring engines (NEWS2, GCS, SOFA, SIRS/sepsis, qSOFA) aggregate multiple vitals and labs into acuity scores. When sepsis is suspected (via SIRS, qSOFA, or SOFA delta ≥ 2 from baseline), treatment bundles and sepsis alerts are triggered. Clinicians acknowledge and resolve alerts. If a critical alert goes unacknowledged for five minutes, the system escalates to the on-call backup. All events flow through Kafka so the Elasticsearch dashboard, scoring engines, and data lake writer consume the same stream independently.
|
||||
|
||||
```
|
||||
Patient ─────────────────────────── one patient = one MRN, many lifetime encounters
|
||||
@@ -60,11 +60,13 @@ An `OutboxEvent` is written in the same transaction as any observation or alert,
|
||||
- **Clinical Order Management** — `POST /encounters/:id/orders` create; `GET /encounters/:id/orders` list with optional status filter; `GET /orders/:id` detail; `PATCH /orders/:id/status` status transitions; `PATCH /orders/:id/result` record result and transition to `Resulted`; status machine enforces `Pending → InProgress → Resulted` and terminal `Cancelled`
|
||||
- **Clinical Alert Lifecycle** — paginated alert list per encounter and globally; acknowledge with clinician ID and optional note; resolve (must be acknowledged first); global list filterable by status, severity, and department
|
||||
- **Outbox Relay** — `IHostedService` polling every 500ms; reads unprocessed outbox rows, publishes to Kafka, marks processed; partitioned by `encounterId` for per-encounter ordering
|
||||
- **Kafka Pipeline** — three topics (`observation.recorded`, `alert.generated`, `encounter.status.changed`) with six partitions each; KRaft mode, no Zookeeper; `KAFKA_AUTO_CREATE_TOPICS_ENABLE=false` — topics are provisioned explicitly by `KafkaTopicProvisioner`
|
||||
- **Kafka Pipeline** — core topics (`observation.recorded`, `alert.generated`, `encounter.status.changed`, `gcs.scored`, sepsis bundle topics) with six partitions each; KRaft mode, no Zookeeper; `KAFKA_AUTO_CREATE_TOPICS_ENABLE=false` — topics are provisioned explicitly by `KafkaTopicProvisioner` (including `gcs.scored` for SOFA CNS re-scoring)
|
||||
- **Elasticsearch CQRS Projection** — `EsIndexerService` consumer group upserts `patient_encounters` documents, appends to the `observations` index, increments `openAlertCount` on alert events, stamps `news2Score` / `news2RiskLevel` when a NEWS2 alert is generated, and projects `sepsisBundleStatus` / `sepsisBundleElementsCompleted` / `sepsisBundleDeadlineAt` from sepsis bundle events; patient/encounter search; per-encounter observation trend (hourly avg/min/max); alert volume summary by department and severity; population query (numeric range aggregation across all patients)
|
||||
- **Sepsis Early Warning Engine** — `SepsisEngineService` Kafka consumer evaluates both SIRS and qSOFA criteria per encounter using Redis keys with a 30-minute TTL sliding window; SIRS evaluates temperature, heart rate, respiratory rate, and WBC; qSOFA evaluates respiratory rate ≥ 22, systolic BP ≤ 100, and altered mentation (AVPU ≥ 1); on ≥ 2 active criteria in either system, inserts a `SEPSIS_WARNING` or `QSOFA_WARNING / CRITICAL` alert idempotently (`INSERT WHERE NOT EXISTS`); both alert types trigger automatic sepsis bundle creation via `SepsisAlertHandler`
|
||||
- **Sepsis Bundle Compliance** — `SepsisBundleService` creates a four-element treatment bundle (blood cultures, serum lactate, broad-spectrum antibiotics, IV fluid resuscitation) when a SIRS or qSOFA alert fires; each element maps to an auto-created clinical order (`orderedBy: sepsis-bundle-engine`); one-hour compliance deadline from recognition; `OrderService.RecordResult` calls back to `OnOrderResultedAsync` to mark elements complete; final element completion sets bundle to `COMPLIANT` or `NON_COMPLIANT`; `SepsisBundleMonitorService` scans every 5 minutes for overdue in-progress bundles past their deadline and marks them `NON_COMPLIANT`; idempotent — only one in-progress bundle per encounter; `GET /encounters/:id/sepsis-bundle/current` and `GET /sepsis-bundles/:id` expose bundle state; Kafka topics `sepsis.bundle.created` / `sepsis.bundle.updated`; Prometheus `qsofa_detections_total` and `sepsis_bundle_compliance_total`
|
||||
- **NEWS2 Composite Scoring Engine** — `News2ScoringService` Kafka consumer (`news2-scoring`) evaluates seven vital parameters per encounter (`RESP_RATE`, `SPO2`, `SYSTOLIC_BP`, `HEART_RATE`, `AVPU`, `TEMP_C`, `SUPPLEMENTAL_O2`) using Redis keys with a 4-hour TTL; when all seven are present, computes the official NEWS2 aggregate score, persists to `news2_scores`, and creates `NEWS2_WARNING` (score 5–6 or single param = 3) or `NEWS2_EMERGENCY` (score ≥ 7) alerts idempotently; `GET /encounters/:id/news2/current` and `/history` expose score history; Prometheus `news2_scores_total` and `news2_scoring_duration_seconds`
|
||||
- **NEWS2 Composite Scoring Engine** — `News2ScoringService` Kafka consumer (`news2-scoring`) evaluates seven vital parameters per encounter (`RESP_RATE`, `SPO2`, `SYSTOLIC_BP`, `HEART_RATE`, `AVPU`, `TEMP_C`, `SUPPLEMENTAL_O2`) using Redis keys with a 4-hour TTL; when all seven are present, computes the official NEWS2 aggregate score, persists to `news2_scores`, and creates `NEWS2_WARNING` (score 5–6 or single param = 3) or `NEWS2_EMERGENCY` (score ≥ 7) alerts idempotently; consciousness resolves GCS-first with AVPU fallback; `GET /encounters/:id/news2/current` and `/history` expose score history; Prometheus `news2_scores_total` and `news2_scoring_duration_seconds`
|
||||
- **Glasgow Coma Scale (GCS) Scoring** — `GcsScoringService` Kafka consumer (`gcs-scoring`) tracks three components (`GCS_EYE`, `GCS_VERBAL`, `GCS_MOTOR`) in Redis; when all three are present, computes total score and classification (`MILD` / `MODERATE` / `SEVERE`), persists to `gcs_scores`, creates `GCS_CRITICAL` (total ≤ 8) or `GCS_WARNING` (9–12) alerts idempotently, and publishes `gcs.scored` via outbox for downstream SOFA CNS re-scoring; feeds NEWS2 consciousness and qSOFA altered mentation; `GET /encounters/:id/gcs` exposes the latest score; Prometheus `gcs_scores_total`
|
||||
- **SOFA Organ-Dysfunction Scoring** — `SofaScoringService` Kafka consumer (`sofa-scoring`) subscribes to `observation.recorded` and `gcs.scored`; scores six organ systems (respiratory, coagulation, liver, cardiovascular, CNS, renal) from Redis lab cache with carry-forward staleness, MAP derivation, SpO₂/FiO₂ fallback, and vasopressor detection from `MedicationAdministration`; persists to `sofa_scores` with baseline tracking (≥ 4 populated organ systems) and delta-from-baseline; delta ≥ 2 creates `SOFA_SEPSIS` (CRITICAL), delta = 1 creates `SOFA_WARNING`; skips stale Kafka events when the encounter row no longer exists; `GET /encounters/:id/sofa` and `/sofa/history` expose scores; Prometheus `sofa_scores_total` and `sofa_scoring_duration_seconds`
|
||||
- **Trend Detection Engine** — `TrendAnalyzerService` Kafka consumer (`trend-analyzer`) tracks rate-of-change for five vital parameters (`HEART_RATE`, `RESP_RATE`, `SYSTOLIC_BP`, `TEMP_C`, `SPO2`) using Redis sliding-window history; when velocity exceeds configured thresholds (e.g. 72→95 bpm in 30 min), creates a `RAPID_DETERIORATION` alert even if the current value is below warning thresholds; Prometheus `trend_alerts_total` and `trend_analysis_duration_seconds`
|
||||
- **Alert Suppression Windows** — acknowledging a suppressible alert (`WARNING_*`, `NEWS2_WARNING`) sets a Redis key `suppress:{encounterId}:{alertType}` with a configurable TTL (default 30 min from `AlertSuppression` config; optional per-code override via `alert_thresholds.suppression_window_minutes`); `WarningEvaluator` and `News2Detector` check suppression before creating new warning alerts; critical alerts (`CRITICAL_*`, `NEWS2_EMERGENCY`, `SEPSIS_WARNING`, `RAPID_DETERIORATION`) are never suppressed; observations and NEWS2 scores continue to persist during suppression; Prometheus `alert_suppressions_total`
|
||||
- **Medication Administration** — `POST /encounters/:id/medications` records drug administrations (name, dose, route, timestamp, administered-by); `GET /encounters/:id/medications` lists with optional `since` filter; `GET /medications/:id` detail; active-encounter guard; FluentValidation on request DTOs
|
||||
@@ -78,7 +80,7 @@ An `OutboxEvent` is written in the same transaction as any observation or alert,
|
||||
- **Reconciliation Jobs** — three scheduled checks: (1) unacknowledged CRITICAL alerts older than 30 minutes, (2) pending orders without results after 4 hours, (3) active inpatients with no observation in 2 hours; each finding creates a `reconciliation_alerts` row and publishes to RabbitMQ
|
||||
- **Standard Envelope** — all responses use a consistent `{ success, statusCode, data, error }` wrapper; validation errors use the same shape; `ApiBehaviorOptions` overridden so model validation also produces the standard envelope with field-level `details`
|
||||
- **Input Validation** — FluentValidation validators on all request DTOs (patient registration, encounter open, observation ingest, alert acknowledge, alert thresholds, orders); invalid requests return 400 before reaching the service layer
|
||||
- **Observability** — Serilog structured logging enriched with `correlationId`, `encounterId`, `patientId` on alert paths; Seq sink (`http://localhost:5345`); Prometheus (`http://localhost:9101`) scrapes `GET /metrics`; fifteen application metric families via `ClinicalMetrics` and three background collectors (`AlertsUnacknowledgedCollector`, `OutboxPendingCollector`, `KafkaConsumerLagCollector`); Grafana clinical dashboard (`http://localhost:3101`, admin/admin) with `alerts_unacknowledged_gauge` as the primary safety panel; per-request correlation IDs in request logs and `X-Correlation-Id` response headers
|
||||
- **Observability** — Serilog structured logging enriched with `correlationId`, `encounterId`, `patientId` on alert paths; Seq sink (`http://localhost:5345`); Prometheus (`http://localhost:9101`) scrapes `GET /metrics`; application metric families via `ClinicalMetrics` (including GCS and SOFA scoring) and three background collectors (`AlertsUnacknowledgedCollector`, `OutboxPendingCollector`, `KafkaConsumerLagCollector`); Grafana clinical dashboard (`http://localhost:3101`, admin/admin) with `alerts_unacknowledged_gauge` as the primary safety panel; per-request correlation IDs in request logs and `X-Correlation-Id` response headers
|
||||
- **Swagger UI** — OpenAPI spec via Swashbuckle (Development only)
|
||||
|
||||
---
|
||||
@@ -105,6 +107,8 @@ IHostedServices (background):
|
||||
SepsisEngineService → Kafka → Redis SIRS + qSOFA state → PostgreSQL alert → SepsisAlertHandler → SepsisBundleService (consumer group: sepsis-engine)
|
||||
WarningAlertService → Kafka → WarningEvaluator (+ MedicationCorrelationHelper) → PostgreSQL WARNING alert (consumer group: warning-evaluator)
|
||||
News2ScoringService → Kafka → News2Detector (+ MedicationCorrelationHelper) → Redis NEWS2 state → PostgreSQL score + alert (consumer group: news2-scoring)
|
||||
GcsScoringService → Kafka → GcsDetector → Redis GCS components → PostgreSQL gcs_scores + gcs.scored outbox (consumer group: gcs-scoring)
|
||||
SofaScoringService → Kafka (observation.recorded + gcs.scored) → SofaDetector → Redis SOFA lab cache → PostgreSQL sofa_scores + delta alerts (consumer group: sofa-scoring)
|
||||
TrendAnalyzerService → Kafka → TrendDetector → Redis trend history → PostgreSQL RAPID_DETERIORATION alert (consumer group: trend-analyzer)
|
||||
AlertSuppressionService → Redis suppress:{enc}:{type} keys set on acknowledge; read by WarningEvaluator + News2Detector
|
||||
NotificationPublisherService → Kafka → RabbitMQ paging.queue (consumer group: notification-publisher)
|
||||
@@ -162,6 +166,8 @@ VigilCareClinicalAPI/
|
||||
│ ├── AlertsController.cs # Alert list (global + per-encounter), acknowledge, resolve
|
||||
│ ├── OrdersController.cs # Order create, list, get, status transition, record result
|
||||
│ ├── News2Controller.cs # Current NEWS2 score and cursor-paginated history
|
||||
│ ├── GcsController.cs # Latest GCS score per encounter
|
||||
│ ├── SofaController.cs # Current SOFA score and cursor-paginated history
|
||||
│ ├── SepsisBundlesController.cs # Current bundle per encounter, bundle detail by ID
|
||||
│ └── AnalyticsController.cs # Elasticsearch-backed patient search, trend, alert summary, population
|
||||
├── Domains/
|
||||
@@ -173,6 +179,8 @@ VigilCareClinicalAPI/
|
||||
│ │ ├── ClinicalAlert.cs # open → acknowledged → resolved / escalated
|
||||
│ │ ├── Order.cs
|
||||
│ │ ├── News2Score.cs # Composite score with seven component scores + risk level
|
||||
│ │ ├── GcsScore.cs # Eye/verbal/motor components, total, classification
|
||||
│ │ ├── SofaScore.cs # Six organ-system scores, baseline flag, delta, staleness JSON
|
||||
│ │ ├── OutboxEvent.cs # topic + payload JSONB + processed_at
|
||||
│ │ ├── ReconciliationAlert.cs
|
||||
│ │ ├── SepsisBundle.cs # Four-element treatment bundle with 1-hour compliance deadline
|
||||
@@ -205,6 +213,8 @@ VigilCareClinicalAPI/
|
||||
│ ├── AlertSuppressionService.cs # Redis suppress:{enc}:{type} TTL keys
|
||||
│ ├── OrderService.cs # Order lifecycle; status machine; calls SepsisBundleService.OnOrderResultedAsync on result
|
||||
│ ├── News2Service.cs # Current score + cursor-paginated history from PostgreSQL
|
||||
│ ├── GcsService.cs # Latest GCS score from PostgreSQL
|
||||
│ ├── SofaService.cs # Current, baseline, and history SOFA scores
|
||||
│ ├── SepsisBundleService.cs # Bundle creation, element completion, compliance evaluation
|
||||
│ ├── MedicationService.cs # Medication CRUD; GetRecentForEncounterAsync for correlation
|
||||
│ ├── QsofaService.cs # Redis-backed qSOFA criteria count for API/dashboard
|
||||
@@ -219,7 +229,7 @@ VigilCareClinicalAPI/
|
||||
├── Validators/ # FluentValidation — RegisterPatient, OpenEncounter, IngestObservation, CreateMedicationAdministration, …
|
||||
├── Observability/
|
||||
│ └── Metrics/
|
||||
│ └── ClinicalMetrics.cs # Fifteen Prometheus metric families (counters, histograms, gauges)
|
||||
│ └── ClinicalMetrics.cs # Prometheus metric families (counters, histograms, gauges)
|
||||
├── BackgroundServices/
|
||||
│ ├── ThresholdCacheLoader.cs # Pre-loads all thresholds into Redis on startup
|
||||
│ ├── KafkaTopicProvisioner.cs # Creates topics with NumPartitions from config
|
||||
@@ -234,6 +244,8 @@ VigilCareClinicalAPI/
|
||||
│ ├── SepsisEngineService.cs # consumer group: sepsis-engine; SIRS eval via Redis TTL keys
|
||||
│ ├── WarningAlertService.cs # consumer group: warning-evaluator; observation.recorded → WARNING alerts
|
||||
│ ├── News2ScoringService.cs # consumer group: news2-scoring; observation.recorded → NEWS2 score + alert
|
||||
│ ├── GcsScoringService.cs # consumer group: gcs-scoring; observation.recorded → GCS score + alert
|
||||
│ ├── SofaScoringService.cs # consumer group: sofa-scoring; observation.recorded + gcs.scored → SOFA score + delta alerts
|
||||
│ ├── TrendAnalyzerService.cs # consumer group: trend-analyzer; observation.recorded → RAPID_DETERIORATION alert
|
||||
│ ├── SepsisBundleMonitorService.cs # Polls every 5 min; marks overdue in-progress bundles NON_COMPLIANT
|
||||
│ ├── Notifications/
|
||||
@@ -262,6 +274,15 @@ VigilCareClinicalAPI/
|
||||
├── News2/
|
||||
│ ├── News2Calculator.cs # Pure static NEWS2 scoring tables (no I/O)
|
||||
│ └── News2Detector.cs # Redis parameter state, score persistence, alert creation
|
||||
├── Gcs/
|
||||
│ ├── GcsCalculator.cs # GCS total, classification, NEWS2/qSOFA/SOFA mappings
|
||||
│ └── GcsDetector.cs # Redis component state, score persistence, gcs.scored outbox
|
||||
├── Sofa/
|
||||
│ ├── SofaCalculator.cs # Six organ-system SOFA scoring (0–4 each)
|
||||
│ ├── SofaDetector.cs # Lab cache compose, baseline/delta, alert creation
|
||||
│ ├── SofaLabCache.cs # Redis carry-forward with staleness classification
|
||||
│ └── SofaVasopressorResolver.cs # Vasopressor dose from MedicationAdministration + Redis cache
|
||||
├── Services/MapCalculator.cs # MAP from systolic + diastolic BP
|
||||
├── Elasticsearch/Documents/
|
||||
│ ├── PatientEncounterDocument.cs
|
||||
│ ├── ObservationDocument.cs
|
||||
@@ -332,7 +353,9 @@ tests/
|
||||
├── MedicationCorrelationTests.cs # End-to-end warning/NEWS2 annotation with medication context
|
||||
├── MedicationValidationTests.cs # FluentValidation 400 on invalid medication requests
|
||||
├── EncountersListTests.cs # Ward encounter list filters and summary fields
|
||||
└── QsofaCurrentTests.cs # qSOFA current API — Redis state, criteria breakdown
|
||||
├── QsofaCurrentTests.cs # qSOFA current API — Redis state, criteria breakdown
|
||||
├── GcsScoringTests.cs # GCS component scoring, alerts, NEWS2/qSOFA integration paths
|
||||
└── SofaScoringTests.cs # SOFA organ scores, baseline, delta alerts, carry-forward, vasopressors
|
||||
|
||||
VigilCare.Simulator/ # Phase 16 — console replay simulator (HTTP-only, no direct DB/Kafka)
|
||||
├── Program.cs # CLI: replay, replay-all, validate, dry-run
|
||||
@@ -368,7 +391,9 @@ scripts/
|
||||
├── run-phase12-verification.sh # Phase 12 — NEWS2 end-to-end pipeline, API, ES, Prometheus, integration tests
|
||||
├── run-phase13-verification.sh # Phase 13 — trend detection, alert suppression, consumer lag, integration tests
|
||||
├── run-phase14-verification.sh # Phase 14 — qSOFA, sepsis bundle compliance, integration tests
|
||||
└── run-phase15-verification.sh # Phase 15 — medication administration + correlation annotations
|
||||
├── run-phase15-verification.sh # Phase 15 — medication administration + correlation annotations
|
||||
├── run-phase25-verification.sh # Phase 25 — GCS scoring integration tests + manual API checks
|
||||
└── run-phase26-verification.sh # Phase 26 — SOFA scoring integration tests + baseline/delta API checks
|
||||
|
||||
docs/
|
||||
├── plans/ # Phase implementation and verification guides
|
||||
@@ -504,7 +529,7 @@ On startup the application:
|
||||
3. Pre-loads all thresholds into Redis
|
||||
4. Provisions Kafka topics and Elasticsearch indices
|
||||
5. Declares the RabbitMQ exchange and queue topology
|
||||
6. Starts all background consumers (outbox relay, ES indexer, sepsis engine with SIRS + qSOFA, warning evaluator, NEWS2 scoring, trend analyzer, notification workers, data lake writer, reconciliation scheduler, sepsis bundle monitor)
|
||||
6. Starts all background consumers (outbox relay, ES indexer, sepsis engine with SIRS + qSOFA, warning evaluator, NEWS2 scoring, GCS scoring, SOFA scoring, trend analyzer, notification workers, data lake writer, reconciliation scheduler, sepsis bundle monitor)
|
||||
7. Starts Prometheus metric collectors (unacknowledged alerts, outbox pending, Kafka consumer lag)
|
||||
|
||||
Swagger UI is available at `http://localhost:5270/swagger` in Development (API binds to `0.0.0.0:5270` per `launchSettings.json`).
|
||||
@@ -569,6 +594,8 @@ Integration tests use `WebApplicationFactory` with a `Testing` environment and T
|
||||
| `MedicationValidationTests` | 15 | FluentValidation 400 on empty drug name, zero dose, future `administeredAt` |
|
||||
| `EncountersListTests` | — | Ward encounter list — status/department filters, summary fields |
|
||||
| `QsofaCurrentTests` | — | `GET /qsofa/current` — criteria count and breakdown from Redis |
|
||||
| `GcsScoringTests` | 25 | GCS component scoring, classification, alerts, CNS integration with SOFA |
|
||||
| `SofaScoringTests` | 26 | SOFA organ scores, baseline eligibility, delta alerts, carry-forward, vasopressors |
|
||||
|
||||
### Verification Scripts
|
||||
|
||||
@@ -585,6 +612,22 @@ With the API running (`dotnet run`) and Docker Compose up:
|
||||
./scripts/run-phase15-verification.sh # Medication administration + correlation annotation pipeline
|
||||
```
|
||||
|
||||
Phase 25 — GCS scoring (requires running API + Docker Compose; set an active encounter UUID):
|
||||
|
||||
```bash
|
||||
export ENCOUNTER_ID=$(docker compose exec -T postgres psql -U postgres -d vigilcare -t -A \
|
||||
-c "SELECT id FROM encounters WHERE status = 'ACTIVE' LIMIT 1;")
|
||||
./scripts/run-phase25-verification.sh
|
||||
```
|
||||
|
||||
Phase 26 — SOFA scoring (same prerequisites; script polls for async Kafka scoring):
|
||||
|
||||
```bash
|
||||
export ENCOUNTER_ID=$(docker compose exec -T postgres psql -U postgres -d vigilcare -t -A \
|
||||
-c "SELECT id FROM encounters WHERE status = 'ACTIVE' LIMIT 1;")
|
||||
./scripts/run-phase26-verification.sh
|
||||
```
|
||||
|
||||
Phase 13 unit/integration tests only:
|
||||
|
||||
```bash
|
||||
@@ -627,7 +670,7 @@ See `docs/plans/phase-8-plan.md` through `docs/plans/phase-12-plan.md` for manua
|
||||
|
||||
## Prometheus Metrics
|
||||
|
||||
`GET /metrics` exposes fifteen application metric families registered in `ClinicalMetrics`. Three background collectors poll PostgreSQL and Kafka every 30 seconds; counters and histograms are updated inline during request handling and background processing.
|
||||
`GET /metrics` exposes application metric families registered in `ClinicalMetrics`. Three background collectors poll PostgreSQL and Kafka every 30 seconds; counters and histograms are updated inline during request handling and background processing.
|
||||
|
||||
| Metric | Type | Labels | Source |
|
||||
|---|---|---|---|
|
||||
@@ -639,6 +682,9 @@ See `docs/plans/phase-8-plan.md` through `docs/plans/phase-12-plan.md` for manua
|
||||
| `sepsis_bundle_compliance_total` | Counter | `status` | `SepsisBundleService` — on bundle completion (`COMPLIANT`, `NON_COMPLIANT`) |
|
||||
| `news2_scores_total` | Counter | `risk_level` | `News2Detector` — on each persisted score (`LOW`, `MEDIUM`, `HIGH`, …) |
|
||||
| `news2_scoring_duration_seconds` | Histogram | — | `News2Detector` — Redis update through score persistence |
|
||||
| `gcs_scores_total` | Counter | `classification` | `GcsDetector` — on each persisted score (`MILD`, `MODERATE`, `SEVERE`) |
|
||||
| `sofa_scores_total` | Counter | `has_delta_alert` | `SofaDetector` — on each persisted score (`true` / `false`) |
|
||||
| `sofa_scoring_duration_seconds` | Histogram | — | `SofaDetector` — full SOFA compose + persist |
|
||||
| `trend_alerts_total` | Counter | `observation_code` | `TrendDetector` — on each `RAPID_DETERIORATION` alert created |
|
||||
| `trend_analysis_duration_seconds` | Histogram | — | `TrendDetector` — per-observation trend evaluation |
|
||||
| `alert_suppressions_total` | Counter | `alert_type` | `AlertSuppressionService` — on each suppression window set after acknowledge |
|
||||
@@ -915,6 +961,27 @@ The `population` query uses Elasticsearch's numeric range aggregation engine —
|
||||
|
||||
Scores are computed asynchronously by `News2ScoringService` after observations are ingested — allow a few seconds for the Kafka consumer to process all seven parameters before querying.
|
||||
|
||||
### GCS (Glasgow Coma Scale)
|
||||
|
||||
| Method | Path | Description |
|
||||
|---|---|---|
|
||||
| GET | `/encounters/{id}/gcs` | Latest GCS score for an encounter (404 if none computed) |
|
||||
|
||||
**Response** includes `eyeScore`, `verbalScore`, `motorScore`, `totalScore` (3–15), `classification` (`MILD`, `MODERATE`, `SEVERE`), and `calculatedAt`.
|
||||
|
||||
All three components (`GCS_EYE`, `GCS_VERBAL`, `GCS_MOTOR`) must be recorded before a score is computed. Scores are asynchronous via `GcsScoringService`. A completed GCS score publishes `gcs.scored` to Kafka (via outbox) for SOFA CNS re-scoring.
|
||||
|
||||
### SOFA (Sequential Organ Failure Assessment)
|
||||
|
||||
| Method | Path | Description |
|
||||
|---|---|---|
|
||||
| GET | `/encounters/{id}/sofa` | Latest SOFA score for an encounter (404 if none computed) |
|
||||
| GET | `/encounters/{id}/sofa/history` | Cursor-paginated score history |
|
||||
|
||||
**Response** includes `totalScore`, six component scores (`respiratoryScore` … `renalScore`), `isBaseline`, `deltaFromBaseline`, optional `staleness` metadata, and `calculatedAt`.
|
||||
|
||||
SOFA scores are computed asynchronously by `SofaScoringService` from SOFA-related observation codes (`PAO2_MMHG`, `FIO2_PCT`, `PLATELET_K_UL`, `BILIRUBIN_MG_DL`, `CREATININE_MG_DL`, `URINE_OUTPUT_ML_H`, vitals, `SPO2`, vasopressors) and from `gcs.scored` events (CNS organ system). Baseline is established when ≥ 4 of 6 organ systems have available data. Delta ≥ 2 from baseline creates a `SOFA_SEPSIS` alert; delta = 1 creates `SOFA_WARNING`. Poll `/sofa/history` to confirm baseline — the latest score may have `isBaseline: false` after subsequent observations.
|
||||
|
||||
### Sepsis Bundles
|
||||
|
||||
| Method | Path | Description |
|
||||
@@ -1057,6 +1124,43 @@ calculatedAt DateTimeOffset
|
||||
|
||||
Indexes: `(encounter_id, calculated_at DESC)`, `(patient_id, calculated_at DESC)`
|
||||
|
||||
### GcsScore
|
||||
|
||||
```
|
||||
id Guid PK
|
||||
encounterId Guid FK → Encounter
|
||||
patientId Guid FK → Patient
|
||||
eyeScore int 1–4
|
||||
verbalScore int 1–5
|
||||
motorScore int 1–6
|
||||
totalScore int 3–15
|
||||
classification string MILD | MODERATE | SEVERE
|
||||
calculatedAt DateTimeOffset
|
||||
```
|
||||
|
||||
Indexes: `(encounter_id, calculated_at DESC)`
|
||||
|
||||
### SofaScore
|
||||
|
||||
```
|
||||
id Guid PK
|
||||
encounterId Guid FK → Encounter
|
||||
patientId Guid FK → Patient
|
||||
totalScore int sum of six components (0–24)
|
||||
respiratoryScore int 0–4
|
||||
coagulationScore int 0–4
|
||||
liverScore int 0–4
|
||||
cardiovascularScore int 0–4
|
||||
cnsScore int 0–4
|
||||
renalScore int 0–4
|
||||
isBaseline bool true for admission baseline row
|
||||
deltaFromBaseline int? current total minus baseline total
|
||||
stalenessFlags jsonb? stale/missing components, SpO2 fallback flag
|
||||
calculatedAt DateTimeOffset
|
||||
```
|
||||
|
||||
Indexes: `(encounter_id, calculated_at DESC)`, partial `(encounter_id) WHERE is_baseline = true`
|
||||
|
||||
### Order
|
||||
|
||||
```
|
||||
@@ -1229,13 +1333,14 @@ Exchange: `clinical.notifications.exchange` (direct)
|
||||
|
||||
| Topic | Partition key | Consumer groups |
|
||||
|---|---|---|
|
||||
| `observation.recorded` | `encounterId` | `es-indexer`, `sepsis-engine`, `warning-evaluator`, `news2-scoring`, `trend-analyzer`, `data-lake-writer` |
|
||||
| `observation.recorded` | `encounterId` | `es-indexer`, `sepsis-engine`, `warning-evaluator`, `news2-scoring`, `gcs-scoring`, `sofa-scoring`, `trend-analyzer`, `data-lake-writer` |
|
||||
| `alert.generated` | `encounterId` | `es-indexer`, `notification-publisher`, `data-lake-writer` |
|
||||
| `encounter.status.changed` | `encounterId` | `es-indexer`, `data-lake-writer` |
|
||||
| `gcs.scored` | `encounterId` | `sofa-scoring` |
|
||||
| `sepsis.bundle.created` | `encounterId` | `es-indexer` |
|
||||
| `sepsis.bundle.updated` | `encounterId` | `es-indexer` |
|
||||
|
||||
All topics use 6 partitions. `KAFKA_AUTO_CREATE_TOPICS_ENABLE=false` — topics are provisioned explicitly by `KafkaTopicProvisioner` to guarantee correct partition count.
|
||||
All topics use 6 partitions. `KAFKA_AUTO_CREATE_TOPICS_ENABLE=false` — topics (including `gcs.scored`) are provisioned explicitly by `KafkaTopicProvisioner` on API startup.
|
||||
|
||||
**`alert.generated` payload (minimum fields for downstream consumers):**
|
||||
|
||||
@@ -1413,7 +1518,7 @@ Observation history uses cursor pagination on `(recorded_at DESC, id DESC)`. Off
|
||||
|
||||
## Implemented Phases
|
||||
|
||||
Nineteen phases from the project roadmap are implemented and verified. Integration tests (`dotnet test` — 116 test methods) and per-phase verification scripts cover Phases 8–15. Phases 17–19 add the Vue dashboard and clinician feedback (Vitest in `vigilcare-dashboard/`).
|
||||
Nineteen core phases from the project roadmap are implemented and verified, plus **Phase 25 (GCS)** and **Phase 26 (SOFA)**. Integration tests (`dotnet test`) and per-phase verification scripts cover Phases 8–15, 25, and 26. Phases 17–19 add the Vue dashboard and clinician feedback (Vitest in `vigilcare-dashboard/`).
|
||||
|
||||
| Phase | Feature | Status |
|
||||
|---|---|---|
|
||||
@@ -1436,7 +1541,11 @@ Nineteen phases from the project roadmap are implemented and verified. Integrati
|
||||
| 17 | Ward dashboard shell — Vue 3 + Vite + Pinia + Tailwind; virtual ward table (NEWS2-sorted, department filter); patient detail (vitals, scores, alerts, orders, sepsis bundle); alert center (global acknowledge/resolve); API polling; CORS-backed `GET /encounters` ward list | Done |
|
||||
| 18 | Clinical review mode — Chart.js vital sign trends (5 charts), NEWS2 history chart, local replay controls, alert reasoning panel, medication context on alerts; `fetchNews2History` / `fetchMedications`; Vitest composable and component tests; `docs/dashboard-guide.md` | Done |
|
||||
| 19 | Clinician feedback mode — six rating buttons per alert, optional notes, Feedback Summary with aggregate stats, JSON/CSV export, localStorage persistence; `docs/clinical-testing-guide.md` for doctor/nurse evaluation sessions | Done |
|
||||
| 25 | Glasgow Coma Scale — `GcsCalculator`, `GcsDetector`, `GcsScoringService`; `gcs_scores` table; `GCS_CRITICAL` / `GCS_WARNING` alerts; `gcs.scored` outbox topic; NEWS2 consciousness GCS-first; qSOFA altered mentation sync; `GcsController`; Prometheus `gcs_scores_total`; `GcsScoringTests`; `run-phase25-verification.sh` | Done |
|
||||
| 26 | SOFA scoring — `SofaCalculator`, `SofaDetector`, `SofaLabCache`, `SofaVasopressorResolver`, `SofaScoringService`; `sofa_scores` table with baseline + delta; `SOFA_SEPSIS` / `SOFA_WARNING` alerts; six new observation codes; `SofaController`; Kafka topic `gcs.scored` provisioned for CNS re-score; stale-encounter guard for Kafka replay; Prometheus `sofa_scores_total`; `SofaScoringTests`; `run-phase26-verification.sh` | Done |
|
||||
|
||||
**Ward dashboard:** backend APIs (`GET /encounters` ward list, `GET /qsofa/current`, CORS) and frontend SPA — `EncountersListTests`, `QsofaCurrentTests`, `vigilcare-dashboard` Vitest suite (41 tests: replay scrubbing, feedback store, FeedbackButtons, FeedbackSummary, alert components, charts, ward table).
|
||||
|
||||
**Scoring pipeline (Phases 25–26):** GCS components → `gcs_scores` + `gcs.scored` → SOFA CNS organ system; SOFA lab/vital observations → `sofa_scores` with baseline tracking → delta sepsis alerts when organ dysfunction worsens.
|
||||
|
||||
**Optional follow-up:** execute and document the Kafka replay demonstration for the data lake (reset `data-lake-writer` offsets, clear MinIO prefixes, restart API, confirm Parquet rebuild). See `docs/plans/phase-9-plan.md` § Replay demonstration.
|
||||
|
||||
Reference in New Issue
Block a user