reasonCode !== null ? $this->reasonPresenter->forArtifactTruth($stats->reasonCode, 'baseline_compare') : null; $hasCoverageWarnings = in_array($stats->coverageStatus, ['warning', 'unproven'], true); $hasEvidenceGaps = (int) ($stats->evidenceGapsCount ?? 0) > 0; $hasWarnings = $hasCoverageWarnings || $hasEvidenceGaps; $findingsCount = (int) ($stats->findingsCount ?? 0); $executionOutcome = match ($stats->state) { 'comparing' => 'in_progress', 'failed' => 'failed', default => $hasWarnings ? 'completed_with_follow_up' : 'completed', }; $executionOutcomeLabel = match ($executionOutcome) { 'in_progress' => 'In progress', 'failed' => 'Execution failed', 'completed_with_follow_up' => 'Completed with follow-up', default => 'Completed successfully', }; $family = $reason?->absencePattern !== null ? ExplanationFamily::tryFrom(str_replace('true_no_result', 'no_issues_detected', $reason->absencePattern)) ?? null : null; $family ??= match (true) { $stats->state === 'comparing' => ExplanationFamily::InProgress, $stats->state === 'failed' => ExplanationFamily::BlockedPrerequisite, $stats->state === 'no_tenant', $stats->state === 'no_assignment', $stats->state === 'no_snapshot', $stats->state === 'idle' => ExplanationFamily::Unavailable, $findingsCount === 0 && ! $hasWarnings => ExplanationFamily::NoIssuesDetected, $hasWarnings => ExplanationFamily::CompletedButLimited, default => ExplanationFamily::TrustworthyResult, }; $trustworthiness = $reason?->trustImpact !== null ? TrustworthinessLevel::tryFrom($reason->trustImpact) : null; $trustworthiness ??= match (true) { $family === ExplanationFamily::NoIssuesDetected, $family === ExplanationFamily::TrustworthyResult => TrustworthinessLevel::Trustworthy, $family === ExplanationFamily::CompletedButLimited => TrustworthinessLevel::LimitedConfidence, $family === ExplanationFamily::InProgress => TrustworthinessLevel::DiagnosticOnly, default => TrustworthinessLevel::Unusable, }; $evaluationResult = match ($family) { ExplanationFamily::TrustworthyResult => 'full_result', ExplanationFamily::NoIssuesDetected => 'no_result', ExplanationFamily::SuppressedOutput => 'suppressed_result', ExplanationFamily::MissingInput, ExplanationFamily::BlockedPrerequisite, ExplanationFamily::Unavailable, ExplanationFamily::InProgress => 'unavailable', ExplanationFamily::CompletedButLimited => $reason?->absencePattern === 'suppressed_output' ? 'suppressed_result' : 'incomplete_result', }; $headline = match ($family) { ExplanationFamily::NoIssuesDetected => 'The comparison found no actionable drift.', ExplanationFamily::TrustworthyResult => 'The comparison produced a usable drift result.', ExplanationFamily::CompletedButLimited => $findingsCount > 0 ? 'The comparison found drift, but the result needs caution.' : 'The comparison finished, but the current result is not an all-clear.', ExplanationFamily::SuppressedOutput => 'The comparison finished, but normal result output was suppressed.', ExplanationFamily::MissingInput => 'The comparison could not produce a usable result because required inputs were missing.', ExplanationFamily::BlockedPrerequisite => 'The comparison could not produce a usable result because a prerequisite blocked it.', ExplanationFamily::InProgress => 'The comparison is still running.', ExplanationFamily::Unavailable => 'A usable comparison result is not currently available.', }; $coverageStatement = match (true) { $stats->state === 'idle' => 'No compare run has produced a result yet for this tenant.', $stats->coverageStatus === 'unproven' => 'Coverage proof was unavailable, so suppressed output is possible even when the findings count is zero.', $stats->uncoveredTypes !== [] => 'One or more in-scope policy types were not fully covered in this compare run.', $hasEvidenceGaps => 'Evidence gaps limited the quality of the visible result for some subjects.', $stats->state === 'comparing' => 'Counts will become decision-grade after the compare run finishes.', in_array($family, [ExplanationFamily::MissingInput, ExplanationFamily::BlockedPrerequisite, ExplanationFamily::Unavailable], true) => $reason?->shortExplanation ?? 'The compare inputs were not complete enough to produce a normal result.', default => 'Coverage matched the in-scope compare input for this run.', }; $reliabilityStatement = match ($trustworthiness) { TrustworthinessLevel::Trustworthy => $findingsCount > 0 ? 'The compare completed with enough coverage to treat the recorded drift as decision-grade.' : 'The compare completed with enough coverage to treat the absence of findings as trustworthy.', TrustworthinessLevel::LimitedConfidence => 'The compare completed, but coverage or evidence limitations mean the visible result should be treated as partial.', TrustworthinessLevel::DiagnosticOnly => 'The compare is still in progress, so current counts are only diagnostic.', TrustworthinessLevel::Unusable => 'The compare did not produce a result that should be used for the intended decision yet.', }; $nextActionText = $reason?->firstNextStep()?->label ?? match ($family) { ExplanationFamily::NoIssuesDetected => 'No action needed', ExplanationFamily::TrustworthyResult => 'Review the detected drift findings', ExplanationFamily::SuppressedOutput => 'Review the missing coverage or evidence before treating this compare as complete', ExplanationFamily::CompletedButLimited => 'Review coverage limits before acting on this compare', ExplanationFamily::InProgress => 'Wait for the compare to finish', ExplanationFamily::MissingInput, ExplanationFamily::BlockedPrerequisite, ExplanationFamily::Unavailable => $stats->state === 'idle' ? 'Run the baseline compare to generate a result' : 'Review the blocking baseline or scope prerequisite', }; return $this->builder->build( family: $family, headline: $headline, executionOutcome: $executionOutcome, executionOutcomeLabel: $executionOutcomeLabel, evaluationResult: $evaluationResult, trustworthinessLevel: $trustworthiness, reliabilityStatement: $reliabilityStatement, coverageStatement: $coverageStatement, dominantCauseCode: $reason?->internalCode ?? $stats->reasonCode, dominantCauseLabel: $reason?->operatorLabel, dominantCauseExplanation: $reason?->shortExplanation ?? $stats->message ?? $stats->failureReason, nextActionCategory: $family === ExplanationFamily::NoIssuesDetected ? 'none' : match ($family) { ExplanationFamily::TrustworthyResult => 'manual_validate', ExplanationFamily::MissingInput, ExplanationFamily::BlockedPrerequisite, ExplanationFamily::Unavailable => 'fix_prerequisite', default => 'review_evidence_gaps', }, nextActionText: $nextActionText, countDescriptors: $this->countDescriptors($stats, $hasCoverageWarnings, $hasEvidenceGaps), diagnosticsAvailable: $stats->operationRunId !== null || $reason !== null, diagnosticsSummary: 'Run evidence and low-level compare diagnostics remain available below the primary explanation.', ); } /** * @return array */ private function countDescriptors( BaselineCompareStats $stats, bool $hasCoverageWarnings, bool $hasEvidenceGaps, ): array { $descriptors = []; if ($stats->findingsCount !== null) { $descriptors[] = new CountDescriptor( label: 'Findings shown', value: (int) $stats->findingsCount, role: CountDescriptor::ROLE_EVALUATION_OUTPUT, qualifier: $hasCoverageWarnings || $hasEvidenceGaps ? 'not complete' : null, ); } if ($stats->uncoveredTypesCount !== null) { $descriptors[] = new CountDescriptor( label: 'Uncovered types', value: (int) $stats->uncoveredTypesCount, role: CountDescriptor::ROLE_COVERAGE, qualifier: (int) $stats->uncoveredTypesCount > 0 ? 'coverage gap' : null, ); } if ($stats->evidenceGapsCount !== null) { $descriptors[] = new CountDescriptor( label: 'Evidence gaps', value: (int) $stats->evidenceGapsCount, role: CountDescriptor::ROLE_RELIABILITY_SIGNAL, qualifier: (int) $stats->evidenceGapsCount > 0 ? 'review needed' : null, ); } if ($stats->severityCounts !== []) { foreach (['high' => 'High severity', 'medium' => 'Medium severity', 'low' => 'Low severity'] as $key => $label) { $value = (int) ($stats->severityCounts[$key] ?? 0); if ($value === 0) { continue; } $descriptors[] = new CountDescriptor( label: $label, value: $value, role: CountDescriptor::ROLE_EVALUATION_OUTPUT, visibilityTier: CountDescriptor::VISIBILITY_DIAGNOSTIC, ); } } return $descriptors; } }