public static class ExpectedOutcomeValidator { private static readonly TimeSpan AsyncSettleDelay = TimeSpan.FromSeconds(8); public static async Task> ValidateAsync( VigilCareApiClient client, Guid encounterId, ScenarioFile scenario, CancellationToken ct = default) { if (scenario.ExpectedOutcomes is not { Count: > 0 }) return []; await Task.Delay(AsyncSettleDelay, ct); var failures = new List(); var alerts = await client.GetAlertsAsync(encounterId); foreach (var outcome in scenario.ExpectedOutcomes) { switch (outcome.Type) { case "alert": failures.AddRange(ValidateAlertOutcome(outcome, alerts, scenario.Scenario.Id)); break; case "score": failures.AddRange(await ValidateScoreOutcomeAsync(client, encounterId, outcome, scenario.Scenario.Id, ct)); break; case "bundle": failures.AddRange(await ValidateBundleOutcomeAsync(client, encounterId, outcome, scenario.Scenario.Id, ct)); break; default: failures.Add($"[{scenario.Scenario.Id}] Unknown expected outcome type '{outcome.Type}'"); break; } } return failures; } private static IEnumerable ValidateAlertOutcome( ExpectedOutcome outcome, List alerts, string scenarioId) { if (string.IsNullOrWhiteSpace(outcome.AlertType)) { yield return $"[{scenarioId}] Alert outcome at T+{outcome.AfterOffsetMinutes}m missing alertType"; yield break; } var matching = alerts .Where(a => string.Equals(a.AlertType, outcome.AlertType, StringComparison.OrdinalIgnoreCase)) .ToList(); if (matching.Count == 0) { yield return $"[{scenarioId}] Expected alert {outcome.AlertType} at T+{outcome.AfterOffsetMinutes}m — not found " + $"(description: {outcome.Description ?? "n/a"})"; yield break; } if (!string.IsNullOrWhiteSpace(outcome.NarrativeContains)) { var withNarrative = matching.FirstOrDefault(a => a.Explanation?.NarrativeSummary?.Contains( outcome.NarrativeContains, StringComparison.OrdinalIgnoreCase) == true); if (withNarrative is null) { var summaries = matching .Select(a => a.Explanation?.NarrativeSummary ?? a.Details) .Take(2); yield return $"[{scenarioId}] Alert {outcome.AlertType} at T+{outcome.AfterOffsetMinutes}m — " + $"narrative does not contain '{outcome.NarrativeContains}' " + $"(got: {string.Join(" | ", summaries)})"; } } } private static async Task> ValidateScoreOutcomeAsync( VigilCareApiClient client, Guid encounterId, ExpectedOutcome outcome, string scenarioId, CancellationToken ct) { if (string.IsNullOrWhiteSpace(outcome.ScoreType)) return [$"[{scenarioId}] Score outcome at T+{outcome.AfterOffsetMinutes}m missing scoreType"]; if (!outcome.ExpectedMinimum.HasValue) return [$"[{scenarioId}] Score outcome at T+{outcome.AfterOffsetMinutes}m missing expectedMinimum"]; var actual = await GetScoreAsync(client, encounterId, outcome.ScoreType, ct); if (actual is null) { return [ $"[{scenarioId}] Expected {outcome.ScoreType} ≥ {outcome.ExpectedMinimum} at T+{outcome.AfterOffsetMinutes}m — score not available" ]; } if (actual.Value < outcome.ExpectedMinimum.Value) { return [ $"[{scenarioId}] Expected {outcome.ScoreType} ≥ {outcome.ExpectedMinimum} at T+{outcome.AfterOffsetMinutes}m — got {actual.Value} " + $"(description: {outcome.Description ?? "n/a"})" ]; } return []; } private static async Task> ValidateBundleOutcomeAsync( VigilCareApiClient client, Guid encounterId, ExpectedOutcome outcome, string scenarioId, CancellationToken ct) { var bundle = await client.GetSepsisBundleAsync(encounterId); if (bundle is null) { return [ $"[{scenarioId}] Expected sepsis bundle at T+{outcome.AfterOffsetMinutes}m — bundle not found " + $"(description: {outcome.Description ?? "n/a"})" ]; } return []; } private static async Task GetScoreAsync( VigilCareApiClient client, Guid encounterId, string scoreType, CancellationToken ct) { return scoreType.ToUpperInvariant() switch { "NEWS2" => (await client.GetCurrentNews2Async(encounterId))?.TotalScore, "GCS" => (await client.GetCurrentGcsAsync(encounterId))?.TotalScore, "SOFA" => (await client.GetCurrentSofaAsync(encounterId))?.TotalScore, "QSOFA" => (await client.GetCurrentQsofaAsync(encounterId))?.ActiveCriteria, _ => null }; } }