*/ private const array BASELINE_REASON_CODES = [ 'ok', 'not_applicable', 'missing_configuration', 'permission_denied', 'authentication_failed', 'throttled', 'dependency_unreachable', 'invalid_state', 'unknown_error', ]; /** * @param array> $checks * @param array $identity * @return array */ public static function write(OperationRun $run, array $checks, array $identity = []): array { $flow = is_string($run->type) && trim($run->type) !== '' ? (string) $run->type : 'unknown'; $report = self::build($flow, $checks, $identity); $report = VerificationReportSanitizer::sanitizeReport($report); if (! VerificationReportSchema::isValidReport($report)) { $report = VerificationReportSanitizer::sanitizeReport(self::buildFallbackReport($flow)); } $context = is_array($run->context) ? $run->context : []; $context['verification_report'] = $report; $run->update(['context' => $context]); return $report; } /** * @param array> $checks * @param array $identity * @return array */ public static function build(string $flow, array $checks, array $identity = []): array { $flow = trim($flow); $flow = $flow !== '' ? $flow : 'unknown'; $normalizedChecks = []; foreach ($checks as $check) { if (! is_array($check)) { continue; } $normalizedChecks[] = self::normalizeCheckResult($check); } $counts = self::deriveCounts($normalizedChecks); $report = [ 'schema_version' => VerificationReportSchema::CURRENT_SCHEMA_VERSION, 'flow' => $flow, 'generated_at' => now()->toISOString(), 'summary' => [ 'overall' => self::deriveOverall($normalizedChecks, $counts), 'counts' => $counts, ], 'checks' => $normalizedChecks, ]; if ($identity !== []) { $report['identity'] = $identity; } return $report; } /** * @return array */ private static function buildFallbackReport(string $flow): array { return [ 'schema_version' => VerificationReportSchema::CURRENT_SCHEMA_VERSION, 'flow' => $flow !== '' ? $flow : 'unknown', 'generated_at' => now()->toISOString(), 'summary' => [ 'overall' => VerificationReportOverall::NeedsAttention->value, 'counts' => [ 'total' => 0, 'pass' => 0, 'fail' => 0, 'warn' => 0, 'skip' => 0, 'running' => 0, ], ], 'checks' => [], ]; } /** * @param array $check * @return array{ * key: string, * title: string, * status: string, * severity: string, * blocking: bool, * reason_code: string, * message: string, * evidence: array, * next_steps: array * } */ private static function normalizeCheckResult(array $check): array { $key = self::normalizeNonEmptyString($check['key'] ?? null, fallback: 'unknown_check'); $title = self::normalizeNonEmptyString($check['title'] ?? null, fallback: 'Check'); return [ 'key' => $key, 'title' => $title, 'status' => self::normalizeCheckStatus($check['status'] ?? null), 'severity' => self::normalizeCheckSeverity($check['severity'] ?? null), 'blocking' => is_bool($check['blocking'] ?? null) ? (bool) $check['blocking'] : false, 'reason_code' => self::normalizeReasonCode($check['reason_code'] ?? null), 'message' => self::normalizeNonEmptyString($check['message'] ?? null, fallback: '—'), 'evidence' => self::normalizeEvidence($check['evidence'] ?? null), 'next_steps' => self::normalizeNextSteps($check['next_steps'] ?? null), ]; } private static function normalizeCheckStatus(mixed $status): string { if (! is_string($status)) { return VerificationCheckStatus::Fail->value; } $status = strtolower(trim($status)); return in_array($status, VerificationCheckStatus::values(), true) ? $status : VerificationCheckStatus::Fail->value; } private static function normalizeCheckSeverity(mixed $severity): string { if (! is_string($severity)) { return VerificationCheckSeverity::Info->value; } $severity = strtolower(trim($severity)); return in_array($severity, VerificationCheckSeverity::values(), true) ? $severity : VerificationCheckSeverity::Info->value; } private static function normalizeReasonCode(mixed $reasonCode): string { if (! is_string($reasonCode)) { return 'unknown_error'; } $reasonCode = strtolower(trim($reasonCode)); if ($reasonCode === '') { return 'unknown_error'; } if (str_starts_with($reasonCode, 'ext.')) { return $reasonCode; } $reasonCode = match ($reasonCode) { 'graph_throttled' => 'throttled', 'graph_timeout', 'provider_outage' => 'dependency_unreachable', 'provider_auth_failed' => 'authentication_failed', 'validation_error', 'conflict_detected' => 'invalid_state', 'unknown' => 'unknown_error', default => $reasonCode, }; return in_array($reasonCode, self::BASELINE_REASON_CODES, true) ? $reasonCode : 'unknown_error'; } /** * @return array */ private static function normalizeEvidence(mixed $evidence): array { if (! is_array($evidence)) { return []; } $normalized = []; foreach ($evidence as $pointer) { if (! is_array($pointer)) { continue; } $kind = self::normalizeNonEmptyString($pointer['kind'] ?? null, fallback: null); $value = $pointer['value'] ?? null; if ($kind === null) { continue; } if (! is_int($value) && ! is_string($value)) { continue; } if (is_string($value) && trim($value) === '') { continue; } $normalized[] = [ 'kind' => $kind, 'value' => is_int($value) ? $value : trim($value), ]; } return $normalized; } /** * @return array */ private static function normalizeNextSteps(mixed $steps): array { if (! is_array($steps)) { return []; } $normalized = []; foreach ($steps as $step) { if (! is_array($step)) { continue; } $label = self::normalizeNonEmptyString($step['label'] ?? null, fallback: null); $url = self::normalizeNonEmptyString($step['url'] ?? null, fallback: null); if ($label === null || $url === null) { continue; } $normalized[] = [ 'label' => $label, 'url' => $url, ]; } return $normalized; } /** * @param array $checks * @return array{total: int, pass: int, fail: int, warn: int, skip: int, running: int} */ private static function deriveCounts(array $checks): array { $counts = [ 'total' => count($checks), 'pass' => 0, 'fail' => 0, 'warn' => 0, 'skip' => 0, 'running' => 0, ]; foreach ($checks as $check) { $status = $check['status'] ?? null; if (! is_string($status) || ! array_key_exists($status, $counts)) { continue; } $counts[$status] += 1; } return $counts; } /** * @param array $checks * @param array{total: int, pass: int, fail: int, warn: int, skip: int, running: int} $counts */ private static function deriveOverall(array $checks, array $counts): string { if (($counts['running'] ?? 0) > 0) { return VerificationReportOverall::Running->value; } if (($counts['total'] ?? 0) === 0) { return VerificationReportOverall::NeedsAttention->value; } foreach ($checks as $check) { if (($check['status'] ?? null) === VerificationCheckStatus::Fail->value && ($check['blocking'] ?? false) === true) { return VerificationReportOverall::Blocked->value; } } if (($counts['fail'] ?? 0) > 0 || ($counts['warn'] ?? 0) > 0) { return VerificationReportOverall::NeedsAttention->value; } return VerificationReportOverall::Ready->value; } private static function normalizeNonEmptyString(mixed $value, ?string $fallback): ?string { if (! is_string($value)) { return $fallback; } $value = trim($value); if ($value === '') { return $fallback; } return $value; } }