# VigilCare Alert Optimization Roadmap ## Context VigilCare has established a strong clinical deterioration and sepsis detection foundation through NEWS2, qSOFA, SOFA, GCS, trend detection, medication correlation, and escalation workflows. Research on alert fatigue (PMC6748819), CDS/ML alert optimization (PMID 31206159), and alert usability/human factors (PMC11845892) consistently indicates that commercial value now lies in improving alert quality, trust, relevance, and workflow integration rather than increasing alert types. The next competitive advantage is not detecting more clinical events. It is helping clinicians trust and act on the events already being detected. > **Prerequisite:** Phase 32 (PHI Column Encryption + Access Logging) complete. Alert analytics and lifecycle tracking require authenticated users from Phase 31 RBAC. --- ## Scoring Guide Each priority is scored on three dimensions using a 1-10 scale. | Dimension | Description | |------------------------|-----------------------------------------------------------------------------| | Commercial Viability | Revenue impact, procurement influence, contract value, market positioning | | Clinical Usage | Bedside adoption, clinician trust, workflow integration, patient outcomes | | Composite | Weighted average (Commercial 50%, Clinical 50%) | --- ## Summary Matrix | Phase | Feature | Commercial | Clinical | Composite | Dependencies | |-------|------------------------------------|------------|----------|-----------|--------------------| | 33 | Alert Quality Analytics | 10 | 9 | 9.5 | None | | 34 | Explainable Alerts | 8 | 10 | 9.0 | None | | 35 | Alert Lifecycle Analytics | 9 | 7 | 8.0 | Phase 33 | | 36 | Role-Based Alert Routing | 8 | 9 | 8.5 | Phase 33, 35 | | 37 | Alert Bundling and Correlation | 7 | 8 | 7.5 | Phase 34 | | 38 | Adaptive Threshold Recommendations | 9 | 6 | 7.5 | Phase 33, 35 (months of data) | | 39 | Scoring Framework Abstraction | 5 | 4 | 4.5 | None | | 40 | MEWS | 5 | 5 | 5.0 | Phase 39 | --- ## Time Allocation For the first 100 hours of development: | Feature | Hours | Percentage | |---------------------------|-------|------------| | Alert Quality Analytics | 50 | 50% | | Explainable Alerts | 20 | 20% | | Alert Lifecycle Analytics | 15 | 15% | | Role-Based Alert Routing | 10 | 10% | | Design work for P37-P40 | 5 | 5% | --- # Phase 33 — Alert Quality Analytics | Dimension | Score | |----------------------|-------| | Commercial Viability | 10 | | Clinical Usage | 9 | | Composite | 9.5 | Architecture: Clinician feedback is captured per alert through a new `AlertFeedback` entity. Aggregate quality metrics are materialized into `AlertQualityMetric` snapshots by a background service that rolls up feedback, acknowledgement, and resolution data per alert type per time window. The existing `ClinicalAlert` entity gains a `FeedbackReceived` flag. All feedback writes go through `AlertService` which already owns alert state transitions. Metrics are exposed through a new read-only controller for dashboard consumption. > **Prerequisite:** Phase 31 (RBAC) for authenticated `UserId` on feedback. Phase 32 (audit logging) for PHI-safe access patterns. ### What exists After Phase 32: - `ClinicalAlert` entity with `Status` (Open, Acknowledged, Resolved, Escalated), `AcknowledgedAt`, `AcknowledgedBy`, `ResolvedAt` - `AlertService.AcknowledgeAsync()` transitions alert state and writes audit log - `AlertSuppressionService` with Redis-backed TTL suppression per encounter per alert type - `ClinicalAuditLog` for append-only audit trail - Prometheus metrics on alert suppression counts - No clinician feedback capture beyond acknowledgement - No aggregate quality metrics ### What needs to be built Seven steps, in order. --- #### Step 1 — Clinician Feedback Model **`Domains/Enums/AlertFeedbackType.cs`** (NEW): ```csharp public enum AlertFeedbackType { Useful, TooEarly, TooLate, FalsePositive, MissingContext, WouldAct } ``` **`Domains/Entities/AlertFeedback.cs`** (NEW): ```csharp public class AlertFeedback { public Guid Id { get; set; } public Guid AlertId { get; set; } public ClinicalAlert Alert { get; set; } = null!; public string UserId { get; set; } = null!; public AlertFeedbackType FeedbackType { get; set; } public string? Comment { get; set; } public DateTime CreatedAt { get; set; } } ``` EF configuration: index on `AlertId`, index on `FeedbackType` for aggregation queries. --- #### Step 2 — Alert Quality Metric Snapshot Entity **`Domains/Entities/AlertQualityMetric.cs`** (NEW): ```csharp public class AlertQualityMetric { public Guid Id { get; set; } public AlertType AlertType { get; set; } public DateTime WindowStart { get; set; } public DateTime WindowEnd { get; set; } public int TotalAlerts { get; set; } public int AcknowledgedCount { get; set; } public int ResolvedCount { get; set; } public int EscalatedCount { get; set; } public int FeedbackUsefulCount { get; set; } public int FeedbackFalsePositiveCount { get; set; } public int FeedbackWouldActCount { get; set; } public double AcknowledgementRate { get; set; } public double FalsePositiveRate { get; set; } public double UsefulRate { get; set; } public double WouldActRate { get; set; } public double AvgSecondsToAcknowledge { get; set; } public double AvgSecondsToResolution { get; set; } public DateTime ComputedAt { get; set; } } ``` Unique constraint on (`AlertType`, `WindowStart`, `WindowEnd`). Partitioned by `WindowStart` for time-range queries. --- #### Step 3 — EF Migration Single migration adding `alert_feedbacks` and `alert_quality_metrics` tables. Add `AlertFeedback` DbSet and `AlertQualityMetric` DbSet to `ApplicationDbContext`. Configure relationships: `ClinicalAlert` has many `AlertFeedback`. --- #### Step 4 — Feedback Submission Endpoint Extend `AlertService` with `SubmitFeedbackAsync(Guid alertId, AlertFeedbackType type, string? comment)`. Validates alert exists and is in `Acknowledged` or `Resolved` status. One feedback per user per alert enforced by unique constraint on (`AlertId`, `UserId`). Writes `ClinicalAuditLog` entry. **Controller:** `POST /api/alerts/{alertId}/feedback` ```json { "feedbackType": "Useful", "comment": "Caught early respiratory decline" } ``` Returns `201 Created` with feedback id. Returns `409 Conflict` if user already submitted feedback for this alert. Returns `400 Bad Request` if alert is still `Open`. --- #### Step 5 — Quality Metrics Aggregation Service **`BackgroundServices/AlertQualityAggregatorService.cs`** (NEW): Hosted service running on configurable interval (default: 1 hour). For each `AlertType`: 1. Query alerts in the time window 2. Count status transitions (acknowledged, resolved, escalated) 3. Count feedback by type 4. Calculate rates and averages 5. Upsert `AlertQualityMetric` row Uses `IServiceScopeFactory` for scoped DbContext. Prometheus gauges for each computed rate. --- #### Step 6 — Quality Metrics Read Endpoint **Controller:** `GET /api/alerts/quality-metrics` Query parameters: | Parameter | Type | Default | |-------------|----------|---------------| | alertType | string? | all types | | from | DateTime | 7 days ago | | to | DateTime | now | Returns array of `AlertQualityMetric` snapshots. Authorized for `Physician` and `Admin` roles. **Controller:** `GET /api/alerts/quality-metrics/summary` Returns current-period summary across all alert types. Single object with aggregate rates. --- #### Step 7 — Prometheus Metrics and Dashboard Integration Add Prometheus gauges: - `vigilcare_alert_acknowledgement_rate` (by alert type) - `vigilcare_alert_false_positive_rate` (by alert type) - `vigilcare_alert_useful_rate` (by alert type) - `vigilcare_alert_avg_ack_seconds` (by alert type) Grafana dashboard JSON template for alert quality overview. --- ### Verification Checklist (Phase 33) - [ ] `POST /api/alerts/{id}/feedback` accepts all six feedback types and returns 201 - [ ] Duplicate feedback from same user returns 409 - [ ] Feedback on Open alert returns 400 - [ ] Aggregator service computes correct rates for a known dataset - [ ] `GET /api/alerts/quality-metrics` returns filtered snapshots - [ ] `GET /api/alerts/quality-metrics/summary` returns aggregate rates - [ ] Prometheus metrics update after aggregation cycle - [ ] Audit log entry written for each feedback submission - [ ] Existing alert acknowledgement and resolution flows unaffected - [ ] Migration applies cleanly and seeds no data --- # Phase 34 — Explainable Alerts | Dimension | Score | |----------------------|-------| | Commercial Viability | 8 | | Clinical Usage | 10 | | Composite | 9.0 | Architecture: Each alert carries a structured explanation payload alongside the existing `Details` string. A new `AlertExplanation` value object captures score contributors, trend context, and medication context at alert creation time. Scoring consumers (NEWS2, SOFA, GCS) emit contributor breakdowns. The `TrendDetector` attaches trend summaries. `MedicationCorrelationHelper` attaches recent medication context. All explanation data is serialized as JSONB on the `ClinicalAlert` row so the explanation is immutable — it reflects the state at alert time, not query time. > **Prerequisite:** Phase 33 for feedback loop. No hard technical dependency but sequencing allows feedback data to inform which explanations clinicians value. ### What exists After Phase 33: - `ClinicalAlert.Details` is a free-text string, sometimes containing score values - `TrendDetector` returns `TrendOutcome` enum but no narrative description - `MedicationCorrelationHelper.TryAnnotateDetailsAsync()` appends drug info to the details string - NEWS2, SOFA, GCS scoring returns aggregate scores but not per-component breakdowns to the alert layer - No structured explanation model ### What needs to be built Six steps, in order. --- #### Step 1 — Explanation Value Objects **`Domains/ValueObjects/AlertExplanation.cs`** (NEW): ```csharp public class AlertExplanation { public List ScoreContributors { get; set; } = new(); public TrendContext? Trend { get; set; } public MedicationContext? MedicationContext { get; set; } public string NarrativeSummary { get; set; } = string.Empty; } public class ScoreContributor { public string Parameter { get; set; } = null!; public int Points { get; set; } public string? RawValue { get; set; } public string? NormalRange { get; set; } } public class TrendContext { public string Parameter { get; set; } = null!; public double PercentChange { get; set; } public TimeSpan Duration { get; set; } public string Direction { get; set; } = null!; } public class MedicationContext { public string DrugName { get; set; } = null!; public string Dose { get; set; } = null!; public string Route { get; set; } = null!; public DateTime AdministeredAt { get; set; } public string? RelevanceNote { get; set; } } ``` --- #### Step 2 — ClinicalAlert JSONB Column Add `Explanation` JSONB column to `ClinicalAlert`. EF migration adds nullable `explanation` column of type `jsonb`. EF configuration uses `HasColumnType("jsonb")`. Existing alerts have null explanation — no backfill required. --- #### Step 3 — Scoring Consumer Contributor Extraction Modify NEWS2, SOFA, and GCS scoring result types to include per-component breakdowns: - NEWS2: return `List` with each vital sign parameter and its contribution - SOFA: return `List` with each organ system and its score - GCS: return `List` with Eye, Verbal, Motor components Each scoring consumer builds the contributor list and passes it through to alert creation. The scoring algorithms themselves do not change — only the return shape expands to include what was already computed internally. --- #### Step 4 — Trend Narrative Builder Extend `TrendDetector` to return `TrendContext` alongside `TrendOutcome`. When `RapidDeterioration` is detected, populate: - Parameter name - Percent change over window - Duration of trend - Direction (increasing/decreasing) No change to the detection logic. The trend context is attached to the alert explanation at creation time. --- #### Step 5 — Explanation Assembly in Alert Creation At alert creation time (wherever `ClinicalAlert` is instantiated), assemble `AlertExplanation`: 1. Attach score contributors from the scoring result 2. Attach trend context from trend detector if applicable 3. Attach medication context from `MedicationCorrelationHelper` if applicable 4. Generate `NarrativeSummary` — a single human-readable sentence combining the above Example narrative: "NEWS2 8 — respiratory rate (+3), SpO2 (+2), supplemental O2 (+2), temperature (+1). Respiratory rate increased 37% over 2 hours. Paracetamol 1g IV administered 45 minutes ago." --- #### Step 6 — Explanation in API Responses Extend alert GET endpoints to include the `Explanation` object. The explanation is read-only and immutable. Dashboard can render contributors as a breakdown chart and trend as a sparkline annotation. ```json { "alertId": "...", "alertType": "News2HighRisk", "severity": "Critical", "explanation": { "scoreContributors": [ { "parameter": "Respiratory Rate", "points": 3, "rawValue": "28", "normalRange": "12-20" }, { "parameter": "SpO2", "points": 2, "rawValue": "92%", "normalRange": "96-100%" } ], "trend": { "parameter": "Respiratory Rate", "percentChange": 37.0, "duration": "02:00:00", "direction": "Increasing" }, "medicationContext": { "drugName": "Paracetamol", "dose": "1g", "route": "IV", "administeredAt": "2026-06-22T10:15:00Z", "relevanceNote": "Antipyretic — may affect temperature trend" }, "narrativeSummary": "NEWS2 8 — respiratory rate (+3), SpO2 (+2). Respiratory rate increased 37% over 2 hours." } } ``` --- ### Verification Checklist (Phase 34) - [ ] NEWS2 alert includes per-component score contributors in explanation - [ ] SOFA alert includes per-organ-system contributors - [ ] GCS alert includes Eye/Verbal/Motor breakdown - [ ] Trend-triggered alerts include trend context with percent change and duration - [ ] Medication-correlated alerts include drug context - [ ] Narrative summary is human-readable and accurate - [ ] Existing alerts with null explanation still serialize correctly - [ ] Alert GET endpoints return explanation object - [ ] No change to scoring algorithm outputs (same scores, same thresholds) - [ ] Migration applies cleanly on existing data --- # Phase 35 — Alert Lifecycle Analytics | Dimension | Score | |----------------------|-------| | Commercial Viability | 9 | | Clinical Usage | 7 | | Composite | 8.0 | Architecture: Every state transition on a `ClinicalAlert` writes an append-only row to `alert_lifecycle_events`. A background aggregation service materializes lifecycle metrics per alert type per time window into `alert_lifecycle_metrics`. This shares the aggregation pattern from Phase 33 but tracks timing and workflow progression rather than clinician feedback. Together with Phase 33 quality metrics, this provides the complete alert effectiveness picture. > **Prerequisite:** Phase 33 (Alert Quality Analytics). Shares aggregation infrastructure and metric exposure patterns. ### What exists After Phase 34: - `ClinicalAlert` has `Status` transitions: Open → Acknowledged → Resolved, Open → Escalated → Acknowledged → Resolved - `AcknowledgedAt`, `ResolvedAt` timestamps exist on the entity - `ClinicalAuditLog` captures some transitions but is not structured for time-series analytics - No dedicated lifecycle event log - No computed lifecycle metrics (median ack time, escalation rate) ### What needs to be built Five steps, in order. --- #### Step 1 — Lifecycle Event Entity **`Domains/Entities/AlertLifecycleEvent.cs`** (NEW): ```csharp public class AlertLifecycleEvent { public Guid Id { get; set; } public Guid AlertId { get; set; } public ClinicalAlert Alert { get; set; } = null!; public AlertStatus FromStatus { get; set; } public AlertStatus ToStatus { get; set; } public string UserId { get; set; } = null!; public DateTime OccurredAt { get; set; } public string? Metadata { get; set; } } ``` EF configuration: index on (`AlertId`, `OccurredAt`), index on (`ToStatus`, `OccurredAt`) for aggregation. --- #### Step 2 — Lifecycle Event Emission Modify `AlertService` to write an `AlertLifecycleEvent` on every status transition: - Alert creation: `None → Open` - Acknowledgement: `Open → Acknowledged` or `Escalated → Acknowledged` - Resolution: `Acknowledged → Resolved` - Escalation: `Open → Escalated` Each write is within the same transaction as the status update. No additional round trips. --- #### Step 3 — Lifecycle Metric Snapshot Entity **`Domains/Entities/AlertLifecycleMetric.cs`** (NEW): ```csharp public class AlertLifecycleMetric { public Guid Id { get; set; } public AlertType AlertType { get; set; } public DateTime WindowStart { get; set; } public DateTime WindowEnd { get; set; } public int TotalAlerts { get; set; } public double MedianSecondsToAcknowledge { get; set; } public double MedianSecondsToAction { get; set; } public double EscalationRate { get; set; } public double BundleCompletionRate { get; set; } public double P90SecondsToAcknowledge { get; set; } public double P90SecondsToAction { get; set; } public int UnacknowledgedCount { get; set; } public DateTime ComputedAt { get; set; } } ``` --- #### Step 4 — Lifecycle Aggregation Service **`BackgroundServices/AlertLifecycleAggregatorService.cs`** (NEW): Hosted service running on configurable interval (default: 1 hour). For each `AlertType`: 1. Query lifecycle events in the time window 2. Compute median and P90 times from Open → Acknowledged 3. Compute median and P90 times from Acknowledged → Resolved (action time) 4. Compute escalation rate (escalated / total) 5. Compute bundle completion rate for sepsis alerts 6. Count unacknowledged alerts past threshold 7. Upsert `AlertLifecycleMetric` row Can share the aggregation scheduling infrastructure with Phase 33's `AlertQualityAggregatorService`. Both run in the same interval but compute different metric sets. --- #### Step 5 — Lifecycle Metrics Endpoints and Prometheus **Controller:** `GET /api/alerts/lifecycle-metrics` Query parameters: `alertType`, `from`, `to`. Same pattern as Phase 33 quality metrics. Prometheus gauges: - `vigilcare_alert_median_ack_seconds` (by alert type) - `vigilcare_alert_median_action_seconds` (by alert type) - `vigilcare_alert_escalation_rate` (by alert type) - `vigilcare_alert_unacknowledged_count` (by alert type) --- ### Verification Checklist (Phase 35) - [ ] Alert creation writes lifecycle event with `None → Open` - [ ] Acknowledgement writes lifecycle event with correct from/to status - [ ] Escalation writes lifecycle event - [ ] Resolution writes lifecycle event - [ ] Lifecycle events are in same transaction as status update - [ ] Aggregator computes correct median and P90 for known dataset - [ ] `GET /api/alerts/lifecycle-metrics` returns filtered snapshots - [ ] Prometheus gauges update after aggregation cycle - [ ] Existing alert flows unaffected (same behavior, additional event write) - [ ] Migration applies cleanly --- # Phase 36 — Role-Based Alert Routing | Dimension | Score | |----------------------|-------| | Commercial Viability | 8 | | Clinical Usage | 9 | | Composite | 8.5 | Architecture: A configurable routing rules engine determines which roles receive which alert types at which severity levels. Rules are stored in the database and cached in Redis. The existing `EscalationWorkerService` RabbitMQ consumer is extended to route alerts through the rules engine before dispatching notifications. Each facility can configure its own rule set through an admin endpoint. Default rule sets ship with sensible mappings that customers can override. > **Prerequisite:** Phase 31 (RBAC) for role definitions. Phases 33 and 35 for proving routing effectiveness with metrics. ### What exists After Phase 35: - `EscalationWorkerService` dispatches to `alerts.escalation.queue` — all escalated alerts go to same destination - RabbitMQ notification infrastructure exists - Phase 31 RBAC roles: `Nurse`, `Physician`, `Admin`, `Integration` - No per-role filtering on alert dispatch - No configurable routing rules ### What needs to be built Six steps, in order. --- #### Step 1 — Alert Routing Rule Entity **`Domains/Entities/AlertRoutingRule.cs`** (NEW): ```csharp public class AlertRoutingRule { public Guid Id { get; set; } public string FacilityId { get; set; } = null!; public AlertType AlertType { get; set; } public AlertSeverity MinSeverity { get; set; } public string TargetRole { get; set; } = null!; public bool IsEnabled { get; set; } = true; public int Priority { get; set; } public DateTime CreatedAt { get; set; } public DateTime UpdatedAt { get; set; } } ``` Unique constraint on (`FacilityId`, `AlertType`, `TargetRole`). Index on (`FacilityId`, `IsEnabled`). --- #### Step 2 — Default Routing Rules Seed Migration seeds default rules: | Alert Type | Severity | Target Role | |----------------------|----------|-------------| | NEWS2 Warning | Warning | Nurse | | Rapid Deterioration | Warning | Nurse | | NEWS2 High Risk | Critical | Nurse, Physician | | SOFA Sepsis | Critical | Physician | | NEWS2 Emergency | Critical | Physician | | Escalated (any) | Critical | Physician | | Bundle Compliance | Warning | Admin | Default facility ID: `"default"`. Facilities without custom rules fall back to default. --- #### Step 3 — Routing Rules Cache **`Services/AlertRoutingCacheService.cs`** (NEW): Redis-backed cache of routing rules per facility. Cache key: `routing:{facilityId}`. TTL: 10 minutes. Write-through invalidation on rule update. Falls back to `"default"` facility rules when no facility-specific rules exist. --- #### Step 4 — Routing Engine **`Services/AlertRoutingEngine.cs`** (NEW): ```csharp public interface IAlertRoutingEngine { Task> GetTargetRolesAsync( string facilityId, AlertType alertType, AlertSeverity severity); } ``` Evaluates rules in priority order. Returns list of roles that should receive the alert. Used by `EscalationWorkerService` and any future notification dispatch point. --- #### Step 5 — Integrate with Notification Dispatch Modify `EscalationWorkerService` to call `IAlertRoutingEngine.GetTargetRolesAsync()` before dispatching. Route to role-specific RabbitMQ queues: - `alerts.nurse.queue` - `alerts.physician.queue` - `alerts.admin.queue` Existing `alerts.escalation.queue` remains as catch-all for unmatched alerts. --- #### Step 6 — Routing Rules Admin Endpoint **Controller:** `GET /api/admin/alert-routing-rules?facilityId={id}` **Controller:** `PUT /api/admin/alert-routing-rules` **Controller:** `DELETE /api/admin/alert-routing-rules/{id}` Authorized for `Admin` role only. PUT upserts a rule and invalidates the Redis cache. Audit log entry on every change. --- ### Verification Checklist (Phase 36) - [ ] Default rules seed correctly on migration - [ ] Routing engine returns correct roles for each alert type/severity combination - [ ] Facility-specific rules override defaults - [ ] Cache invalidates on rule update - [ ] Cache falls back to default when facility has no custom rules - [ ] Escalation worker routes to role-specific queues - [ ] Unmatched alerts fall through to catch-all queue - [ ] Admin endpoint requires Admin role - [ ] Audit log written on rule changes - [ ] Existing escalation behavior preserved when no routing rules exist --- # Phase 37 — Alert Bundling and Correlation | Dimension | Score | |----------------------|-------| | Commercial Viability | 7 | | Clinical Usage | 8 | | Composite | 7.5 | Architecture: A correlation engine groups alerts that fire within a configurable time window for the same patient into a single clinical narrative. Correlated alerts are linked by a shared `CorrelationGroupId`. The first alert in a group becomes the primary; subsequent alerts within the window attach as secondary. The group carries a composite explanation built from Phase 34 individual explanations. Clinicians see one bundled notification with the full picture instead of multiple independent alerts. > **Prerequisite:** Phase 34 (Explainable Alerts) for structured explanations to compose into bundles. ### What exists After Phase 36: - Alerts fire independently per scoring consumer and trend detector - No correlation between simultaneous alerts for the same patient - Phase 34 `AlertExplanation` provides structured per-alert context - `AlertSuppressionService` prevents duplicate alert types but not cross-type bundling - Medication correlation annotates individual alerts but does not group them ### What needs to be built Five steps, in order. --- #### Step 1 — Correlation Group Entity **`Domains/Entities/AlertCorrelationGroup.cs`** (NEW): ```csharp public class AlertCorrelationGroup { public Guid Id { get; set; } public Guid EncounterId { get; set; } public Guid PatientId { get; set; } public Guid PrimaryAlertId { get; set; } public ClinicalAlert PrimaryAlert { get; set; } = null!; public List CorrelatedAlerts { get; set; } = new(); public string CompositeSummary { get; set; } = null!; public AlertSeverity HighestSeverity { get; set; } public DateTime WindowStart { get; set; } public DateTime WindowEnd { get; set; } public DateTime CreatedAt { get; set; } } ``` Add nullable `CorrelationGroupId` FK to `ClinicalAlert`. --- #### Step 2 — Correlation Window Configuration **`Configuration/AlertCorrelationOptions.cs`** (NEW): ```csharp public class AlertCorrelationOptions { public const string Section = "AlertCorrelation"; public int WindowSeconds { get; set; } = 300; public int MinAlertsToBundle { get; set; } = 2; } ``` Default 5-minute window. Alerts for the same encounter within the window are candidates for correlation. --- #### Step 3 — Correlation Engine **`Services/AlertCorrelationEngine.cs`** (NEW): On alert creation: 1. Query for open `AlertCorrelationGroup` for this encounter within the active window 2. If group exists: attach alert as secondary, update `HighestSeverity`, regenerate `CompositeSummary` 3. If no group: check for other recent alerts for this encounter within window. If count >= `MinAlertsToBundle`, create group with earliest alert as primary 4. If standalone: no group created, alert dispatches normally The composite summary merges individual `AlertExplanation.NarrativeSummary` entries into a unified clinical picture. --- #### Step 4 — Bundled Notification Format Modify notification dispatch (Phase 36 routing) to send bundled notifications when a correlation group exists: ```json { "correlationGroupId": "...", "patientId": "...", "highestSeverity": "Critical", "alertCount": 3, "compositeSummary": "Deterioration detected: NEWS2 rose to 7, driven by respiratory decline over 2 hours. SOFA increased by 2. Rapid deterioration in respiratory rate (37% over 2h).", "alerts": [ { "alertId": "...", "alertType": "News2HighRisk", "severity": "Critical" }, { "alertId": "...", "alertType": "SofaSepsis", "severity": "Critical" }, { "alertId": "...", "alertType": "RapidDeterioration", "severity": "Warning" } ], "recommendedActions": [ "Escalate to physician", "Initiate sepsis screening bundle" ] } ``` --- #### Step 5 — Correlation Group API **Controller:** `GET /api/alerts/correlation-groups/{id}` **Controller:** `GET /api/encounters/{encounterId}/alert-groups` Returns correlation group with all linked alerts and composite explanation. Dashboard renders as a single card with expandable individual alert details. --- ### Verification Checklist (Phase 37) - [ ] Two alerts for same encounter within window create a correlation group - [ ] Third alert within window attaches to existing group - [ ] Alert outside window creates new standalone alert - [ ] Composite summary merges individual explanations coherently - [ ] Highest severity reflects the most severe alert in the group - [ ] Bundled notification dispatches as single message - [ ] Standalone alerts still dispatch individually - [ ] Correlation group API returns full linked alert details - [ ] Suppressed alerts do not enter correlation groups - [ ] Migration adds nullable FK without breaking existing alerts --- # Phase 38 — Adaptive Threshold Recommendations | Dimension | Score | |----------------------|-------| | Commercial Viability | 9 | | Clinical Usage | 6 | | Composite | 7.5 | Architecture: A recommendation engine analyzes accumulated quality metrics (Phase 33) and lifecycle metrics (Phase 35) to suggest threshold adjustments for alert types with poor performance. Recommendations are human-reviewed and human-approved. The system never automatically changes thresholds. Each recommendation includes the evidence (metrics over time), the proposed change, and the projected impact. Approved recommendations apply to the threshold cache and are audit-logged. > **Prerequisite:** Phases 33 and 35 must have accumulated a minimum data volume (configurable, default 30 days of metrics). This phase cannot ship until sufficient real-world data exists. ### What exists After Phase 37: - `AlertQualityMetric` snapshots per alert type per time window (Phase 33) - `AlertLifecycleMetric` snapshots per alert type per time window (Phase 35) - Threshold values stored in Redis cache via existing threshold management - No analysis of metric trends to identify poorly performing thresholds - No recommendation workflow ### What needs to be built Five steps, in order. --- #### Step 1 — Threshold Recommendation Entity **`Domains/Entities/ThresholdRecommendation.cs`** (NEW): ```csharp public class ThresholdRecommendation { public Guid Id { get; set; } public AlertType AlertType { get; set; } public string ParameterName { get; set; } = null!; public double CurrentThreshold { get; set; } public double RecommendedThreshold { get; set; } public string Rationale { get; set; } = null!; public double CurrentFalsePositiveRate { get; set; } public double CurrentUsefulRate { get; set; } public double ProjectedFalsePositiveRate { get; set; } public double ProjectedUsefulRate { get; set; } public int DataPointCount { get; set; } public DateTime AnalysisPeriodStart { get; set; } public DateTime AnalysisPeriodEnd { get; set; } public ThresholdRecommendationStatus Status { get; set; } public string? ReviewedBy { get; set; } public DateTime? ReviewedAt { get; set; } public DateTime CreatedAt { get; set; } } ``` **`Domains/Enums/ThresholdRecommendationStatus.cs`** (NEW): ```csharp public enum ThresholdRecommendationStatus { Pending, Approved, Dismissed, Applied, Expired } ``` --- #### Step 2 — Recommendation Analysis Service **`BackgroundServices/ThresholdRecommendationService.cs`** (NEW): Hosted service running daily. For each alert type: 1. Query quality metrics for the analysis period (default 30 days) 2. Identify alert types with false positive rate > configurable threshold (default 40%) 3. Identify alert types with useful rate < configurable threshold (default 50%) 4. Calculate recommended threshold adjustment based on the distribution of values that triggered false positives versus useful alerts 5. Generate `ThresholdRecommendation` with rationale and projections 6. Only generate if data point count exceeds minimum (default 100 alerts) Conservative approach: recommendations move thresholds by at most one step per cycle. --- #### Step 3 — Review and Approval Workflow **Controller:** `GET /api/admin/threshold-recommendations` **Controller:** `POST /api/admin/threshold-recommendations/{id}/approve` **Controller:** `POST /api/admin/threshold-recommendations/{id}/dismiss` Authorized for `Physician` and `Admin` roles. Approval triggers threshold update in Redis cache and writes audit log. Dismiss records the decision without changing thresholds. Approval response includes before/after comparison and requires confirmation. --- #### Step 4 — Threshold Application On approval: 1. Update threshold value in Redis cache 2. Write `ClinicalAuditLog` entry with old value, new value, recommendation ID 3. Mark recommendation as `Applied` 4. Emit Kafka event for downstream consumers to pick up new threshold Rollback: Admin can dismiss an applied recommendation, which reverts to previous threshold and marks status as `Dismissed`. --- #### Step 5 — Recommendation Dashboard Data **Controller:** `GET /api/admin/threshold-recommendations/history` Returns history of all recommendations with outcomes. Allows tracking whether approved changes actually improved metrics in subsequent periods. Includes before/after metric comparison. --- ### Verification Checklist (Phase 38) - [ ] Recommendation service skips alert types with insufficient data - [ ] Recommendations include accurate current metrics and projections - [ ] Recommendations limited to one-step adjustments - [ ] Approval updates Redis threshold cache - [ ] Approval writes audit log with old/new values - [ ] Dismissal records decision without threshold change - [ ] Applied recommendations can be reverted - [ ] Recommendation history shows before/after metric impact - [ ] No recommendations generated before minimum data threshold - [ ] Existing threshold behavior unchanged without approved recommendations --- # Phase 39 — Scoring Framework Abstraction | Dimension | Score | |----------------------|-------| | Commercial Viability | 5 | | Clinical Usage | 4 | | Composite | 4.5 | Architecture: Refactor existing scoring implementations (NEWS2, qSOFA, SOFA, GCS) behind a common `IScoringEngine` interface. Each scoring algorithm becomes a pluggable module with standardized input/output contracts. The alert pipeline consumes scoring results through the unified interface. New scoring algorithms can be added by implementing the interface and registering in DI. This is a pure refactor — no behavioral changes to existing scoring. > **Prerequisite:** None. Can be done independently but logically follows the alert optimization phases to avoid disrupting active development. ### What exists After Phase 38: - NEWS2 scoring implementation in dedicated service - SOFA scoring implementation in dedicated service - qSOFA scoring implementation in dedicated service - GCS scoring implementation in dedicated service - Each has different input shapes, output shapes, and integration points - Phase 34 added contributor extraction but each scoring system returns it differently ### What needs to be built Four steps, in order. --- #### Step 1 — Scoring Contracts **`Scoring/IScoringEngine.cs`** (NEW): ```csharp public interface IScoringEngine { string ScoringSystem { get; } ScoreResult Calculate(ScoringInput input); IReadOnlyList RequiredParameters { get; } IReadOnlyList OptionalParameters { get; } } public class ScoringInput { public Guid EncounterId { get; set; } public Guid PatientId { get; set; } public Dictionary Observations { get; set; } = new(); public Dictionary? Context { get; set; } } public class ObservationValue { public double NumericValue { get; set; } public DateTime RecordedAt { get; set; } public string? Unit { get; set; } } public class ScoreResult { public string ScoringSystem { get; set; } = null!; public int TotalScore { get; set; } public string RiskLevel { get; set; } = null!; public List Contributors { get; set; } = new(); public List MissingParameters { get; set; } = new(); public bool IsComplete { get; set; } public DateTime ComputedAt { get; set; } } ``` --- #### Step 2 — Adapt Existing Scoring Implementations Wrap each existing scoring service behind `IScoringEngine`: - `News2ScoringEngine : IScoringEngine` - `SofaScoringEngine : IScoringEngine` - `QSofaScoringEngine : IScoringEngine` - `GcsScoringEngine : IScoringEngine` Internal logic unchanged. Adapters translate between existing input/output shapes and the unified contract. Existing direct consumers continue to work during migration. --- #### Step 3 — Scoring Engine Registry **`Scoring/ScoringEngineRegistry.cs`** (NEW): ```csharp public interface IScoringEngineRegistry { IScoringEngine GetEngine(string scoringSystem); IReadOnlyList GetAllEngines(); } ``` DI registration scans for `IScoringEngine` implementations. Alert pipeline can request any scoring engine by name. --- #### Step 4 — Migrate Alert Pipeline to Registry Update alert creation pipeline to resolve scoring engines through `IScoringEngineRegistry` instead of direct service injection. Phase 34 contributor extraction now uses the standardized `ScoreResult.Contributors` instead of per-system extraction. Deprecate direct scoring service injection in alert consumers. Existing tests updated to use registry. --- ### Verification Checklist (Phase 39) - [ ] All four scoring engines implement `IScoringEngine` - [ ] Registry returns correct engine by name - [ ] NEWS2 scores identical before and after refactor - [ ] SOFA scores identical before and after refactor - [ ] qSOFA scores identical before and after refactor - [ ] GCS scores identical before and after refactor - [ ] Alert pipeline uses registry instead of direct injection - [ ] Phase 34 contributor extraction works through unified interface - [ ] Missing parameter handling consistent across engines - [ ] All existing scoring tests pass without modification --- # Phase 40 — MEWS | Dimension | Score | |----------------------|-------| | Commercial Viability | 5 | | Clinical Usage | 5 | | Composite | 5.0 | Architecture: MEWS (Modified Early Warning Score) is implemented as a new `IScoringEngine` module using the Phase 39 framework. It scores five parameters (systolic blood pressure, heart rate, respiratory rate, temperature, AVPU/consciousness level) on a 0-3 scale each (total 0-15). Alert thresholds follow published MEWS guidelines. This is the first external consumer of the scoring framework, validating the abstraction. > **Prerequisite:** Phase 39 (Scoring Framework Abstraction) complete. ### What exists After Phase 39: - `IScoringEngine` interface and `IScoringEngineRegistry` - Four existing scoring engines registered - Unified `ScoreResult` with contributors - Alert pipeline consuming through registry ### What needs to be built Four steps, in order. --- #### Step 1 — MEWS Scoring Engine **`Scoring/MewsScoringEngine.cs`** (NEW): ```csharp public class MewsScoringEngine : IScoringEngine { public string ScoringSystem => "MEWS"; public IReadOnlyList RequiredParameters => new[] { "SystolicBloodPressure", "HeartRate", "RespiratoryRate", "Temperature", "ConsciousnessLevel" }; public IReadOnlyList OptionalParameters => Array.Empty(); } ``` MEWS scoring table: | Score | SBP (mmHg) | HR (bpm) | RR (breaths/min) | Temp (°C) | Consciousness | |-------|-----------|----------|-------------------|-----------|---------------| | 0 | 101-199 | 51-100 | 9-14 | 35.0-38.4 | Alert | | 1 | 81-100 | 41-50 | 15-20 | < 35.0 | Reacts Voice | | 2 | 71-80 | 101-110 | 21-29 | ≥ 38.5 | Reacts Pain | | 3 | ≤ 70 or ≥ 200 | < 40 or > 110 | < 9 or ≥ 30 | — | Unresponsive | --- #### Step 2 — MEWS Alert Types and Thresholds Add to `AlertType` enum: ```csharp MewsLowRisk, // MEWS 0-2 MewsMediumRisk, // MEWS 3-4 MewsHighRisk, // MEWS 5+ ``` Threshold defaults: | Risk Level | Score Range | Severity | Action | |------------|-------------|----------|-------------------------------| | Low | 0-2 | — | Routine monitoring | | Medium | 3-4 | Warning | Increase monitoring frequency | | High | 5+ | Critical | Immediate clinical review | --- #### Step 3 — MEWS Kafka Consumer **`BackgroundServices/MewsScoringConsumer.cs`** (NEW): Kafka consumer on `ObservationRecorded` topic, consumer group `mews-scorer`. Collects required observations from Redis cache, calculates MEWS through `IScoringEngineRegistry`, creates alert if threshold met. Same pattern as existing NEWS2 consumer. MEWS scoring is opt-in per facility. Configuration flag `Scoring:MewsEnabled` defaults to `false`. Consumer skips processing when disabled. --- #### Step 4 — MEWS Integration with Alert Optimization Stack - Phase 33: MEWS alert types included in quality metrics aggregation automatically - Phase 34: MEWS contributor breakdown via `ScoreResult.Contributors` - Phase 35: MEWS lifecycle events tracked via same pipeline - Phase 36: Default routing rules seeded for MEWS alert types - Phase 37: MEWS alerts eligible for correlation grouping No additional work required — the optimization stack handles MEWS generically through the alert type enum. --- ### Verification Checklist (Phase 40) - [ ] MEWS engine registered in `IScoringEngineRegistry` - [ ] Scoring matches published MEWS tables for all parameter combinations - [ ] MEWS alerts created at correct thresholds - [ ] MEWS opt-in flag prevents scoring when disabled - [ ] Quality metrics aggregation includes MEWS alert types - [ ] Explainable alerts work for MEWS (five contributors) - [ ] Lifecycle tracking works for MEWS alerts - [ ] Routing rules include MEWS defaults - [ ] Correlation engine bundles MEWS with other simultaneous alerts - [ ] Existing scoring systems unaffected by MEWS addition --- ## Research References - PMC6748819: Alert fatigue review - PMID 31206159: CDS/ML alert optimization - PMC11845892: Alert usability and human factors