using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; using StellaOps.Auth.Abstractions; using StellaOps.Policy.Engine.Options; using StellaOps.Policy.Engine.Services; using StellaOps.Policy.Engine.Simulation; namespace StellaOps.Policy.Engine.Endpoints; /// /// Risk simulation endpoints for Policy Engine and Policy Studio. /// Enhanced with detailed analytics per POLICY-RISK-68-001. /// internal static class RiskSimulationEndpoints { public static IEndpointRouteBuilder MapRiskSimulation(this IEndpointRouteBuilder endpoints) { var group = endpoints.MapGroup("/api/risk/simulation") .RequireAuthorization() .RequireRateLimiting(PolicyEngineRateLimitOptions.PolicyName) .WithTags("Risk Simulation"); group.MapPost("/", RunSimulation) .WithName("RunRiskSimulation") .WithSummary("Run a risk simulation with score distributions and contribution breakdowns.") .Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status400BadRequest) .Produces(StatusCodes.Status404NotFound); group.MapPost("/quick", RunQuickSimulation) .WithName("RunQuickRiskSimulation") .WithSummary("Run a quick risk simulation without detailed breakdowns.") .Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status400BadRequest) .Produces(StatusCodes.Status404NotFound); group.MapPost("/compare", CompareProfiles) .WithName("CompareProfileSimulations") .WithSummary("Compare risk scoring between two profile configurations.") .Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status400BadRequest); group.MapPost("/whatif", RunWhatIfSimulation) .WithName("RunWhatIfSimulation") .WithSummary("Run a what-if simulation with hypothetical signal changes.") .Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status400BadRequest); // Policy Studio specific endpoints per POLICY-RISK-68-001 group.MapPost("/studio/analyze", RunStudioAnalysis) .WithName("RunPolicyStudioAnalysis") .WithSummary("Run a detailed analysis for Policy Studio with full breakdown analytics.") .WithDescription("Provides comprehensive breakdown including signal analysis, override tracking, score distributions, and component breakdowns for policy authoring.") .Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status400BadRequest) .Produces(StatusCodes.Status404NotFound); group.MapPost("/studio/compare", CompareProfilesWithBreakdown) .WithName("CompareProfilesWithBreakdown") .WithSummary("Compare profiles with full breakdown analytics and trend analysis.") .Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status400BadRequest); group.MapPost("/studio/preview", PreviewProfileChanges) .WithName("PreviewProfileChanges") .WithSummary("Preview impact of profile changes before committing.") .WithDescription("Simulates findings against both current and proposed profile to show impact.") .Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status400BadRequest); return endpoints; } private static IResult RunSimulation( HttpContext context, [FromBody] RiskSimulationRequest request, RiskSimulationService simulationService) { var scopeResult = ScopeAuthorization.RequireScope(context, StellaOpsScopes.PolicyRead); if (scopeResult is not null) { return scopeResult; } if (request == null || string.IsNullOrWhiteSpace(request.ProfileId)) { return Results.BadRequest(new ProblemDetails { Title = "Invalid request", Detail = "ProfileId is required.", Status = StatusCodes.Status400BadRequest }); } if (request.Findings == null || request.Findings.Count == 0) { return Results.BadRequest(new ProblemDetails { Title = "Invalid request", Detail = "At least one finding is required.", Status = StatusCodes.Status400BadRequest }); } try { var result = simulationService.Simulate(request); return Results.Ok(new RiskSimulationResponse(result)); } catch (InvalidOperationException ex) when (ex.Message.Contains("not found")) { return Results.NotFound(new ProblemDetails { Title = "Profile not found", Detail = ex.Message, Status = StatusCodes.Status404NotFound }); } } private static IResult RunQuickSimulation( HttpContext context, [FromBody] QuickSimulationRequest request, RiskSimulationService simulationService) { var scopeResult = ScopeAuthorization.RequireScope(context, StellaOpsScopes.PolicyRead); if (scopeResult is not null) { return scopeResult; } if (request == null || string.IsNullOrWhiteSpace(request.ProfileId)) { return Results.BadRequest(new ProblemDetails { Title = "Invalid request", Detail = "ProfileId is required.", Status = StatusCodes.Status400BadRequest }); } var fullRequest = new RiskSimulationRequest( ProfileId: request.ProfileId, ProfileVersion: request.ProfileVersion, Findings: request.Findings, IncludeContributions: false, IncludeDistribution: true, Mode: SimulationMode.Quick); try { var result = simulationService.Simulate(fullRequest); var quickResponse = new QuickSimulationResponse( SimulationId: result.SimulationId, ProfileId: result.ProfileId, ProfileVersion: result.ProfileVersion, Timestamp: result.Timestamp, AggregateMetrics: result.AggregateMetrics, Distribution: result.Distribution, ExecutionTimeMs: result.ExecutionTimeMs); return Results.Ok(quickResponse); } catch (InvalidOperationException ex) when (ex.Message.Contains("not found")) { return Results.NotFound(new ProblemDetails { Title = "Profile not found", Detail = ex.Message, Status = StatusCodes.Status404NotFound }); } } private static IResult CompareProfiles( HttpContext context, [FromBody] ProfileComparisonRequest request, RiskSimulationService simulationService) { var scopeResult = ScopeAuthorization.RequireScope(context, StellaOpsScopes.PolicyRead); if (scopeResult is not null) { return scopeResult; } if (request == null || string.IsNullOrWhiteSpace(request.BaseProfileId) || string.IsNullOrWhiteSpace(request.CompareProfileId)) { return Results.BadRequest(new ProblemDetails { Title = "Invalid request", Detail = "Both BaseProfileId and CompareProfileId are required.", Status = StatusCodes.Status400BadRequest }); } try { var baseRequest = new RiskSimulationRequest( ProfileId: request.BaseProfileId, ProfileVersion: request.BaseProfileVersion, Findings: request.Findings, IncludeContributions: true, IncludeDistribution: true, Mode: SimulationMode.Full); var compareRequest = new RiskSimulationRequest( ProfileId: request.CompareProfileId, ProfileVersion: request.CompareProfileVersion, Findings: request.Findings, IncludeContributions: true, IncludeDistribution: true, Mode: SimulationMode.Full); var baseResult = simulationService.Simulate(baseRequest); var compareResult = simulationService.Simulate(compareRequest); var deltas = ComputeDeltas(baseResult, compareResult); return Results.Ok(new ProfileComparisonResponse( BaseProfile: new ProfileSimulationSummary( baseResult.ProfileId, baseResult.ProfileVersion, baseResult.AggregateMetrics), CompareProfile: new ProfileSimulationSummary( compareResult.ProfileId, compareResult.ProfileVersion, compareResult.AggregateMetrics), Deltas: deltas)); } catch (InvalidOperationException ex) when (ex.Message.Contains("not found")) { return Results.BadRequest(new ProblemDetails { Title = "Profile not found", Detail = ex.Message, Status = StatusCodes.Status400BadRequest }); } } private static IResult RunWhatIfSimulation( HttpContext context, [FromBody] WhatIfSimulationRequest request, RiskSimulationService simulationService) { var scopeResult = ScopeAuthorization.RequireScope(context, StellaOpsScopes.PolicyRead); if (scopeResult is not null) { return scopeResult; } if (request == null || string.IsNullOrWhiteSpace(request.ProfileId)) { return Results.BadRequest(new ProblemDetails { Title = "Invalid request", Detail = "ProfileId is required.", Status = StatusCodes.Status400BadRequest }); } try { // Run baseline simulation var baselineRequest = new RiskSimulationRequest( ProfileId: request.ProfileId, ProfileVersion: request.ProfileVersion, Findings: request.Findings, IncludeContributions: true, IncludeDistribution: true, Mode: SimulationMode.Full); var baselineResult = simulationService.Simulate(baselineRequest); // Apply hypothetical changes to findings and re-simulate var modifiedFindings = ApplyHypotheticalChanges(request.Findings, request.HypotheticalChanges); var modifiedRequest = new RiskSimulationRequest( ProfileId: request.ProfileId, ProfileVersion: request.ProfileVersion, Findings: modifiedFindings, IncludeContributions: true, IncludeDistribution: true, Mode: SimulationMode.WhatIf); var modifiedResult = simulationService.Simulate(modifiedRequest); return Results.Ok(new WhatIfSimulationResponse( BaselineResult: baselineResult, ModifiedResult: modifiedResult, ImpactSummary: ComputeImpactSummary(baselineResult, modifiedResult))); } catch (InvalidOperationException ex) when (ex.Message.Contains("not found")) { return Results.NotFound(new ProblemDetails { Title = "Profile not found", Detail = ex.Message, Status = StatusCodes.Status404NotFound }); } } private static ComparisonDeltas ComputeDeltas( RiskSimulationResult baseResult, RiskSimulationResult compareResult) { return new ComparisonDeltas( MeanScoreDelta: compareResult.AggregateMetrics.MeanScore - baseResult.AggregateMetrics.MeanScore, MedianScoreDelta: compareResult.AggregateMetrics.MedianScore - baseResult.AggregateMetrics.MedianScore, CriticalCountDelta: compareResult.AggregateMetrics.CriticalCount - baseResult.AggregateMetrics.CriticalCount, HighCountDelta: compareResult.AggregateMetrics.HighCount - baseResult.AggregateMetrics.HighCount, MediumCountDelta: compareResult.AggregateMetrics.MediumCount - baseResult.AggregateMetrics.MediumCount, LowCountDelta: compareResult.AggregateMetrics.LowCount - baseResult.AggregateMetrics.LowCount); } private static IReadOnlyList ApplyHypotheticalChanges( IReadOnlyList findings, IReadOnlyList changes) { var result = new List(); foreach (var finding in findings) { var modifiedSignals = new Dictionary(finding.Signals); foreach (var change in changes) { if (change.ApplyToAll || change.FindingIds.Contains(finding.FindingId)) { modifiedSignals[change.SignalName] = change.NewValue; } } result.Add(finding with { Signals = modifiedSignals }); } return result.AsReadOnly(); } private static WhatIfImpactSummary ComputeImpactSummary( RiskSimulationResult baseline, RiskSimulationResult modified) { var baseScores = baseline.FindingScores.ToDictionary(f => f.FindingId, f => f.NormalizedScore); var modScores = modified.FindingScores.ToDictionary(f => f.FindingId, f => f.NormalizedScore); var improved = 0; var worsened = 0; var unchanged = 0; var totalDelta = 0.0; foreach (var (findingId, baseScore) in baseScores) { if (modScores.TryGetValue(findingId, out var modScore)) { var delta = modScore - baseScore; totalDelta += delta; if (Math.Abs(delta) < 0.1) unchanged++; else if (delta < 0) improved++; else worsened++; } } return new WhatIfImpactSummary( FindingsImproved: improved, FindingsWorsened: worsened, FindingsUnchanged: unchanged, AverageScoreDelta: baseline.FindingScores.Count > 0 ? totalDelta / baseline.FindingScores.Count : 0, SeverityShifts: new SeverityShifts( ToLower: improved, ToHigher: worsened, Unchanged: unchanged)); } #region Policy Studio Endpoints (POLICY-RISK-68-001) private static IResult RunStudioAnalysis( HttpContext context, [FromBody] PolicyStudioAnalysisRequest request, RiskSimulationService simulationService) { var scopeResult = ScopeAuthorization.RequireScope(context, StellaOpsScopes.PolicyRead); if (scopeResult is not null) { return scopeResult; } if (request == null || string.IsNullOrWhiteSpace(request.ProfileId)) { return Results.BadRequest(new ProblemDetails { Title = "Invalid request", Detail = "ProfileId is required.", Status = StatusCodes.Status400BadRequest }); } if (request.Findings == null || request.Findings.Count == 0) { return Results.BadRequest(new ProblemDetails { Title = "Invalid request", Detail = "At least one finding is required.", Status = StatusCodes.Status400BadRequest }); } try { var breakdownOptions = request.BreakdownOptions ?? RiskSimulationBreakdownOptions.Default; var result = simulationService.SimulateWithBreakdown( new RiskSimulationRequest( ProfileId: request.ProfileId, ProfileVersion: request.ProfileVersion, Findings: request.Findings, IncludeContributions: true, IncludeDistribution: true, Mode: SimulationMode.Full), breakdownOptions); return Results.Ok(new PolicyStudioAnalysisResponse( Result: result.Result, Breakdown: result.Breakdown, TotalExecutionTimeMs: result.TotalExecutionTimeMs)); } catch (InvalidOperationException ex) when (ex.Message.Contains("not found")) { return Results.NotFound(new ProblemDetails { Title = "Profile not found", Detail = ex.Message, Status = StatusCodes.Status404NotFound }); } catch (InvalidOperationException ex) when (ex.Message.Contains("Breakdown service")) { return Results.Problem( title: "Service unavailable", detail: ex.Message, statusCode: StatusCodes.Status503ServiceUnavailable); } } private static IResult CompareProfilesWithBreakdown( HttpContext context, [FromBody] PolicyStudioComparisonRequest request, RiskSimulationService simulationService) { var scopeResult = ScopeAuthorization.RequireScope(context, StellaOpsScopes.PolicyRead); if (scopeResult is not null) { return scopeResult; } if (request == null || string.IsNullOrWhiteSpace(request.BaseProfileId) || string.IsNullOrWhiteSpace(request.CompareProfileId)) { return Results.BadRequest(new ProblemDetails { Title = "Invalid request", Detail = "Both BaseProfileId and CompareProfileId are required.", Status = StatusCodes.Status400BadRequest }); } if (request.Findings == null || request.Findings.Count == 0) { return Results.BadRequest(new ProblemDetails { Title = "Invalid request", Detail = "At least one finding is required.", Status = StatusCodes.Status400BadRequest }); } try { var result = simulationService.CompareProfilesWithBreakdown( request.BaseProfileId, request.CompareProfileId, request.Findings, request.BreakdownOptions); return Results.Ok(new PolicyStudioComparisonResponse( BaselineResult: result.BaselineResult, CompareResult: result.CompareResult, Breakdown: result.Breakdown, ExecutionTimeMs: result.ExecutionTimeMs)); } catch (InvalidOperationException ex) when (ex.Message.Contains("not found")) { return Results.BadRequest(new ProblemDetails { Title = "Profile not found", Detail = ex.Message, Status = StatusCodes.Status400BadRequest }); } catch (InvalidOperationException ex) when (ex.Message.Contains("Breakdown service")) { return Results.Problem( title: "Service unavailable", detail: ex.Message, statusCode: StatusCodes.Status503ServiceUnavailable); } } private static IResult PreviewProfileChanges( HttpContext context, [FromBody] ProfileChangePreviewRequest request, RiskSimulationService simulationService) { var scopeResult = ScopeAuthorization.RequireScope(context, StellaOpsScopes.PolicyRead); if (scopeResult is not null) { return scopeResult; } if (request == null || string.IsNullOrWhiteSpace(request.CurrentProfileId)) { return Results.BadRequest(new ProblemDetails { Title = "Invalid request", Detail = "CurrentProfileId is required.", Status = StatusCodes.Status400BadRequest }); } if (string.IsNullOrWhiteSpace(request.ProposedProfileId) && (request.ProposedWeightChanges == null || request.ProposedWeightChanges.Count == 0) && (request.ProposedOverrideChanges == null || request.ProposedOverrideChanges.Count == 0)) { return Results.BadRequest(new ProblemDetails { Title = "Invalid request", Detail = "Either ProposedProfileId or at least one proposed change is required.", Status = StatusCodes.Status400BadRequest }); } try { // Run simulation against current profile var currentRequest = new RiskSimulationRequest( ProfileId: request.CurrentProfileId, ProfileVersion: request.CurrentProfileVersion, Findings: request.Findings, IncludeContributions: true, IncludeDistribution: true, Mode: SimulationMode.Full); var currentResult = simulationService.Simulate(currentRequest); RiskSimulationResult proposedResult; if (!string.IsNullOrWhiteSpace(request.ProposedProfileId)) { // Compare against existing proposed profile var proposedRequest = new RiskSimulationRequest( ProfileId: request.ProposedProfileId, ProfileVersion: request.ProposedProfileVersion, Findings: request.Findings, IncludeContributions: true, IncludeDistribution: true, Mode: SimulationMode.Full); proposedResult = simulationService.Simulate(proposedRequest); } else { // Inline changes not yet supported - return preview of current only proposedResult = currentResult; } var impactSummary = ComputePreviewImpact(currentResult, proposedResult); return Results.Ok(new ProfileChangePreviewResponse( CurrentResult: new ProfileSimulationSummary( currentResult.ProfileId, currentResult.ProfileVersion, currentResult.AggregateMetrics), ProposedResult: new ProfileSimulationSummary( proposedResult.ProfileId, proposedResult.ProfileVersion, proposedResult.AggregateMetrics), Impact: impactSummary, HighImpactFindings: ComputeHighImpactFindings(currentResult, proposedResult))); } catch (InvalidOperationException ex) when (ex.Message.Contains("not found")) { return Results.NotFound(new ProblemDetails { Title = "Profile not found", Detail = ex.Message, Status = StatusCodes.Status404NotFound }); } } private static ProfileChangeImpact ComputePreviewImpact( RiskSimulationResult current, RiskSimulationResult proposed) { var currentScores = current.FindingScores.ToDictionary(f => f.FindingId); var proposedScores = proposed.FindingScores.ToDictionary(f => f.FindingId); var improved = 0; var worsened = 0; var unchanged = 0; var severityEscalations = 0; var severityDeescalations = 0; var actionChanges = 0; foreach (var (findingId, currentScore) in currentScores) { if (!proposedScores.TryGetValue(findingId, out var proposedScore)) continue; var scoreDelta = proposedScore.NormalizedScore - currentScore.NormalizedScore; if (Math.Abs(scoreDelta) < 1.0) unchanged++; else if (scoreDelta < 0) improved++; else worsened++; if (proposedScore.Severity > currentScore.Severity) severityEscalations++; else if (proposedScore.Severity < currentScore.Severity) severityDeescalations++; if (proposedScore.RecommendedAction != currentScore.RecommendedAction) actionChanges++; } return new ProfileChangeImpact( FindingsImproved: improved, FindingsWorsened: worsened, FindingsUnchanged: unchanged, SeverityEscalations: severityEscalations, SeverityDeescalations: severityDeescalations, ActionChanges: actionChanges, MeanScoreDelta: proposed.AggregateMetrics.MeanScore - current.AggregateMetrics.MeanScore, CriticalCountDelta: proposed.AggregateMetrics.CriticalCount - current.AggregateMetrics.CriticalCount, HighCountDelta: proposed.AggregateMetrics.HighCount - current.AggregateMetrics.HighCount); } private static IReadOnlyList ComputeHighImpactFindings( RiskSimulationResult current, RiskSimulationResult proposed) { var currentScores = current.FindingScores.ToDictionary(f => f.FindingId); var proposedScores = proposed.FindingScores.ToDictionary(f => f.FindingId); var highImpact = new List(); foreach (var (findingId, currentScore) in currentScores) { if (!proposedScores.TryGetValue(findingId, out var proposedScore)) continue; var scoreDelta = Math.Abs(proposedScore.NormalizedScore - currentScore.NormalizedScore); var severityChanged = proposedScore.Severity != currentScore.Severity; var actionChanged = proposedScore.RecommendedAction != currentScore.RecommendedAction; if (scoreDelta > 10 || severityChanged || actionChanged) { highImpact.Add(new HighImpactFindingPreview( FindingId: findingId, CurrentScore: currentScore.NormalizedScore, ProposedScore: proposedScore.NormalizedScore, ScoreDelta: proposedScore.NormalizedScore - currentScore.NormalizedScore, CurrentSeverity: currentScore.Severity.ToString(), ProposedSeverity: proposedScore.Severity.ToString(), CurrentAction: currentScore.RecommendedAction.ToString(), ProposedAction: proposedScore.RecommendedAction.ToString(), ImpactReason: DetermineImpactReason(currentScore, proposedScore))); } } return highImpact .OrderByDescending(f => Math.Abs(f.ScoreDelta)) .Take(20) .ToList(); } private static string DetermineImpactReason(FindingScore current, FindingScore proposed) { var reasons = new List(); if (proposed.Severity != current.Severity) { reasons.Add($"Severity {(proposed.Severity > current.Severity ? "escalated" : "deescalated")} from {current.Severity} to {proposed.Severity}"); } if (proposed.RecommendedAction != current.RecommendedAction) { reasons.Add($"Action changed from {current.RecommendedAction} to {proposed.RecommendedAction}"); } var scoreDelta = proposed.NormalizedScore - current.NormalizedScore; if (Math.Abs(scoreDelta) > 10) { reasons.Add($"Score {(scoreDelta > 0 ? "increased" : "decreased")} by {Math.Abs(scoreDelta):F1} points"); } return reasons.Count > 0 ? string.Join("; ", reasons) : "Significant score change"; } #endregion } #region Request/Response DTOs internal sealed record RiskSimulationResponse(RiskSimulationResult Result); internal sealed record QuickSimulationRequest( string ProfileId, string? ProfileVersion, IReadOnlyList Findings); internal sealed record QuickSimulationResponse( string SimulationId, string ProfileId, string ProfileVersion, DateTimeOffset Timestamp, AggregateRiskMetrics AggregateMetrics, RiskDistribution? Distribution, double ExecutionTimeMs); internal sealed record ProfileComparisonRequest( string BaseProfileId, string? BaseProfileVersion, string CompareProfileId, string? CompareProfileVersion, IReadOnlyList Findings); internal sealed record ProfileComparisonResponse( ProfileSimulationSummary BaseProfile, ProfileSimulationSummary CompareProfile, ComparisonDeltas Deltas); internal sealed record ProfileSimulationSummary( string ProfileId, string ProfileVersion, AggregateRiskMetrics Metrics); internal sealed record ComparisonDeltas( double MeanScoreDelta, double MedianScoreDelta, int CriticalCountDelta, int HighCountDelta, int MediumCountDelta, int LowCountDelta); internal sealed record WhatIfSimulationRequest( string ProfileId, string? ProfileVersion, IReadOnlyList Findings, IReadOnlyList HypotheticalChanges); internal sealed record HypotheticalChange( string SignalName, object? NewValue, bool ApplyToAll = true, IReadOnlyList? FindingIds = null) { public IReadOnlyList FindingIds { get; init; } = FindingIds ?? Array.Empty(); } internal sealed record WhatIfSimulationResponse( RiskSimulationResult BaselineResult, RiskSimulationResult ModifiedResult, WhatIfImpactSummary ImpactSummary); internal sealed record WhatIfImpactSummary( int FindingsImproved, int FindingsWorsened, int FindingsUnchanged, double AverageScoreDelta, SeverityShifts SeverityShifts); internal sealed record SeverityShifts( int ToLower, int ToHigher, int Unchanged); #endregion #region Policy Studio DTOs (POLICY-RISK-68-001) internal sealed record PolicyStudioAnalysisRequest( string ProfileId, string? ProfileVersion, IReadOnlyList Findings, RiskSimulationBreakdownOptions? BreakdownOptions = null); internal sealed record PolicyStudioAnalysisResponse( RiskSimulationResult Result, RiskSimulationBreakdown Breakdown, double TotalExecutionTimeMs); internal sealed record PolicyStudioComparisonRequest( string BaseProfileId, string CompareProfileId, IReadOnlyList Findings, RiskSimulationBreakdownOptions? BreakdownOptions = null); internal sealed record PolicyStudioComparisonResponse( RiskSimulationResult BaselineResult, RiskSimulationResult CompareResult, RiskSimulationBreakdown Breakdown, double ExecutionTimeMs); internal sealed record ProfileChangePreviewRequest( string CurrentProfileId, string? CurrentProfileVersion, string? ProposedProfileId, string? ProposedProfileVersion, IReadOnlyList Findings, IReadOnlyDictionary? ProposedWeightChanges = null, IReadOnlyList? ProposedOverrideChanges = null); internal sealed record ProposedOverrideChange( string OverrideType, Dictionary When, object Value, string? Reason = null); internal sealed record ProfileChangePreviewResponse( ProfileSimulationSummary CurrentResult, ProfileSimulationSummary ProposedResult, ProfileChangeImpact Impact, IReadOnlyList HighImpactFindings); internal sealed record ProfileChangeImpact( int FindingsImproved, int FindingsWorsened, int FindingsUnchanged, int SeverityEscalations, int SeverityDeescalations, int ActionChanges, double MeanScoreDelta, int CriticalCountDelta, int HighCountDelta); internal sealed record HighImpactFindingPreview( string FindingId, double CurrentScore, double ProposedScore, double ScoreDelta, string CurrentSeverity, string ProposedSeverity, string CurrentAction, string ProposedAction, string ImpactReason); #endregion