Files
vigilcare-clinical/docs/alert-optimization-roadmap.md
2026-06-25 00:46:54 +08:00

45 KiB
Raw Permalink Blame History

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 Status
33 Alert Quality Analytics 10 9 9.5 None
34 Explainable Alerts 8 10 9.0 None Complete
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):

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:

  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

Status: Complete — see phase-34-plan.md for full implementation detail, tests, and file inventory.

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 34 (implemented):

  • AlertExplanation value object with ScoreContributor, TrendContext, MedicationContext, and NarrativeSummary
  • ClinicalAlert.Explanation — nullable JSONB column, immutable at alert creation
  • NEWS2, SOFA, GCS, and Trend detectors assemble explanation at alert creation; alert.generated outbox/Kafka payloads include explanation
  • MedicationCorrelationHelper.TryGetContextAsync() — structured medication context (replaces string-append to Details)
  • Scoring results carry contributors: News2Result, SofaScoringResult, GcsResult, TrendResult
  • AlertResponse DTO with Explanation; GET/list/acknowledge/resolve endpoints return it
  • Dashboard AlertReasoning.vue and alert surfaces consume structured explanation; legacy alerts fall back to Details
  • Simulator AlertResponse.Explanation + ExpectedOutcomeValidator with narrativeContains on key scenarios
  • Elasticsearch indexes NarrativeSummary; data lake Parquet includes explanation_json; ward gateway sync forwards explanation
  • ExplainableAlertsTests (10 tests) + scripts/run-phase34-verification.sh
  • Details string unchanged for backward compatibility with threshold-only and legacy alerts

What needs to be built

Nothing — Phase 34 is complete. Next: Phase 35 — Alert Lifecycle Analytics.

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
  • Dashboard and simulator consume explanation
  • Downstream consumers (ES, data lake, gateway sync) include explanation fields

Implementation reference (Steps 17)

The step-by-step design below is retained for interview prep and onboarding. All steps are implemented; see phase-34-plan.md for code paths and tests.

Original step-by-step design (Steps 16)

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:

  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.

{
    "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."
    }
}

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
  • ClinicalAlert.Explanation JSONB with immutable score contributors, trend, and medication context at alert time
  • AlertResponse exposes explanation to dashboard and simulator; Details retained for legacy consumers
  • 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):

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):

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):

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.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) complete — structured explanations compose into bundle narratives.

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 shipped — per-alert NarrativeSummary, score contributors, trend, and medication context available on AlertResponse
  • AlertSuppressionService prevents duplicate alert types but not cross-type bundling
  • Medication correlation attaches structured context to 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:

  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:

{
    "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):

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:

  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 (implemented — unify via ScoreResult.Contributors in Phase 39)

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 : 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):

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:

  • 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):

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