From 25fdcc1e490fee1d11a694f80d84e09db0e007f1 Mon Sep 17 00:00:00 2001 From: voltsrage Date: Sun, 21 Jun 2026 05:13:25 +0800 Subject: [PATCH] chore: update readme --- README.md | 187 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 98 insertions(+), 89 deletions(-) diff --git a/README.md b/README.md index 0c3e8ca..f4795ce 100644 --- a/README.md +++ b/README.md @@ -2,20 +2,20 @@ 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:** 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). +**Implementation status:** Twenty-four planned phases are complete through Phase 29 — 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 bedside screening, medication administration with alert correlation annotations, the console replay simulator, the **Vue 3 ward dashboard**, clinician feedback mode, **Glasgow Coma Scale (GCS) scoring**, **SOFA organ-dysfunction scoring with baseline tracking and delta sepsis alerts**, the **Sepsis-3 clinical refactor** (SIRS removed, qSOFA repositioned as screening, SOFA delta ≥ 2 triggers bundles), **frontend GCS entry and SOFA display**, and **expanded simulator scenarios with clinical validation**. 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, 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. +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, qSOFA) aggregate multiple vitals and labs into acuity scores. The sepsis pathway follows Sepsis-3 consensus: qSOFA ≥ 2 creates a bedside screening alert recommending SOFA labs; when SOFA delta ≥ 2 from baseline confirms organ dysfunction, a `SOFA_SEPSIS` alert triggers the treatment bundle. 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 └── Encounter one clinical episode (inpatient, outpatient, ED) ├── Observation one measurement: vital sign, lab value, SpO₂ │ └── OutboxEvent written in the same transaction → relayed to Kafka - └── ClinicalAlert generated on threshold breach, SIRS detection, qSOFA score, or NEWS2 composite score + └── ClinicalAlert generated on threshold breach, qSOFA screen, SOFA delta, or NEWS2 composite score ├── OutboxEvent → Kafka → RabbitMQ → clinician page → escalation - └── SepsisBundle auto-created on SIRS/qSOFA alert → four treatment orders → compliance tracking + └── SepsisBundle auto-created on SOFA_SEPSIS alert → four treatment orders → compliance tracking ``` ### Patient @@ -38,11 +38,11 @@ An `idempotencyKey` (partial unique index) prevents duplicate observations when ### ClinicalAlert -A `ClinicalAlert` is generated when an observation breaches a threshold, when the sepsis engine detects two or more concurrent SIRS criteria, when the qSOFA engine detects two or more organ-dysfunction criteria, or when the NEWS2 engine computes a medium/high-risk composite score (or a single-parameter score of 3). Lifecycle: `open → acknowledged → resolved` (or `escalated` after a five-minute NACK cycle through the RabbitMQ dead-letter queue). Alerts carry an audit trail: who acknowledged, when, and with what note. SIRS and qSOFA alerts additionally trigger automatic sepsis bundle creation. +A `ClinicalAlert` is generated when an observation breaches a threshold, when the qSOFA engine detects two or more organ-dysfunction criteria (screening), when SOFA delta ≥ 2 from baseline confirms sepsis, or when the NEWS2 engine computes a medium/high-risk composite score (or a single-parameter score of 3). Lifecycle: `open → acknowledged → resolved` (or `escalated` after a five-minute NACK cycle through the RabbitMQ dead-letter queue). Alerts carry an audit trail: who acknowledged, when, and with what note. Only `SOFA_SEPSIS` alerts trigger automatic sepsis bundle creation. ### SepsisBundle -A `SepsisBundle` is created automatically when either the SIRS detector or the qSOFA scorer generates a sepsis-related alert. Each bundle contains four mandatory treatment elements (blood cultures, serum lactate, broad-spectrum antibiotics, IV fluid resuscitation) mapped to clinical orders that are created simultaneously. A one-hour compliance deadline is set from the recognition time. As linked orders are resulted, bundle elements transition to `COMPLETED`; when all four are done, the bundle is marked `COMPLIANT` (within deadline) or `NON_COMPLIANT` (past deadline). Only one in-progress bundle can exist per encounter at a time. +A `SepsisBundle` is created automatically when the SOFA scoring engine detects a delta ≥ 2 from baseline (`SOFA_SEPSIS` alert). Each bundle contains four mandatory treatment elements (blood cultures, serum lactate, broad-spectrum antibiotics, IV fluid resuscitation) mapped to clinical orders that are created simultaneously. A one-hour compliance deadline is set from the recognition time. As linked orders are resulted, bundle elements transition to `COMPLETED`; when all four are done, the bundle is marked `COMPLIANT` (within deadline) or `NON_COMPLIANT` (past deadline). Only one in-progress bundle can exist per encounter at a time. ### OutboxEvent @@ -60,10 +60,10 @@ 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** — 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` +- **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 and `sofa.scored` for downstream consumers) +- **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 SOFA-triggered 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 Screening Engine** — `SepsisEngineService` Kafka consumer evaluates qSOFA criteria per encounter using Redis keys with a 30-minute TTL sliding window; qSOFA evaluates respiratory rate ≥ 22, systolic BP ≤ 100, and altered mentation (GCS < 15 or AVPU ≥ 1); on ≥ 2 active criteria and no open screening alert, inserts a `QSOFA_SCREEN` (WARNING-level) alert idempotently (`INSERT WHERE NOT EXISTS`); qSOFA screening recommends ordering SOFA labs — definitive sepsis detection and bundle triggering are handled by `SofaScoringService` via SOFA delta ≥ 2 +- **Sepsis Bundle Compliance** — `SepsisBundleService` creates a four-element treatment bundle (blood cultures, serum lactate, broad-spectrum antibiotics, IV fluid resuscitation) when a `SOFA_SEPSIS` alert fires (delta ≥ 2 from baseline); 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; 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` @@ -72,9 +72,9 @@ An `OutboxEvent` is written in the same transaction as any observation or alert, - **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 - **Medication Correlation Annotations** — `MedicationCorrelationHelper` appends medication context to warning and NEWS2 alert details when a mapped drug was administered within the correlation window (default 90 min); drug-to-vital mappings in `MedicationCorrelation` config (`appsettings.json`); annotates rather than suppresses — alerts still fire; sepsis, trend, and critical sync-path alerts are never annotated; design rationale in `docs/decisions/medication-correlation-design.md` - **Ward Dashboard APIs** — `GET /encounters` returns paginated `WardEncounterSummary` rows (patient name/MRN, room/bed, department, status, latest NEWS2 score, live qSOFA criteria count from Redis, sepsis bundle status, open alert count); filterable by `status` and `department`; `GET /encounters/:id/qsofa/current` exposes Redis-backed qSOFA state; CORS policy `Dashboard` allows configured origins (default `http://localhost:5173`) -- **Ward Dashboard Frontend** — Vue 3 SPA (`vigilcare-dashboard/`) with virtual ward table (NEWS2-sorted), patient detail (vitals, scores, alerts, orders, sepsis bundle), alert center (global acknowledge/resolve), vital sign trend charts with local replay scrubbing, NEWS2 history chart, alert reasoning with optional medication context, and clinician feedback on every alert; polls API every 5–10 s; guides in `docs/dashboard-guide.md` and `docs/clinical-testing-guide.md` +- **Ward Dashboard Frontend** — Vue 3 SPA (`vigilcare-dashboard/`) with virtual ward table (NEWS2-sorted), patient detail (vitals, scores, alerts, orders, sepsis bundle, GCS entry form, SOFA score panel), alert center (global acknowledge/resolve), vital sign trend charts with local replay scrubbing, NEWS2 history chart, alert reasoning with optional medication context, and clinician feedback on every alert; polls API every 5–10 s; guides in `docs/dashboard-guide.md` and `docs/clinical-testing-guide.md` - **Clinician Feedback Mode** — six quick ratings per alert (useful, too early, too late, false positive, missing context, would act); optional notes; Feedback Summary with aggregate stats and JSON/CSV export; client-side persistence for product research -- **Console Replay Simulator** — standalone `VigilCare.Simulator` .NET console app replays JSON scenario files against the live API with configurable speed (`--speed 0` instant, `60` = 60× faster); commands: `replay`, `replay-all`, `validate`, `dry-run`; optional `--poll` shows alerts, NEWS2, and sepsis bundle state during replay; eight sample scenarios in `VigilCare.Simulator/Scenarios/List/`; user guide in `docs/simulator-guide.md` +- **Console Replay Simulator** — standalone `VigilCare.Simulator` .NET console app replays JSON scenario files against the live API with configurable speed (`--speed 0` instant, `60` = 60× faster); commands: `replay`, `replay-all`, `validate`, `dry-run`; optional `--poll` shows alerts, NEWS2, GCS, SOFA, and sepsis bundle state during replay; eleven sample scenarios in `VigilCare.Simulator/Scenarios/List/` (including GCS neurological decline, SOFA sepsis progression, and SpO₂/FiO₂ fallback); user guide in `docs/simulator-guide.md` - **RabbitMQ Notification Workers** — `NotificationPublisherService` reads `alert.generated` from Kafka and publishes paging jobs to `alerts.paging.queue`; `PagingWorkerService` sends the page and waits for acknowledgment; if no ack arrives before timeout it NACKs to `alerts.paging.dlq` with `x-message-ttl = 300000ms`; if the host is stopping, in-flight paging messages are NACKed with `requeue=true` so they are retried after restart and do not false-escalate; `EscalationWorkerService` pages the on-call backup and sets alert status to `escalated`; `DischargeSummaryWorkerService` reads `encounter.status.changed`, generates a discharge summary, and stores it in MinIO under `/discharge-summaries/{encounterId}/summary.pdf` - **Data Lake Writer** — `DataLakeWriterService` (consumer group `data-lake-writer`) buffers `observation.recorded`, `alert.generated`, and `encounter.status.changed` events, flushes date-partitioned Parquet files to MinIO (`/observations/`, `/alerts/`, `/encounters/`), and commits Kafka offsets only after at least one successful upload; shutdown flush uses an uncanceled token so MinIO writes complete on Ctrl+C; `kafka_partition` and `kafka_offset` columns provide audit lineage - **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 @@ -94,7 +94,7 @@ HTTP request → Controllers → Services ├── PostgreSQL (EF Core — writes, keyed reads) - ├── Redis (threshold cache, SIRS state, NEWS2 parameter state, trend history, alert suppression keys) + ├── Redis (threshold cache, qSOFA state, NEWS2 parameter state, GCS state, SOFA lab cache, trend history, alert suppression keys) └── OutboxEvent (same transaction as domain write) IHostedServices (background): @@ -104,13 +104,13 @@ IHostedServices (background): ElasticIndexProvisioner → creates index mappings OutboxRelayService → PostgreSQL outbox → Kafka (every 500ms) EsIndexerService → Kafka → Elasticsearch (consumer group: es-indexer) - SepsisEngineService → Kafka → Redis SIRS + qSOFA state → PostgreSQL alert → SepsisAlertHandler → SepsisBundleService (consumer group: sepsis-engine) + SepsisEngineService → Kafka → Redis qSOFA state → PostgreSQL QSOFA_SCREEN alert (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) + SofaScoringService → Kafka (observation.recorded + gcs.scored) → SofaDetector → Redis SOFA lab cache → PostgreSQL sofa_scores + delta alerts → SepsisAlertHandler → SepsisBundleService on SOFA_SEPSIS (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 + AlertSuppressionService → Redis suppress:{enc}:{type} keys set on acknowledge; read by WarningEvaluator, News2Detector, QsofaDetector NotificationPublisherService → Kafka → RabbitMQ paging.queue (consumer group: notification-publisher) PagingWorkerService → RabbitMQ paging.queue → log page → NACK on timeout (or requeue on shutdown) EscalationWorkerService → RabbitMQ escalation.queue → update alert status @@ -121,7 +121,7 @@ IHostedServices (background): AlertsUnacknowledgedCollector → polls PostgreSQL every 30s → alerts_unacknowledged_gauge OutboxPendingCollector → polls outbox every 30s → outbox_pending_events KafkaConsumerLagCollector → polls four consumer groups every 30s → kafka_consumer_lag - ClinicalMetrics (singleton) → inline counters/histogram from ingest, SIRS, qSOFA, NEWS2, trend, suppression, bundle compliance, escalation paths + ClinicalMetrics (singleton) → inline counters/histogram from ingest, qSOFA, NEWS2, GCS, SOFA, trend, suppression, bundle compliance, escalation paths ``` **Why Kafka and RabbitMQ coexist:** Kafka is an append-only log — the same observation event reaches the Elasticsearch indexer, the sepsis engine, and the data lake independently without coordination. Each consumer holds its own offset and can replay from the beginning. RabbitMQ handles the action side: one message, one worker, one page. A duplicate page at 3am is a patient safety concern, not a minor inconvenience — RabbitMQ's acknowledgment-then-delete model is correct here. The DLQ TTL-based escalation has no equivalent in Kafka. @@ -134,7 +134,7 @@ IHostedServices (background): |---|---| | Server | ASP.NET Core 8 (.NET 8.0) | | Database | PostgreSQL 16 with EF Core 8 (code-first migrations) | -| Cache / SIRS & NEWS2 state | Redis 7 | +| Cache / scoring state | Redis 7 | | Message log | Apache Kafka 3.7 (KRaft, 6 partitions per topic) | | Task queue | RabbitMQ 3.13 (direct exchange, DLQ escalation) | | Search / analytics | Elasticsearch 8.13 (CQRS read projection) | @@ -191,7 +191,7 @@ VigilCareClinicalAPI/ │ ├── EncounterType.cs # Inpatient, Outpatient, Emergency │ ├── AlertSeverity.cs # Warning, Critical │ ├── AlertStatus.cs # Open, Acknowledged, Resolved, Escalated -│ ├── AlertType.cs # Threshold breach, sepsis, qSOFA, warning*, NEWS2_*, … +│ ├── AlertType.cs # Threshold breach, QSOFA_SCREEN, SOFA_SEPSIS, warning*, NEWS2_*, GCS_*, … │ ├── BloodType.cs # A+, O-, AB-, … with ToDbString/FromDbString │ ├── ObservationSource.cs # Device, Manual, Lab │ ├── SepsisBundleComplianceStatus.cs # InProgress, Compliant, NonCompliant @@ -241,7 +241,7 @@ VigilCareClinicalAPI/ │ ├── ElasticsSearch/ │ │ ├── ElasticIndexProvisioner.cs # Creates patient_encounters, observations, clinical_alerts indices │ │ └── EsIndexerService.cs # consumer group: es-indexer; upserts Elasticsearch documents -│ ├── SepsisEngineService.cs # consumer group: sepsis-engine; SIRS eval via Redis TTL keys +│ ├── SepsisEngineService.cs # consumer group: sepsis-engine; qSOFA screening 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 @@ -266,11 +266,10 @@ VigilCareClinicalAPI/ │ ├── MedicationCorrelationOptions.cs # Drug-vital mappings + correlation window │ └── DashboardOptions.cs # CORS origins for ward dashboard frontend ├── Sepsis/ -│ ├── SirsDetector.cs # Redis SIRS state management (SET/DEL/MGET) -│ ├── SirsEvaluator.cs # Per-code criterion evaluation +│ ├── AlertCreationGuard.cs # Prevents creation of deprecated alert types (SEPSIS_WARNING) │ ├── QsofaCalculator.cs # Pure static qSOFA scoring (3 criteria, no I/O) -│ ├── QsofaDetector.cs # Redis qSOFA state, alert creation, SepsisAlertHandler callback -│ └── SepsisAlertHandler.cs # Bridges SIRS/qSOFA alert creation → SepsisBundleService +│ ├── QsofaDetector.cs # Redis qSOFA state, QSOFA_SCREEN alert creation +│ └── SepsisAlertHandler.cs # Bridges SOFA_SEPSIS alert → SepsisBundleService ├── News2/ │ ├── News2Calculator.cs # Pure static NEWS2 scoring tables (no I/O) │ └── News2Detector.cs # Redis parameter state, score persistence, alert creation @@ -302,7 +301,7 @@ VigilCareClinicalAPI/ │ ├── Encounter/WardEncounterSummary.cs # Denormalized row for ward encounter list │ ├── Medication/CreateMedicationAdministrationRequest.cs │ ├── Qsofa/QsofaCurrentResponse.cs -│ └── Sepsis/QsofaResult.cs, QsofaOutcome.cs # qSOFA detector result and outcome enum +│ └── Sepsis/QsofaResult.cs, QsofaOutcome.cs # qSOFA detector result and screening outcome enum ├── Data/ │ ├── AppDbContext.cs # EF Core context — entity configs, indexes, constraints │ ├── Configurations/ # IEntityTypeConfiguration per entity; ElasticsearchOptions, ElasticIndexOptions @@ -331,8 +330,6 @@ tests/ └── VigilCareClinicalAPI.Tests/ ├── ObservationIngestTests.cs # Ingest happy path, critical alert creation, discharged encounter rejection, idempotency ├── AlertLifecycleTests.cs # Acknowledge, resolve, escalation guard - ├── SirsDetectorTests.cs # Redis SIRS state SET/DEL/MGET logic - ├── SirsEvaluatorTests.cs # Per-code criterion evaluation ├── NotificationPipelineTests.cs # RabbitMQ topology, DLQ routing ├── ReconciliationTests.cs # Three reconciliation checks, deduplication, RabbitMQ publish ├── ObservabilityPhase8Tests.cs # /metrics families and correlation header behavior @@ -347,8 +344,11 @@ tests/ ├── TrendDetectorTests.cs # Trend detector — rapid climb, stable, idempotent, non-trend code ├── AlertSuppressionTests.cs # Suppression on acknowledge, read-side skip, TTL expiry ├── QsofaCalculatorTests.cs # Boundary tests for three qSOFA criteria - ├── QsofaDetectorTests.cs # qSOFA detector — two-criteria alert, normalization, idempotency - ├── SepsisBundleTests.cs # Bundle creation from SIRS/qSOFA, element completion, compliance outcomes + ├── QsofaDetectorTests.cs # qSOFA detector — two-criteria QSOFA_SCREEN alert, normalization, idempotency + ├── SepsisBundleTests.cs # Bundle creation from SOFA_SEPSIS, element completion, compliance outcomes + ├── SepsisRefactorTests.cs # Sepsis-3 refactor — SIRS removal, qSOFA screen workflow, SOFA bundle trigger + ├── AlertCreationGuardTests.cs # Guard prevents deprecated SEPSIS_WARNING creation + ├── ClinicalRefactorEndToEndTests.cs # End-to-end scenario replay: qSOFA screen → SOFA labs → bundle ├── MedicationServiceTests.cs # Medication CRUD, discharged encounter rejection, pagination ├── MedicationCorrelationTests.cs # End-to-end warning/NEWS2 annotation with medication context ├── MedicationValidationTests.cs # FluentValidation 400 on invalid medication requests @@ -360,20 +360,21 @@ tests/ VigilCare.Simulator/ # Phase 16 — console replay simulator (HTTP-only, no direct DB/Kafka) ├── Program.cs # CLI: replay, replay-all, validate, dry-run ├── Commands/ # System.CommandLine command handlers -├── Client/VigilCareApiClient.cs # Typed HTTP client for all API endpoints +├── Client/VigilCareApiClient.cs # Typed HTTP client for all API endpoints (incl. GCS, SOFA) ├── Engine/ReplayEngine.cs # Scenario replay with speed multiplier + event logging -├── Polling/ApiPoller.cs # Optional post-event alert/score/bundle polling +├── Output/SimulatorConsole.cs # Colored output with GCS/SOFA score display +├── Polling/ApiPoller.cs # Optional post-event alert/score/bundle/GCS/SOFA polling ├── Scenarios/ # schema.json, ScenarioLoader, ScenarioValidator -└── Scenarios/List/ # Eight sample scenarios (sepsis, NEWS2, stable, medication, …) +└── Scenarios/List/ # Eleven sample scenarios (sepsis, GCS, SOFA, NEWS2, stable, …) -vigilcare-dashboard/ # Phases 17–19 — Vue 3 ward dashboard SPA +vigilcare-dashboard/ # Phases 17–19, 27–28 — Vue 3 ward dashboard SPA ├── src/ -│ ├── api/ # HTTP client, encounters, clinical, alerts, normalize -│ ├── components/ # charts, replay, alerts, feedback, patient, ward, layout, ui -│ ├── composables/ # useChartData, useReplayControls, usePolling, useFeedback, chartFormat -│ ├── stores/ # Pinia — ward, alerts, settings, feedback (localStorage) +│ ├── api/ # HTTP client, encounters, clinical (GCS, SOFA), alerts, normalize +│ ├── components/ # charts, replay, alerts, feedback, patient (GcsEntryForm, SofaScorePanel), ward, layout, ui +│ ├── composables/ # useChartData, useReplayControls, usePolling, useFeedback, useGcs, useSofa, chartFormat +│ ├── stores/ # Pinia — ward, alerts, settings, feedback, scoring (localStorage) │ ├── views/ # WardDashboard, PatientDetail, AlertCenter, FeedbackSummary -│ └── __tests__/ # Vitest — 41 tests (store, feedback, replay, charts, alerts, ward) +│ └── __tests__/ # Vitest — tests (store, feedback, replay, charts, alerts, ward, GCS, SOFA) ├── vite.config.js └── README.md # Dev quick start → docs/dashboard-guide.md @@ -381,7 +382,7 @@ scripts/ ├── run-api-redis-tests.sh # Phase 1 — patient/encounter/threshold + Redis cache ├── run-kafka-outbox-tests.sh # Phase 3 — outbox relay and Kafka topics ├── run-elasticsearch-analytics-tests.sh # Phase 4 — Elasticsearch CQRS projection -├── run-sepsis-sirs-tests.sh # Phase 5 — SIRS detector and sepsis engine +├── run-sepsis-sirs-tests.sh # Phase 5 — legacy SIRS tests (now qSOFA-only) ├── run-notification-pipeline-tests.sh # Phase 6 — RabbitMQ paging, DLQ, discharge summary ├── run-reconciliation-tests.sh # Phase 7 — reconciliation scheduler checks ├── run-phase8-verification.sh # Phase 8 — Prometheus metrics, alerts_unacknowledged_gauge, correlation headers @@ -393,7 +394,10 @@ scripts/ ├── run-phase14-verification.sh # Phase 14 — qSOFA, sepsis bundle compliance, integration tests ├── 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 +├── run-phase26-verification.sh # Phase 26 — SOFA scoring integration tests + baseline/delta API checks +├── run-phase27-verification.sh # Phase 27 — Sepsis-3 refactor: SIRS removal, QSOFA_SCREEN, SOFA bundle trigger +├── run-phase28-verification.sh # Phase 28 — Frontend GCS entry + SOFA display + sepsis UI refactor +└── run-phase29-verification.sh # Phase 29 — Simulator scenario expansion + clinical validation docs/ ├── plans/ # Phase implementation and verification guides @@ -403,8 +407,9 @@ docs/ ├── simulator-guide.md # VigilCare.Simulator user guide ├── decisions/ │ ├── data-lake-design.md # Parquet vs JSON, partitioning, replay rationale -│ ├── sepsis-engine-design.md # SIRS sliding window and idempotent alert design -│ └── medication-correlation-design.md # Drug-vital mapping and annotation rationale +│ ├── sepsis-engine-design.md # Sepsis-3 qSOFA screening and idempotent alert design +│ ├── medication-correlation-design.md # Drug-vital mapping and annotation rationale +│ └── clinical-refactor-sofa-gcs.md # Interview Q&A: why SIRS→SOFA, carry-forward, GCS dependency chain ├── docker-compose-usage-and-troubleshooting.md └── vigilcare-clinical-api-prd.md # Product requirements and phase roadmap ``` @@ -425,19 +430,23 @@ This is the architectural decision that separates thinking about healthcare syst Observations, alerts, and orders belong to an encounter, not directly to a patient. A patient's blood pressure taken during a 2022 admission belongs to that admission. This bounds queries naturally: "show me all observations for this encounter" is a bounded query. "Show me all observations ever recorded for this patient" is a cross-encounter aggregation that belongs in the data lake. -### Redis for Five Distinct Purposes +### Redis for Seven Distinct Purposes -Redis serves five independent roles with different semantics: +Redis serves seven independent roles with different semantics: 1. **Threshold cache:** write-through invalidation on every threshold update. Staleness here has clinical consequences — a stale threshold could suppress a critical alert. TTL expiry is not sufficient; invalidation must be immediate on write. -2. **SIRS sliding window:** `SET sirs:{encounterId}:{code} EX 1800`. The TTL does real work — a heart rate that was abnormal 31 minutes ago stops contributing to the SIRS count without any cleanup job. The 30-minute TTL is a clinical parameter, not an arbitrary cache timeout. +2. **qSOFA sliding window:** `SET qsofa:{encounterId}:{code} EX 1800`. The 30-minute TTL is a clinical parameter — a respiratory rate that was abnormal 31 minutes ago stops contributing to the qSOFA count without any cleanup job. When a value normalizes, the key is explicitly deleted rather than waiting for TTL — a systolic BP that recovers from 90 to 120 should immediately reduce the qSOFA count. -3. **qSOFA sliding window:** `SET qsofa:{encounterId}:{code} EX 1800`. Same 30-minute TTL as SIRS, but tracking three organ-dysfunction criteria instead of four inflammatory markers. When a value normalizes, the key is explicitly deleted rather than waiting for TTL — a systolic BP that recovers from 90 to 120 should immediately reduce the qSOFA count. +3. **NEWS2 parameter state:** `SET news2:{encounterId}:{code}` with a 4-hour TTL. Each of the seven NEWS2 parameters is scored individually and stored in Redis; `MGET` across all seven keys determines completeness. An incomplete set (fewer than seven present keys) does not produce a score — expired parameters must be re-recorded before the aggregate is computed. -4. **NEWS2 parameter state:** `SET news2:{encounterId}:{code}` with a 4-hour TTL. Each of the seven NEWS2 parameters is scored individually and stored in Redis; `MGET` across all seven keys determines completeness. An incomplete set (fewer than seven present keys) does not produce a score — expired parameters must be re-recorded before the aggregate is computed. +4. **GCS component state:** `SET gcs:{encounterId}:{component}` tracking Eye, Verbal, and Motor components. All three must be present before a total score is computed. Completion triggers `gcs.scored` for SOFA CNS re-scoring. -5. **Alert suppression windows:** `SET suppress:{encounterId}:{alertType}` with a configurable TTL (default 30 min). Set on acknowledge of suppressible alerts; checked by `WarningEvaluator` and `News2Detector` before creating new warning alerts. Critical alerts are never suppressed. +5. **SOFA lab cache:** `SET sofa:{encounterId}:{code}` with carry-forward semantics (configurable 24-hour TTL). Labs are classified as CURRENT (< 12h), STALE (12–24h), or EXPIRED (> 24h). Stale values are still used for scoring but flagged in staleness metadata. This enables SOFA scoring on wards where labs are drawn every 6–12 hours, not continuously. + +6. **Trend history:** sliding-window observation history for rate-of-change detection. Five vital parameters tracked for velocity thresholds. + +7. **Alert suppression windows:** `SET suppress:{encounterId}:{alertType}` with a configurable TTL (default 30 min). Set on acknowledge of suppressible alerts; checked by `WarningEvaluator` and `News2Detector` before creating new warning alerts. Critical alerts are never suppressed. ### Outbox Pattern @@ -445,7 +454,7 @@ Observation and alert writes use the transactional outbox: the `outbox_events` r ### Kafka Partition Key: `encounterId` -All events for the same encounter land on the same partition. The sepsis engine requires this: if observations from the same patient arrive on different partitions, they may be consumed out of order and simultaneous SIRS criteria could be missed. Six partitions balance parallelism against per-encounter ordering guarantees. +All events for the same encounter land on the same partition. The sepsis engine requires this: if observations from the same patient arrive on different partitions, they may be consumed out of order and simultaneous qSOFA criteria could be missed. Six partitions balance parallelism against per-encounter ordering guarantees. ### Elasticsearch as a CQRS Read Projection @@ -461,9 +470,15 @@ The five-minute escalation is not a retry — it is a clinical workflow. When an A medium hospital with 200 concurrent inpatients at five observations per patient per minute produces approximately 17 observations per second at steady state. PostgreSQL with the composite index `(encounter_id, observation_code, recorded_at DESC)` handles this volume with headroom. TimescaleDB would be the correct next step at 10,000+ observations/second — it is PostgreSQL with automatic time partitioning, meaning the query layer would not change. The Parquet data lake handles the analytics workload that would otherwise stress the operational database over a 10-year horizon. -### Dual-Path Sepsis Detection (SIRS + qSOFA) +### Two-Tier Sepsis Detection (Sepsis-3: qSOFA Screen → SOFA Confirmation) -Both SIRS and qSOFA run within the same `SepsisEngineService` Kafka consumer against every `observation.recorded` event. SIRS detects systemic inflammatory response (temperature, heart rate, respiratory rate, WBC — Sepsis-2 criteria). qSOFA detects organ dysfunction (respiratory rate, systolic BP, altered mentation — Sepsis-3 consensus). They fire independently because they measure different clinical dimensions of the same disease process. A patient can trigger SIRS without qSOFA (infection with inflammation but no organ failure) or qSOFA without SIRS (organ dysfunction without classic inflammatory markers). Both paths feed into the same sepsis bundle workflow — the bundle is idempotent, so the second alert for the same encounter does not create a duplicate bundle. This dual-path design reflects current clinical practice where neither scoring system alone captures all sepsis presentations. +The sepsis pathway follows the Sepsis-3 consensus (2016), replacing the older SIRS-based approach: + +1. **Screening (qSOFA):** `SepsisEngineService` evaluates three bedside criteria (respiratory rate ≥ 22, systolic BP ≤ 100, altered mentation via GCS < 15). When ≥ 2 are active, a `QSOFA_SCREEN` (WARNING-level) alert is created, recommending SOFA lab orders. + +2. **Confirmation (SOFA):** `SofaScoringService` scores six organ systems from labs and vitals. When SOFA delta ≥ 2 from baseline, a `SOFA_SEPSIS` (CRITICAL) alert fires and triggers the sepsis bundle via `SepsisAlertHandler`. + +This two-tier design prevents false-positive bundle activations — SIRS criteria (temperature, heart rate, respiratory rate, WBC) were too non-specific, triggering bundles for post-surgical inflammation, anxiety, and viral infections. SOFA measures actual organ dysfunction, making the bundle trigger clinically meaningful. The legacy `SEPSIS_WARNING` and `QSOFA_WARNING` alert types are retained (marked `[Obsolete]`) for historical queries but can no longer be created. --- @@ -529,7 +544,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, GCS scoring, SOFA 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 qSOFA screening, 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`). @@ -572,7 +587,9 @@ Integration tests use `WebApplicationFactory` with a `Testing` environment and T |---|---|---| | `ObservationIngestTests` | 2 | Ingest happy path, critical alert creation, discharged encounter rejection, idempotency | | `AlertLifecycleTests` | 2 | Acknowledge, resolve, escalation guard | -| `SirsDetectorTests` / `SirsEvaluatorTests` | 5 | Redis SIRS state and per-code criterion evaluation | +| `SepsisRefactorTests` | 27 | Sepsis-3 refactor — SIRS removed, qSOFA creates QSOFA_SCREEN, SOFA delta triggers bundle | +| `AlertCreationGuardTests` | 27 | Guard prevents creation of deprecated SEPSIS_WARNING alerts | +| `ClinicalRefactorEndToEndTests` | 29 | End-to-end scenario replay: qSOFA screen → SOFA labs → SOFA_SEPSIS → bundle | | `NotificationPipelineTests` | 6 | RabbitMQ topology, DLQ routing, paging | | `ReconciliationTests` | 7 | Three reconciliation checks, deduplication, RabbitMQ publish | | `ObservabilityPhase8Tests` | 8 | All ten `/metrics` families, correlation headers, ingest counter increment | @@ -587,8 +604,8 @@ Integration tests use `WebApplicationFactory` with a `Testing` environment and T | `TrendDetectorTests` | 13 | Trend detector — rapid HR climb alert, stable high HR, idempotency, non-trend code | | `AlertSuppressionTests` | 13 | Acknowledge sets Redis key, suppressed warning skipped, critical/NEWS2 emergency never suppressed, TTL expiry | | `QsofaCalculatorTests` | 14 | Boundary tests for three qSOFA criteria (RESP_RATE, SYSTOLIC_BP, AVPU) | -| `QsofaDetectorTests` | 14 | qSOFA detector — two-criteria alert, normalization key delete, idempotent duplicate, non-qSOFA code ignored | -| `SepsisBundleTests` | 14 | Bundle creation from SIRS/qSOFA, four auto-orders, element completion, compliant/non-compliant outcomes, monitor marks overdue bundles, idempotency | +| `QsofaDetectorTests` | 14 | qSOFA detector — two-criteria QSOFA_SCREEN alert, normalization key delete, idempotent duplicate, non-qSOFA code ignored | +| `SepsisBundleTests` | 14 | Bundle creation from SOFA_SEPSIS, four auto-orders, element completion, compliant/non-compliant outcomes, monitor marks overdue bundles, idempotency | | `MedicationServiceTests` | 15 | Medication create/list on active encounter, discharged encounter 409, pagination, `since` filter | | `MedicationCorrelationTests` | 15 | Warning and NEWS2 alert details annotated when correlated drug administered | | `MedicationValidationTests` | 15 | FluentValidation 400 on empty drug name, zero dose, future `administeredAt` | @@ -610,6 +627,7 @@ With the API running (`dotnet run`) and Docker Compose up: ./scripts/run-phase13-verification.sh # Trend detection, alert suppression, consumer lag, Phase 13 integration tests ./scripts/run-phase14-verification.sh # qSOFA, sepsis bundle compliance, Phase 14 integration tests ./scripts/run-phase15-verification.sh # Medication administration + correlation annotation pipeline +./scripts/run-phase27-verification.sh # Sepsis-3 refactor: SIRS removal, QSOFA_SCREEN, SOFA bundle trigger ``` Phase 25 — GCS scoring (requires running API + Docker Compose; set an active encounter UUID): @@ -676,9 +694,8 @@ See `docs/plans/phase-8-plan.md` through `docs/plans/phase-12-plan.md` for manua |---|---|---|---| | `observations_ingested_total` | Counter | `observation_code`, `source` | `ObservationService` on each committed observation | | `observation_ingest_duration_seconds` | Histogram | — | `ObservationService` — full ingest transaction to COMMIT | -| `clinical_alerts_total` | Counter | `alert_type`, `severity` | `ObservationService`, `SirsDetector`, `QsofaDetector`, `News2Detector`, `TrendDetector`, `WarningEvaluator` | -| `sirs_detections_total` | Counter | — | `SirsDetector` — only on successful idempotent insert | -| `qsofa_detections_total` | Counter | — | `QsofaDetector` — only on successful idempotent insert | +| `clinical_alerts_total` | Counter | `alert_type`, `severity` | `ObservationService`, `QsofaDetector`, `News2Detector`, `TrendDetector`, `SofaDetector`, `WarningEvaluator` | +| `qsofa_detections_total` | Counter | — | `QsofaDetector` — only on successful idempotent QSOFA_SCREEN insert | | `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 | @@ -989,9 +1006,9 @@ SOFA scores are computed asynchronously by `SofaScoringService` from SOFA-relate | GET | `/encounters/{id}/sepsis-bundle/current` | Current (most recent) sepsis bundle for an encounter (404 if none) | | GET | `/sepsis-bundles/{id}` | Bundle detail with all elements and linked orders | -**`GET /sepsis-bundle/current` response** includes `encounterId`, `triggeringAlertId`, `triggeringAlertType` (`QSOFA_WARNING` or `SEPSIS_WARNING`), `recognizedAt`, `deadlineAt`, `complianceStatus` (`IN_PROGRESS`, `COMPLIANT`, `NON_COMPLIANT`), `completedAt`, and `elements[]` — each with `elementType`, `status`, `orderId`, and `completedAt`. +**`GET /sepsis-bundle/current` response** includes `encounterId`, `triggeringAlertId`, `triggeringAlertType` (`SOFA_SEPSIS`), `recognizedAt`, `deadlineAt`, `complianceStatus` (`IN_PROGRESS`, `COMPLIANT`, `NON_COMPLIANT`), `completedAt`, and `elements[]` — each with `elementType`, `status`, `orderId`, and `completedAt`. -Bundles are created automatically by `SepsisAlertHandler` when a SIRS or qSOFA alert fires. Four clinical orders (blood cultures, serum lactate, broad-spectrum antibiotics, IV fluid resuscitation) are auto-created with `orderedBy: sepsis-bundle-engine`. As orders are resulted via `PATCH /orders/{id}/result`, the corresponding bundle element is marked complete. When all four elements are done, the bundle transitions to `COMPLIANT` (within the 1-hour deadline) or `NON_COMPLIANT`. +Bundles are created automatically by `SepsisAlertHandler` when a `SOFA_SEPSIS` alert fires (delta ≥ 2 from baseline). Four clinical orders (blood cultures, serum lactate, broad-spectrum antibiotics, IV fluid resuscitation) are auto-created with `orderedBy: sepsis-bundle-engine`. As orders are resulted via `PATCH /orders/{id}/result`, the corresponding bundle element is marked complete. When all four elements are done, the bundle transitions to `COMPLIANT` (within the 1-hour deadline) or `NON_COMPLIANT`. ### Medications @@ -1090,8 +1107,8 @@ Indexes: partial unique `(idempotency_key) WHERE idempotency_key IS NOT NULL`, ` id Guid PK encounterId Guid FK → Encounter patientId Guid FK → Patient -observationId Guid? FK → Observation (null for SIRS and NEWS2 composite alerts) -alertType string e.g. THRESHOLD_BREACH, SEPSIS_WARNING, NEWS2_WARNING, NEWS2_EMERGENCY +observationId Guid? FK → Observation (null for NEWS2, GCS, SOFA composite alerts) +alertType string e.g. CRITICAL_HEART_RATE, QSOFA_SCREEN, SOFA_SEPSIS, NEWS2_WARNING, NEWS2_EMERGENCY, GCS_CRITICAL severity string WARNING | CRITICAL details text required status string open | acknowledged | resolved | escalated (default: open) @@ -1183,7 +1200,7 @@ Indexes: `(encounter_id, ordered_at DESC)`, partial `(status, ordered_at) WHERE id Guid PK encounterId Guid FK → Encounter triggeringAlertId Guid FK → ClinicalAlert -triggeringAlertType string required (max 50) — QSOFA_WARNING | SEPSIS_WARNING +triggeringAlertType string required (max 50) — SOFA_SEPSIS recognizedAt DateTimeOffset deadlineAt DateTimeOffset — recognizedAt + 1 hour complianceStatus string IN_PROGRESS | COMPLIANT | NON_COMPLIANT (default: IN_PROGRESS) @@ -1333,7 +1350,7 @@ Exchange: `clinical.notifications.exchange` (direct) | Topic | Partition key | Consumer groups | |---|---|---| -| `observation.recorded` | `encounterId` | `es-indexer`, `sepsis-engine`, `warning-evaluator`, `news2-scoring`, `gcs-scoring`, `sofa-scoring`, `trend-analyzer`, `data-lake-writer` | +| `observation.recorded` | `encounterId` | `es-indexer`, `sepsis-engine` (qSOFA), `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` | @@ -1355,42 +1372,29 @@ All topics use 6 partitions. `KAFKA_AUTO_CREATE_TOPICS_ENABLE=false` — topics --- -## SIRS Criteria - -The sepsis engine evaluates four SIRS (Systemic Inflammatory Response Syndrome) criteria per encounter using Redis keys with a 30-minute TTL: - -| Criterion | Observation Code | Trigger | -|---|---|---| -| Fever or hypothermia | `TEMP_C` | > 38.3°C or < 36.0°C | -| Tachycardia | `HEART_RATE` | > 90 bpm | -| Tachypnea | `RESP_RATE` | > 20 breaths/min | -| Abnormal WBC | `WBC_K_UL` | > 12.0 or < 4.0 k/µL | - -When ≥ 2 criteria are active simultaneously (keys present in Redis) for the same encounter and no open `SEPSIS_WARNING` alert already exists, the engine inserts a `CRITICAL` alert and outbox event. The 30-minute TTL is a clinical parameter — it bounds the window within which simultaneous SIRS criteria must co-occur. A successful SIRS alert triggers sepsis bundle creation via `SepsisAlertHandler`. - --- -## qSOFA Scoring (Sepsis-3) +## qSOFA Screening (Sepsis-3 Bedside Tool) -The qSOFA (quick Sequential Organ Failure Assessment) engine evaluates three organ-dysfunction criteria per encounter, running in parallel with SIRS within the same `SepsisEngineService` Kafka consumer. Redis keys use a 30-minute TTL sliding window, identical to SIRS: +The qSOFA (quick Sequential Organ Failure Assessment) engine evaluates three organ-dysfunction criteria per encounter within the `SepsisEngineService` Kafka consumer. Redis keys use a 30-minute TTL sliding window: | Criterion | Observation Code | Trigger | |---|---|---| | Tachypnea | `RESP_RATE` | ≥ 22 breaths/min | | Hypotension | `SYSTOLIC_BP` | ≤ 100 mmHg | -| Altered mentation | `AVPU` | ≥ 1 (any non-Alert state) | +| Altered mentation | `GCS` / `AVPU` | GCS < 15 or AVPU ≥ 1 (any non-Alert state) | -Redis key pattern: `qsofa:{encounterId}:{code}` with 30-minute TTL. When a criterion normalizes, the key is deleted immediately. When ≥ 2 of 3 criteria are active simultaneously and no open `QSOFA_WARNING` alert exists, the engine inserts a `CRITICAL` alert with details formatted as `"qSOFA score 2/3: RESP_RATE=24, SYSTOLIC_BP=95"`. A successful qSOFA alert triggers sepsis bundle creation via `SepsisAlertHandler`. +Redis key pattern: `qsofa:{encounterId}:{code}` with 30-minute TTL. When a criterion normalizes, the key is deleted immediately. When ≥ 2 of 3 criteria are active simultaneously and no open `QSOFA_SCREEN` alert exists, the engine inserts a `WARNING`-level screening alert with details formatted as `"qSOFA score 2/3: RESP_RATE=24, SYSTOLIC_BP=95 — recommend SOFA lab panel"`. + +**Clinical role:** qSOFA is a bedside screening tool — it identifies patients who should have SOFA labs ordered. It does **not** trigger the sepsis bundle directly. Only `SOFA_SEPSIS` (delta ≥ 2 from baseline) triggers bundle creation. This matches the Sepsis-3 two-tier workflow: screen → confirm → treat. **API:** `GET /encounters/{id}/qsofa/current` returns `activeCriteria` (0–3) and per-criterion values from Redis via `QsofaService` — used by ward dashboards and the simulator poll loop. -**Clinical distinction:** SIRS detects systemic inflammation (infection response); qSOFA detects organ dysfunction (Sepsis-3 consensus). Both can fire independently for the same encounter. The sepsis bundle is idempotent — if one is already in progress, the second alert does not create a duplicate. - --- ## Sepsis Bundle Compliance (SEP-1) -When either a SIRS or qSOFA alert fires, `SepsisAlertHandler` calls `SepsisBundleService.TryCreateBundleAsync()` to initiate a four-element treatment bundle with a one-hour compliance deadline: +When a `SOFA_SEPSIS` alert fires (delta ≥ 2 from baseline), `SepsisAlertHandler` calls `SepsisBundleService.TryCreateBundleAsync()` to initiate a four-element treatment bundle with a one-hour compliance deadline: | Bundle Element | Order Type | Auto-Created Order Description | |---|---|---| @@ -1401,7 +1405,7 @@ When either a SIRS or qSOFA alert fires, `SepsisAlertHandler` calls `SepsisBundl **Lifecycle:** -1. Alert fires → `SepsisAlertHandler` → `SepsisBundleService.TryCreateBundleAsync()` +1. `SOFA_SEPSIS` alert fires → `SepsisAlertHandler` → `SepsisBundleService.TryCreateBundleAsync()` 2. Bundle created with `complianceStatus = IN_PROGRESS`, `deadlineAt = recognizedAt + 1 hour` 3. Four orders created (`orderedBy: sepsis-bundle-engine`), each linked to a bundle element 4. Outbox event → Kafka topic `sepsis.bundle.created` → ES indexer projects `sepsisBundleStatus` on encounter @@ -1518,7 +1522,7 @@ Observation history uses cursor pagination on `(recorded_at DESC, id DESC)`. Off ## Implemented Phases -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/`). +Twenty-four phases from the project roadmap are implemented and verified, including the **Sepsis-3 clinical refactor** (Phases 27–29). Integration tests (`dotnet test`) and per-phase verification scripts cover Phases 8–15, 25–29. Phases 17–19 add the Vue dashboard and clinician feedback (Vitest in `vigilcare-dashboard/`). | Phase | Feature | Status | |---|---|---| @@ -1526,7 +1530,7 @@ Nineteen core phases from the project roadmap are implemented and verified, plus | 2 | Observation ingest — idempotency, plausibility validation, synchronous critical alert creation, outbox event, cursor-paginated history; alert lifecycle (acknowledge, resolve); integration tests | Done | | 3 | Outbox relay (`IHostedService`, 500ms poll); Kafka topics with 6 partitions; `encounterId` partition key; relay survives Kafka restart | Done | | 4 | Elasticsearch CQRS projection (`EsIndexerService`); patient search; observation trend; alert summary; population aggregation; replay procedure | Done | -| 5 | Sepsis detection engine (`SepsisEngineService`); Redis SIRS state with 30-min TTL; idempotent alert creation; integration tests | Done | +| 5 | Sepsis engine (`SepsisEngineService`); Redis-based qSOFA screening (SIRS removed in Phase 27); idempotent alert creation; integration tests | Done | | 6 | RabbitMQ exchange and queue topology; `NotificationPublisherService`; `PagingWorkerService`; DLQ escalation (`EscalationWorkerService`); discharge summary (`DischargeSummaryWorkerService` → MinIO); integration tests | Done | | 7 | Reconciliation scheduler — unacknowledged critical alerts, stale pending orders, disconnected monitors; `reconciliation_alerts` table; RabbitMQ publish; integration tests | Done | | 8 | Prometheus metrics (`GET /metrics`); ten metric families and three collectors; Grafana clinical dashboard; `ObservabilityPhase8Tests`; `run-phase8-verification.sh` | Done | @@ -1535,7 +1539,7 @@ Nineteen core phases from the project roadmap are implemented and verified, plus | 11 | Warning alert consumer (`WarningAlertService` / `warning-evaluator`); 10 `Warning*` alert types; Orders API (`OrdersController`, `OrderService`); FluentValidation on all request DTOs; `WarningAlertTests`, `OrderLifecycleTests`, `ValidationTests`; `run-phase11-verification.sh` | Done | | 12 | NEWS2 composite scoring (`News2Calculator`, `News2Detector`, `News2ScoringService`); `news2_scores` table; `NEWS2_WARNING` / `NEWS2_EMERGENCY` alert types; `News2Controller` (current + history); ES `news2Score` / `news2RiskLevel` projection; Prometheus NEWS2 metrics; `News2CalculatorTests`, `News2DetectorTests`; `run-phase12-verification.sh` | Done | | 13 | Trend detection (`TrendCalculator`, `TrendDetector`, `TrendAnalyzerService`); `RAPID_DETERIORATION` alert type; alert suppression windows (`AlertSuppressionService`, Redis `suppress:{enc}:{type}`); `TrendCalculatorTests`, `TrendDetectorTests`, `AlertSuppressionTests`; `run-phase13-verification.sh` | Done | -| 14 | qSOFA scoring engine (`QsofaCalculator`, `QsofaDetector`); `QSOFA_WARNING` alert type; sepsis bundle compliance (`SepsisBundle`, `SepsisBundleElement`, `SepsisBundleService`); auto-created treatment orders with 1-hour deadline; `SepsisAlertHandler` bridge; `SepsisBundleMonitorService` (5-min overdue scan); `SepsisBundlesController` API; ES projection of bundle status; Kafka topics `sepsis.bundle.created` / `sepsis.bundle.updated`; Prometheus `qsofa_detections_total` and `sepsis_bundle_compliance_total`; `QsofaCalculatorTests`, `QsofaDetectorTests`, `SepsisBundleTests` | Done | +| 14 | qSOFA scoring engine (`QsofaCalculator`, `QsofaDetector`); sepsis bundle compliance (`SepsisBundle`, `SepsisBundleElement`, `SepsisBundleService`); auto-created treatment orders with 1-hour deadline; `SepsisAlertHandler` bridge; `SepsisBundleMonitorService` (5-min overdue scan); `SepsisBundlesController` API; ES projection of bundle status; Kafka topics `sepsis.bundle.created` / `sepsis.bundle.updated`; Prometheus `qsofa_detections_total` and `sepsis_bundle_compliance_total`; `QsofaCalculatorTests`, `QsofaDetectorTests`, `SepsisBundleTests` (bundle trigger updated to SOFA_SEPSIS in Phase 27) | Done | | 15 | Medication administration (`MedicationAdministration`, `MedicationsController`, `MedicationService`); drug-vital correlation config (`MedicationCorrelationOptions`); `MedicationCorrelationHelper` annotates `WarningEvaluator` and `News2Detector` alert details; `medication_administrations` table + migration; `MedicationServiceTests`, `MedicationCorrelationTests`, `MedicationValidationTests`; `run-phase15-verification.sh`; design doc in `docs/decisions/medication-correlation-design.md` | Done | | 16 | Console replay simulator (`VigilCare.Simulator`); scenario JSON schema; CLI commands `replay`, `replay-all`, `validate`, `dry-run`; speed multiplier and optional API polling; eight sample scenarios; `docs/simulator-guide.md` | Done | | 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 | @@ -1543,9 +1547,14 @@ Nineteen core phases from the project roadmap are implemented and verified, plus | 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 | +| 27 | **Sepsis-3 clinical refactor** — SIRS removed (`SirsDetector`, `SirsEvaluator` deleted); qSOFA repositioned as bedside screening (`QSOFA_SCREEN` replaces `QSOFA_WARNING`); sepsis bundle now triggered only by `SOFA_SEPSIS` (delta ≥ 2) via `SepsisAlertHandler`; `AlertCreationGuard` prevents deprecated `SEPSIS_WARNING` creation; legacy alert types retained `[Obsolete]` for historical queries; migration `AddQsofaScreenAlertType`; `SepsisRefactorTests`, `AlertCreationGuardTests`; `run-phase27-verification.sh` | Done | +| 28 | **Frontend GCS + SOFA + sepsis UI refactor** — `GcsEntryForm.vue` (bedside GCS component entry); `SofaScorePanel.vue` (organ-system breakdown with staleness indicators); `useGcs` / `useSofa` composables; `scoring` Pinia store; `ScoresPanel` updated with GCS/SOFA display; `SepsisBundlePanel` and `AlertReasoning` refactored for Sepsis-3 alert types; Vitest tests for GCS entry, SOFA panel, scores panel, alert labels; `run-phase28-verification.sh` | Done | +| 29 | **Simulator scenario expansion + clinical validation** — three new scenarios (`neurological-decline-gcs-01`, `sepsis-sofa-progression-01`, `sofa-partial-spo2-fallback-01`); existing scenarios enriched with GCS/SOFA observations; `ScenarioReplayHelper` for end-to-end test replay; `ClinicalRefactorEndToEndTests` validates qSOFA screen → SOFA labs → bundle workflow; simulator polls GCS/SOFA scores; `run-phase29-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). +**Ward dashboard:** backend APIs (`GET /encounters` ward list, `GET /qsofa/current`, CORS) and frontend SPA — `EncountersListTests`, `QsofaCurrentTests`, `vigilcare-dashboard` Vitest suite (replay scrubbing, feedback store, FeedbackButtons, FeedbackSummary, alert components, charts, ward table, GCS entry, SOFA panel, scores panel, alert labels). **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. +**Sepsis-3 refactor (Phases 27–29):** SIRS removed; qSOFA repositioned as bedside screening (`QSOFA_SCREEN`); SOFA delta ≥ 2 triggers `SOFA_SEPSIS` → sepsis bundle. Frontend gains GCS entry form and SOFA score panel. Eleven simulator scenarios validate the full clinical pipeline end-to-end. + **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.