43 KiB
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
UserIdon feedback. Phase 32 (audit logging) for PHI-safe access patterns.
What exists
After Phase 32:
ClinicalAlertentity withStatus(Open, Acknowledged, Resolved, Escalated),AcknowledgedAt,AcknowledgedBy,ResolvedAtAlertService.AcknowledgeAsync()transitions alert state and writes audit logAlertSuppressionServicewith Redis-backed TTL suppression per encounter per alert typeClinicalAuditLogfor 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):
public enum AlertFeedbackType
{
Useful,
TooEarly,
TooLate,
FalsePositive,
MissingContext,
WouldAct
}
Domains/Entities/AlertFeedback.cs (NEW):
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):
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
{
"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:
- Query alerts in the time window
- Count status transitions (acknowledged, resolved, escalated)
- Count feedback by type
- Calculate rates and averages
- Upsert
AlertQualityMetricrow
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}/feedbackaccepts 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-metricsreturns filtered snapshotsGET /api/alerts/quality-metrics/summaryreturns 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.Detailsis a free-text string, sometimes containing score valuesTrendDetectorreturnsTrendOutcomeenum but no narrative descriptionMedicationCorrelationHelper.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):
public class AlertExplanation
{
public List<ScoreContributor> 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<ScoreContributor>with each vital sign parameter and its contribution - SOFA: return
List<ScoreContributor>with each organ system and its score - GCS: return
List<ScoreContributor>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:
- Attach score contributors from the scoring result
- Attach trend context from trend detector if applicable
- Attach medication context from
MedicationCorrelationHelperif applicable - 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.
{
"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:
ClinicalAlerthasStatustransitions: Open → Acknowledged → Resolved, Open → Escalated → Acknowledged → ResolvedAcknowledgedAt,ResolvedAttimestamps exist on the entityClinicalAuditLogcaptures 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):
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 → AcknowledgedorEscalated → 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):
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:
- Query lifecycle events in the time window
- Compute median and P90 times from Open → Acknowledged
- Compute median and P90 times from Acknowledged → Resolved (action time)
- Compute escalation rate (escalated / total)
- Compute bundle completion rate for sepsis alerts
- Count unacknowledged alerts past threshold
- Upsert
AlertLifecycleMetricrow
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-metricsreturns 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:
EscalationWorkerServicedispatches toalerts.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):
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):
public interface IAlertRoutingEngine
{
Task<IReadOnlyList<string>> 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.queuealerts.physician.queuealerts.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
AlertExplanationprovides structured per-alert context AlertSuppressionServiceprevents 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):
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<ClinicalAlert> 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):
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:
- Query for open
AlertCorrelationGroupfor this encounter within the active window - If group exists: attach alert as secondary, update
HighestSeverity, regenerateCompositeSummary - If no group: check for other recent alerts for this encounter within window. If count >=
MinAlertsToBundle, create group with earliest alert as primary - 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:
{
"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:
AlertQualityMetricsnapshots per alert type per time window (Phase 33)AlertLifecycleMetricsnapshots 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):
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):
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:
- Query quality metrics for the analysis period (default 30 days)
- Identify alert types with false positive rate > configurable threshold (default 40%)
- Identify alert types with useful rate < configurable threshold (default 50%)
- Calculate recommended threshold adjustment based on the distribution of values that triggered false positives versus useful alerts
- Generate
ThresholdRecommendationwith rationale and projections - 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:
- Update threshold value in Redis cache
- Write
ClinicalAuditLogentry with old value, new value, recommendation ID - Mark recommendation as
Applied - 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):
public interface IScoringEngine
{
string ScoringSystem { get; }
ScoreResult Calculate(ScoringInput input);
IReadOnlyList<string> RequiredParameters { get; }
IReadOnlyList<string> OptionalParameters { get; }
}
public class ScoringInput
{
public Guid EncounterId { get; set; }
public Guid PatientId { get; set; }
public Dictionary<string, ObservationValue> Observations { get; set; } = new();
public Dictionary<string, object>? 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<ScoreContributor> Contributors { get; set; } = new();
public List<string> 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 : IScoringEngineSofaScoringEngine : IScoringEngineQSofaScoringEngine : IScoringEngineGcsScoringEngine : 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):
public interface IScoringEngineRegistry
{
IScoringEngine GetEngine(string scoringSystem);
IReadOnlyList<IScoringEngine> 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:
IScoringEngineinterface andIScoringEngineRegistry- Four existing scoring engines registered
- Unified
ScoreResultwith contributors - Alert pipeline consuming through registry
What needs to be built
Four steps, in order.
Step 1 — MEWS Scoring Engine
Scoring/MewsScoringEngine.cs (NEW):
public class MewsScoringEngine : IScoringEngine
{
public string ScoringSystem => "MEWS";
public IReadOnlyList<string> RequiredParameters => new[]
{
"SystolicBloodPressure",
"HeartRate",
"RespiratoryRate",
"Temperature",
"ConsciousnessLevel"
};
public IReadOnlyList<string> OptionalParameters => Array.Empty<string>();
}
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:
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