relationLoaded('items') ? $backupSet->items : $backupSet->items()->get(); return $this->summarizeBackupItems( $items, max((int) ($backupSet->item_count ?? 0), $items->count()), ); } /** * @param iterable $items */ public function summarizeBackupItems(iterable $items, ?int $totalItems = null): BackupQualitySummary { $itemSummaries = Collection::make($items) ->map(fn (BackupItem $item): BackupQualitySummary => $this->forBackupItem($item)) ->values(); $resolvedTotalItems = max($itemSummaries->count(), (int) ($totalItems ?? 0)); $metadataOnlyCount = $itemSummaries->where('metadataOnlyCount', '>', 0)->count(); $assignmentIssueCount = $itemSummaries->where('assignmentIssueCount', '>', 0)->count(); $orphanedAssignmentCount = $itemSummaries->where('orphanedAssignmentCount', '>', 0)->count(); $integrityWarningCount = $itemSummaries->where('integrityWarningCount', '>', 0)->count(); $unknownQualityCount = $itemSummaries->where('unknownQualityCount', '>', 0)->count(); $degradedItemCount = $itemSummaries->filter( fn (BackupQualitySummary $summary): bool => $summary->hasDegradations() )->count(); $degradationFamilies = $this->orderedFamilies( $itemSummaries ->flatMap(fn (BackupQualitySummary $summary): array => $summary->degradationFamilies) ->all(), ); $qualityHighlights = $this->setHighlights( totalItems: $resolvedTotalItems, degradedItemCount: $degradedItemCount, metadataOnlyCount: $metadataOnlyCount, assignmentIssueCount: $assignmentIssueCount, orphanedAssignmentCount: $orphanedAssignmentCount, integrityWarningCount: $integrityWarningCount, unknownQualityCount: $unknownQualityCount, ); $compactSummary = $qualityHighlights === [] ? $this->defaultSetCompactSummary($resolvedTotalItems) : implode(' • ', $qualityHighlights); $summaryMessage = match (true) { $resolvedTotalItems === 0 => 'No backup items were captured in this set.', $degradedItemCount === 0 => sprintf( 'No degradations were detected across %d captured item%s.', $resolvedTotalItems, $resolvedTotalItems === 1 ? '' : 's', ), default => sprintf( '%d of %d captured item%s show degraded input quality.', $degradedItemCount, $resolvedTotalItems, $resolvedTotalItems === 1 ? '' : 's', ), }; $nextAction = match (true) { $resolvedTotalItems === 0 => 'Create or refresh a backup set before starting a restore review.', $degradedItemCount > 0 => 'Open the backup set detail and inspect degraded items before continuing into restore.', default => 'Open the backup set detail to verify item-level context before relying on it for restore work.', }; return new BackupQualitySummary( kind: 'backup_set', snapshotMode: $this->aggregateSnapshotMode($resolvedTotalItems, $metadataOnlyCount, $unknownQualityCount), totalItems: $resolvedTotalItems, degradedItemCount: $degradedItemCount, metadataOnlyCount: $metadataOnlyCount, assignmentIssueCount: $assignmentIssueCount, orphanedAssignmentCount: $orphanedAssignmentCount, integrityWarningCount: $integrityWarningCount, unknownQualityCount: $unknownQualityCount, hasAssignmentIssues: $assignmentIssueCount > 0, hasOrphanedAssignments: $orphanedAssignmentCount > 0, assignmentCaptureReason: null, integrityWarning: null, degradationFamilies: $degradationFamilies, qualityHighlights: $qualityHighlights, compactSummary: $compactSummary, summaryMessage: $summaryMessage, nextAction: $nextAction, positiveClaimBoundary: $this->positiveClaimBoundary(), ); } public function forBackupItem(BackupItem $backupItem): BackupQualitySummary { $snapshotMode = $this->resolveSnapshotMode( source: $backupItem->snapshotSource(), warnings: $backupItem->warningMessages(), hasCapturedPayload: $backupItem->hasCapturedPayload(), ); $assignmentCaptureReason = $backupItem->assignmentCaptureReason(); $integrityWarning = $backupItem->integrityWarning(); $hasAssignmentIssues = $backupItem->assignmentsFetchFailed(); $hasOrphanedAssignments = $backupItem->hasOrphanedAssignments(); $degradationFamilies = $this->singleRecordFamilies( snapshotMode: $snapshotMode, hasAssignmentIssues: $hasAssignmentIssues, hasOrphanedAssignments: $hasOrphanedAssignments, integrityWarning: $integrityWarning, ); $qualityHighlights = $this->singleRecordHighlights( snapshotMode: $snapshotMode, hasAssignmentIssues: $hasAssignmentIssues, hasOrphanedAssignments: $hasOrphanedAssignments, integrityWarning: $integrityWarning, assignmentCaptureReason: $assignmentCaptureReason, ); return new BackupQualitySummary( kind: 'backup_item', snapshotMode: $snapshotMode, totalItems: 1, degradedItemCount: $degradationFamilies === [] ? 0 : 1, metadataOnlyCount: $snapshotMode === 'metadata_only' ? 1 : 0, assignmentIssueCount: $hasAssignmentIssues ? 1 : 0, orphanedAssignmentCount: $hasOrphanedAssignments ? 1 : 0, integrityWarningCount: $integrityWarning !== null ? 1 : 0, unknownQualityCount: $degradationFamilies === ['unknown_quality'] ? 1 : 0, hasAssignmentIssues: $hasAssignmentIssues, hasOrphanedAssignments: $hasOrphanedAssignments, assignmentCaptureReason: $assignmentCaptureReason, integrityWarning: $integrityWarning, degradationFamilies: $degradationFamilies, qualityHighlights: $qualityHighlights, compactSummary: $this->compactSummaryFromHighlights($qualityHighlights, $snapshotMode), summaryMessage: $this->singleRecordSummaryMessage($qualityHighlights, $snapshotMode), nextAction: $degradationFamilies === [] ? 'Open the linked detail if you need deeper restore context.' : 'Inspect the linked detail before relying on this backup item for restore.', positiveClaimBoundary: $this->positiveClaimBoundary(), ); } public function forPolicyVersion(PolicyVersion $policyVersion): BackupQualitySummary { $snapshotMode = $this->resolveSnapshotMode( source: $policyVersion->snapshotSource(), warnings: $policyVersion->warningMessages(), hasCapturedPayload: $policyVersion->hasCapturedPayload(), ); $integrityWarning = $policyVersion->integrityWarning(); $hasAssignmentIssues = $policyVersion->assignmentsFetchFailed(); $hasOrphanedAssignments = $policyVersion->hasOrphanedAssignments(); $degradationFamilies = $this->singleRecordFamilies( snapshotMode: $snapshotMode, hasAssignmentIssues: $hasAssignmentIssues, hasOrphanedAssignments: $hasOrphanedAssignments, integrityWarning: $integrityWarning, ); $qualityHighlights = $this->singleRecordHighlights( snapshotMode: $snapshotMode, hasAssignmentIssues: $hasAssignmentIssues, hasOrphanedAssignments: $hasOrphanedAssignments, integrityWarning: $integrityWarning, ); return new BackupQualitySummary( kind: 'policy_version', snapshotMode: $snapshotMode, totalItems: 1, degradedItemCount: $degradationFamilies === [] ? 0 : 1, metadataOnlyCount: $snapshotMode === 'metadata_only' ? 1 : 0, assignmentIssueCount: $hasAssignmentIssues ? 1 : 0, orphanedAssignmentCount: $hasOrphanedAssignments ? 1 : 0, integrityWarningCount: $integrityWarning !== null ? 1 : 0, unknownQualityCount: $degradationFamilies === ['unknown_quality'] ? 1 : 0, hasAssignmentIssues: $hasAssignmentIssues, hasOrphanedAssignments: $hasOrphanedAssignments, assignmentCaptureReason: null, integrityWarning: $integrityWarning, degradationFamilies: $degradationFamilies, qualityHighlights: $qualityHighlights, compactSummary: $this->compactSummaryFromHighlights($qualityHighlights, $snapshotMode), summaryMessage: $this->singleRecordSummaryMessage($qualityHighlights, $snapshotMode), nextAction: $degradationFamilies === [] ? 'Open the version detail if you need raw settings or diff context.' : 'Prefer a stronger version or inspect the version detail before restore.', positiveClaimBoundary: $this->positiveClaimBoundary(), ); } /** * @param list $warnings */ private function resolveSnapshotMode(?string $source, array $warnings, bool $hasCapturedPayload): string { if ($source === 'metadata_only' || $this->warningsIndicateMetadataOnly($warnings)) { return 'metadata_only'; } if ($hasCapturedPayload) { return 'full'; } return 'unknown'; } /** * @param list $warnings */ private function warningsIndicateMetadataOnly(array $warnings): bool { return Collection::make($warnings) ->contains(function (mixed $warning): bool { if (! is_string($warning)) { return false; } $normalized = Str::lower($warning); return str_contains($normalized, 'metadata') && ( str_contains($normalized, 'only') || str_contains($normalized, 'fallback') ); }); } /** * @return list */ private function singleRecordFamilies( string $snapshotMode, bool $hasAssignmentIssues, bool $hasOrphanedAssignments, ?string $integrityWarning, ): array { $families = []; if ($snapshotMode === 'metadata_only') { $families[] = 'metadata_only'; } if ($hasAssignmentIssues) { $families[] = 'assignment_capture_issue'; } if ($hasOrphanedAssignments) { $families[] = 'orphaned_assignments'; } if ($integrityWarning !== null) { $families[] = 'integrity_warning'; } if ($families === [] && $snapshotMode === 'unknown') { $families[] = 'unknown_quality'; } return $this->orderedFamilies($families); } /** * @return list */ private function singleRecordHighlights( string $snapshotMode, bool $hasAssignmentIssues, bool $hasOrphanedAssignments, ?string $integrityWarning, ?string $assignmentCaptureReason = null, ): array { $highlights = []; if ($snapshotMode === 'metadata_only') { $highlights[] = 'Metadata only'; } if ($hasAssignmentIssues) { $highlights[] = 'Assignment fetch failed'; } elseif ($assignmentCaptureReason === 'separate_role_assignments') { $highlights[] = 'Assignments captured separately'; } if ($hasOrphanedAssignments) { $highlights[] = 'Orphaned assignments'; } if ($integrityWarning !== null) { $highlights[] = 'Integrity warning'; } if ($snapshotMode === 'unknown' && $highlights === []) { $highlights[] = 'Unknown quality'; } return array_values(array_unique($highlights)); } private function compactSummaryFromHighlights(array $qualityHighlights, string $snapshotMode): string { if ($qualityHighlights !== []) { return implode(' • ', $qualityHighlights); } return match ($snapshotMode) { 'full' => 'Full payload', 'unknown' => 'Unknown quality', default => 'No degradations detected', }; } private function singleRecordSummaryMessage(array $qualityHighlights, string $snapshotMode): string { if ($qualityHighlights === []) { return match ($snapshotMode) { 'full' => 'No degradations were detected from the captured snapshot and assignment metadata.', 'unknown' => 'Quality is unknown because this record lacks enough completeness metadata to justify a stronger claim.', default => 'No degradations were detected.', }; } return implode(' • ', $qualityHighlights).'.'; } private function aggregateSnapshotMode(int $totalItems, int $metadataOnlyCount, int $unknownQualityCount): string { if ($totalItems === 0) { return 'unknown'; } if ($metadataOnlyCount === $totalItems) { return 'metadata_only'; } if ($metadataOnlyCount === 0 && $unknownQualityCount === 0) { return 'full'; } return 'unknown'; } /** * @return list */ private function orderedFamilies(array $families): array { $weights = [ 'metadata_only' => 10, 'assignment_capture_issue' => 20, 'orphaned_assignments' => 30, 'integrity_warning' => 40, 'unknown_quality' => 50, ]; $families = array_values(array_unique(array_filter( $families, static fn (mixed $family): bool => is_string($family) && $family !== '', ))); usort($families, static function (string $left, string $right) use ($weights): int { return ($weights[$left] ?? 999) <=> ($weights[$right] ?? 999); }); return $families; } /** * @return list */ private function setHighlights( int $totalItems, int $degradedItemCount, int $metadataOnlyCount, int $assignmentIssueCount, int $orphanedAssignmentCount, int $integrityWarningCount, int $unknownQualityCount, ): array { if ($totalItems === 0) { return []; } $highlights = []; if ($degradedItemCount > 0) { $highlights[] = sprintf( '%d degraded item%s', $degradedItemCount, $degradedItemCount === 1 ? '' : 's', ); } if ($metadataOnlyCount > 0) { $highlights[] = sprintf( '%d metadata-only', $metadataOnlyCount, ); } if ($assignmentIssueCount > 0) { $highlights[] = sprintf( '%d assignment issue%s', $assignmentIssueCount, $assignmentIssueCount === 1 ? '' : 's', ); } if ($orphanedAssignmentCount > 0) { $highlights[] = sprintf( '%d orphaned assignment%s', $orphanedAssignmentCount, $orphanedAssignmentCount === 1 ? '' : 's', ); } if ($integrityWarningCount > 0) { $highlights[] = sprintf( '%d integrity warning%s', $integrityWarningCount, $integrityWarningCount === 1 ? '' : 's', ); } if ($unknownQualityCount > 0) { $highlights[] = sprintf( '%d unknown quality', $unknownQualityCount, ); } return $highlights; } private function defaultSetCompactSummary(int $totalItems): string { if ($totalItems === 0) { return 'No items captured'; } return sprintf( 'No degradations detected across %d item%s', $totalItems, $totalItems === 1 ? '' : 's', ); } private function positiveClaimBoundary(): string { return 'Input quality signals do not prove safe restore, restore readiness, or tenant-wide recoverability.'; } }