|null */ public static function normalizeReport(mixed $report): ?array { if (! is_array($report)) { return null; } if (! self::isValidReport($report)) { return null; } return $report; } /** * @param array $report */ public static function isValidReport(array $report): bool { $schemaVersion = self::schemaVersion($report); if ($schemaVersion === null || ! self::isSupportedSchemaVersion($schemaVersion)) { return false; } if (! self::isNonEmptyString($report['flow'] ?? null)) { return false; } if (! self::isIsoDateTimeString($report['generated_at'] ?? null)) { return false; } if (array_key_exists('identity', $report) && ! is_array($report['identity'])) { return false; } $summary = $report['summary'] ?? null; if (! is_array($summary)) { return false; } $overall = $summary['overall'] ?? null; if (! is_string($overall) || ! in_array($overall, VerificationReportOverall::values(), true)) { return false; } $counts = $summary['counts'] ?? null; if (! is_array($counts)) { return false; } foreach (['total', 'pass', 'fail', 'warn', 'skip', 'running'] as $key) { if (! self::isNonNegativeInt($counts[$key] ?? null)) { return false; } } $checks = $report['checks'] ?? null; if (! is_array($checks)) { return false; } foreach ($checks as $check) { if (! is_array($check) || ! self::isValidCheckResult($check)) { return false; } } return true; } /** * @param array $report */ public static function schemaVersion(array $report): ?string { $candidate = $report['schema_version'] ?? null; if (! is_string($candidate)) { return null; } $candidate = trim($candidate); if ($candidate === '') { return null; } if (! preg_match('/^\d+\.\d+\.\d+$/', $candidate)) { return null; } return $candidate; } public static function isSupportedSchemaVersion(string $schemaVersion): bool { $parts = explode('.', $schemaVersion, 3); if (count($parts) !== 3) { return false; } $major = (int) $parts[0]; return $major === 1; } /** * @param array $check */ private static function isValidCheckResult(array $check): bool { if (! self::isNonEmptyString($check['key'] ?? null)) { return false; } if (! self::isNonEmptyString($check['title'] ?? null)) { return false; } $status = $check['status'] ?? null; if (! is_string($status) || ! in_array($status, VerificationCheckStatus::values(), true)) { return false; } $severity = $check['severity'] ?? null; if (! is_string($severity) || ! in_array($severity, VerificationCheckSeverity::values(), true)) { return false; } if (! is_bool($check['blocking'] ?? null)) { return false; } if (! self::isNonEmptyString($check['reason_code'] ?? null)) { return false; } if (! self::isNonEmptyString($check['message'] ?? null)) { return false; } $evidence = $check['evidence'] ?? null; if (! is_array($evidence)) { return false; } foreach ($evidence as $pointer) { if (! is_array($pointer) || ! self::isValidEvidencePointer($pointer)) { return false; } } $nextSteps = $check['next_steps'] ?? null; if (! is_array($nextSteps)) { return false; } foreach ($nextSteps as $step) { if (! is_array($step) || ! self::isValidNextStep($step)) { return false; } } return true; } /** * @param array $pointer */ private static function isValidEvidencePointer(array $pointer): bool { if (! self::isNonEmptyString($pointer['kind'] ?? null)) { return false; } $value = $pointer['value'] ?? null; return is_int($value) || self::isNonEmptyString($value); } /** * @param array $step */ private static function isValidNextStep(array $step): bool { if (! self::isNonEmptyString($step['label'] ?? null)) { return false; } if (! self::isNonEmptyString($step['url'] ?? null)) { return false; } return true; } private static function isNonEmptyString(mixed $value): bool { return is_string($value) && trim($value) !== ''; } private static function isNonNegativeInt(mixed $value): bool { return is_int($value) && $value >= 0; } private static function isIsoDateTimeString(mixed $value): bool { if (! self::isNonEmptyString($value)) { return false; } try { new DateTimeImmutable((string) $value); return true; } catch (\Throwable) { return false; } } }