|null */ public static function report(OperationRun $run): ?array { $context = is_array($run->context) ? $run->context : []; $report = $context['verification_report'] ?? null; if (! is_array($report)) { return null; } $report = VerificationReportSanitizer::sanitizeReport($report); if (! VerificationReportSchema::isValidReport($report)) { return null; } return $report; } public static function previousReportId(array $report): ?int { $previousReportId = $report['previous_report_id'] ?? null; if (is_int($previousReportId) && $previousReportId > 0) { return $previousReportId; } if (is_string($previousReportId) && ctype_digit(trim($previousReportId))) { return (int) trim($previousReportId); } return null; } public static function fingerprint(array $report): ?string { $fingerprint = $report['fingerprint'] ?? null; if (is_string($fingerprint)) { $fingerprint = strtolower(trim($fingerprint)); if (preg_match('/^[a-f0-9]{64}$/', $fingerprint)) { return $fingerprint; } } return VerificationReportFingerprint::forReport($report); } public static function previousRun(OperationRun $run, array $report): ?OperationRun { $previousReportId = self::previousReportId($report); if ($previousReportId === null) { return null; } $previous = OperationRun::query() ->whereKey($previousReportId) ->where('tenant_id', (int) $run->tenant_id) ->where('workspace_id', (int) $run->workspace_id) ->first(); return $previous instanceof OperationRun ? $previous : null; } public static function shouldRenderForRun(OperationRun $run): bool { $context = is_array($run->context) ? $run->context : []; if (array_key_exists('verification_report', $context)) { return true; } return in_array((string) $run->type, ['provider.connection.check'], true); } /** * @param array> $acknowledgements * @param array{ * hostKind?: string, * changeIndicator?: array{state: string, previous_report_id: int}|null, * previousRunUrl?: string|null, * nextStepPlacement?: 'shared_zone'|'host_action_zone', * hostActions?: array, * hostVariation?: array{ * ownsNoRunState?: bool, * ownsActiveState?: bool, * supportsAssist?: bool, * supportsAcknowledge?: bool, * supportsTechnicalDetailsTrigger?: bool * }, * optionalZones?: array * } $options * @return array */ public static function surface(OperationRun $run, array $acknowledgements = [], array $options = []): array { $report = self::report($run); $summary = is_array($report['summary'] ?? null) ? $report['summary'] : []; $counts = is_array($summary['counts'] ?? null) ? $summary['counts'] : []; $groupedChecks = self::groupedChecks($report, $acknowledgements); $changeIndicator = $options['changeIndicator'] ?? null; $hostKind = is_string($options['hostKind'] ?? null) && $options['hostKind'] !== '' ? (string) $options['hostKind'] : 'operation_run_detail'; $nextStepPlacement = ($options['nextStepPlacement'] ?? 'shared_zone') === 'host_action_zone' ? 'host_action_zone' : 'shared_zone'; $hostActions = collect($options['hostActions'] ?? []) ->filter(static fn (mixed $action): bool => is_array($action)) ->map(static function (array $action): array { $kind = is_string($action['kind'] ?? null) ? trim((string) $action['kind']) : 'navigation'; $label = is_string($action['label'] ?? null) ? trim((string) $action['label']) : 'Action'; return [ 'kind' => $kind !== '' ? $kind : 'navigation', 'label' => $label !== '' ? $label : 'Action', 'ownedByHost' => (bool) ($action['ownedByHost'] ?? true), ]; }) ->values() ->all(); $hostVariation = [ 'ownsNoRunState' => (bool) (($options['hostVariation']['ownsNoRunState'] ?? false)), 'ownsActiveState' => (bool) (($options['hostVariation']['ownsActiveState'] ?? false)), 'supportsAssist' => (bool) (($options['hostVariation']['supportsAssist'] ?? false)), 'supportsAcknowledge' => (bool) (($options['hostVariation']['supportsAcknowledge'] ?? false)), 'supportsTechnicalDetailsTrigger' => (bool) (($options['hostVariation']['supportsTechnicalDetailsTrigger'] ?? false)), ]; $optionalZones = collect($options['optionalZones'] ?? ['technical_details', 'change_indicator', 'previous_run_context']) ->filter(static fn (mixed $zone): bool => is_string($zone) && trim($zone) !== '') ->map(static fn (string $zone): string => trim($zone)) ->values() ->all(); $overall = $summary['overall'] ?? null; $overallSpec = BadgeRenderer::spec(BadgeDomain::VerificationReportOverall, $overall); return [ 'hostKind' => $hostKind, 'coreState' => $report === null ? 'unavailable' : 'completed', 'summary' => [ 'overall' => $overall, 'overallLabel' => $overallSpec->label, 'counts' => [ 'total' => (int) ($counts['total'] ?? 0), 'pass' => (int) ($counts['pass'] ?? 0), 'fail' => (int) ($counts['fail'] ?? 0), 'warn' => (int) ($counts['warn'] ?? 0), 'skip' => (int) ($counts['skip'] ?? 0), 'running' => (int) ($counts['running'] ?? 0), ], 'changeIndicator' => is_array($changeIndicator) ? $changeIndicator : null, ], 'issueGroups' => $groupedChecks['issueGroups'], 'passedChecks' => $groupedChecks['passedChecks'], 'diagnostics' => [ 'hasTechnicalZone' => true, 'fingerprint' => is_array($report) ? self::fingerprint($report) : null, 'previousRunUrl' => is_string($options['previousRunUrl'] ?? null) && $options['previousRunUrl'] !== '' ? (string) $options['previousRunUrl'] : null, 'operationRunId' => (int) $run->getKey(), 'flow' => (string) $run->type, 'completedAt' => $run->completed_at?->toJSON(), ], 'viewZones' => [ ['key' => 'issues', 'label' => 'Issues', 'defaultVisible' => true], ['key' => 'passed', 'label' => 'Passed', 'defaultVisible' => false], ], 'nextSteps' => self::nextSteps($groupedChecks['issueGroups'], $nextStepPlacement), 'hostActions' => $hostActions, 'hostVariation' => $hostVariation, 'optionalZones' => $optionalZones, 'emptyState' => $report === null ? [ 'title' => 'Verification report unavailable', 'message' => 'This operation doesn’t have a report yet. If it is still running, refresh in a moment. If it already completed, start verification again.', ] : null, ]; } /** * @param array|null $report * @param array> $acknowledgements * @return array{issueGroups: array>, acknowledged?: bool}>, passedChecks: array>} */ private static function groupedChecks(?array $report, array $acknowledgements): array { $checks = is_array($report['checks'] ?? null) ? $report['checks'] : []; $ackByKey = []; foreach ($acknowledgements as $checkKey => $acknowledgement) { if (! is_string($checkKey) || $checkKey === '' || ! is_array($acknowledgement)) { continue; } $ackByKey[$checkKey] = $acknowledgement; } $blockers = []; $failures = []; $warnings = []; $acknowledgedIssues = []; $passed = []; foreach ($checks as $check) { if (! is_array($check)) { continue; } $key = is_string($check['key'] ?? null) ? trim((string) $check['key']) : ''; if ($key === '') { continue; } $status = is_string($check['status'] ?? null) ? strtolower(trim((string) $check['status'])) : ''; $blocking = (bool) ($check['blocking'] ?? false); $normalizedCheck = self::normalizeCheck($check, $ackByKey[$key] ?? null); if ($normalizedCheck['acknowledgement'] !== null) { $acknowledgedIssues[] = $normalizedCheck; continue; } if ($status === 'pass') { $passed[] = $normalizedCheck; continue; } if ($status === 'fail' && $blocking) { $blockers[] = $normalizedCheck; continue; } if ($status === 'fail') { $failures[] = $normalizedCheck; continue; } if ($status === 'warn') { $warnings[] = $normalizedCheck; } } $sortChecks = static fn (array $left, array $right): int => strcmp((string) ($left['key'] ?? ''), (string) ($right['key'] ?? '')); usort($blockers, $sortChecks); usort($failures, $sortChecks); usort($warnings, $sortChecks); usort($acknowledgedIssues, $sortChecks); usort($passed, $sortChecks); return [ 'issueGroups' => array_values(array_filter([ ['label' => 'Blockers', 'checks' => $blockers], ['label' => 'Failures', 'checks' => $failures], ['label' => 'Warnings', 'checks' => $warnings], ['label' => 'Acknowledged issues', 'checks' => $acknowledgedIssues, 'acknowledged' => true], ], static fn (array $group): bool => ($group['checks'] ?? []) !== [])), 'passedChecks' => $passed, ]; } /** * @param array $check * @param array|null $acknowledgement * @return array */ private static function normalizeCheck(array $check, ?array $acknowledgement): array { $nextSteps = collect($check['next_steps'] ?? []) ->filter(static fn (mixed $step): bool => is_array($step)) ->map(static function (array $step): array { $label = is_string($step['label'] ?? null) ? trim((string) $step['label']) : ''; $url = is_string($step['url'] ?? null) ? trim((string) $step['url']) : ''; return [ 'label' => $label, 'url' => $url, ]; }) ->filter(static fn (array $step): bool => $step['label'] !== '' && $step['url'] !== '') ->values() ->all(); return [ 'key' => is_string($check['key'] ?? null) ? trim((string) $check['key']) : '', 'title' => is_string($check['title'] ?? null) && trim((string) $check['title']) !== '' ? trim((string) $check['title']) : 'Check', 'message' => is_string($check['message'] ?? null) && trim((string) $check['message']) !== '' ? trim((string) $check['message']) : null, 'status' => is_string($check['status'] ?? null) ? trim((string) $check['status']) : null, 'severity' => is_string($check['severity'] ?? null) ? trim((string) $check['severity']) : null, 'reason_code' => is_string($check['reason_code'] ?? null) ? trim((string) $check['reason_code']) : null, 'blocking' => (bool) ($check['blocking'] ?? false), 'next_steps' => $nextSteps, 'acknowledgement' => is_array($acknowledgement) ? $acknowledgement : null, ]; } /** * @param array>, acknowledged?: bool}> $issueGroups * @return array */ private static function nextSteps(array $issueGroups, string $placement): array { $steps = []; foreach ($issueGroups as $group) { foreach ($group['checks'] as $check) { foreach ($check['next_steps'] ?? [] as $step) { if (! is_array($step)) { continue; } $label = is_string($step['label'] ?? null) ? trim((string) $step['label']) : ''; if ($label === '' || array_key_exists($label, $steps)) { continue; } $steps[$label] = [ 'label' => $label, 'placement' => $placement, 'ownedByHost' => $placement === 'host_action_zone', 'actionKind' => $placement === 'host_action_zone' ? 'assist' : 'navigation', ]; } } } return array_values($steps); } /** * @param array|null $report * @return array */ public static function redactionNotes(?array $report): array { return RedactionIntegrity::verificationNotes($report); } }