$filters * @return array */ public function build(BaselineProfile $profile, User $user, array $filters = []): array { $normalizedFilters = $this->normalizeFilters($filters); $assignments = BaselineTenantAssignment::query() ->inWorkspace((int) $profile->workspace_id) ->forBaselineProfile($profile) ->with('tenant') ->get(); $visibleTenants = $this->visibleTenants($assignments, $user); $referenceResolution = $this->snapshotTruthResolver->resolveCompareSnapshot($profile); $referenceSnapshot = $this->resolvedSnapshot($referenceResolution); $referenceReasonCode = is_string($referenceResolution['reason_code'] ?? null) ? trim((string) $referenceResolution['reason_code']) : null; $reference = [ 'workspaceId' => (int) $profile->workspace_id, 'baselineProfileId' => (int) $profile->getKey(), 'baselineProfileName' => (string) $profile->name, 'baselineStatus' => $profile->status instanceof BaselineProfileStatus ? $profile->status->value : (string) $profile->status, 'referenceSnapshotId' => $referenceSnapshot?->getKey(), 'referenceSnapshotCapturedAt' => $referenceSnapshot?->captured_at?->toIso8601String(), 'referenceState' => $referenceSnapshot instanceof BaselineSnapshot ? 'ready' : 'no_snapshot', 'referenceReasonCode' => $referenceReasonCode, 'assignedTenantCount' => $assignments->count(), 'visibleTenantCount' => $visibleTenants->count(), ]; $snapshotItems = $referenceSnapshot instanceof BaselineSnapshot ? BaselineSnapshotItem::query() ->where('baseline_snapshot_id', (int) $referenceSnapshot->getKey()) ->orderBy('policy_type') ->orderBy('subject_key') ->orderBy('id') ->get() : collect(); $policyTypeOptions = $snapshotItems ->pluck('policy_type') ->filter(static fn (mixed $type): bool => is_string($type) && trim($type) !== '') ->unique() ->sort() ->mapWithKeys(static fn (string $type): array => [ $type => InventoryPolicyTypeMeta::label($type) ?? $type, ]) ->all(); $bundle = [ 'reference' => $reference, 'filters' => [ 'policyTypes' => $normalizedFilters['policyTypes'], 'states' => $normalizedFilters['states'], 'severities' => $normalizedFilters['severities'], 'tenantSort' => $normalizedFilters['tenantSort'], 'subjectSort' => $normalizedFilters['subjectSort'], 'focusedSubjectKey' => $normalizedFilters['focusedSubjectKey'], ], 'policyTypeOptions' => $policyTypeOptions, 'stateOptions' => BadgeCatalog::options(BadgeDomain::BaselineCompareMatrixState, [ 'match', 'differ', 'missing', 'ambiguous', 'not_compared', 'stale_result', ]), 'severityOptions' => BadgeCatalog::options(BadgeDomain::FindingSeverity, [ Finding::SEVERITY_LOW, Finding::SEVERITY_MEDIUM, Finding::SEVERITY_HIGH, Finding::SEVERITY_CRITICAL, ]), 'tenantSortOptions' => [ 'tenant_name' => 'Tenant name', 'deviation_count' => 'Deviation count', 'freshness_urgency' => 'Freshness urgency', ], 'subjectSortOptions' => [ 'deviation_breadth' => 'Deviation breadth', 'policy_type' => 'Policy type', 'display_name' => 'Display name', ], 'stateLegend' => $this->legendSpecs(BadgeDomain::BaselineCompareMatrixState, [ 'match', 'differ', 'missing', 'ambiguous', 'not_compared', 'stale_result', ]), 'freshnessLegend' => $this->legendSpecs(BadgeDomain::BaselineCompareMatrixFreshness, [ 'fresh', 'stale', 'never_compared', 'unknown', ]), 'trustLegend' => $this->legendSpecs(BadgeDomain::BaselineCompareMatrixTrust, [ 'trustworthy', 'limited_confidence', 'diagnostic_only', 'unusable', ]), 'tenantSummaries' => [], 'subjectSummaries' => [], 'rows' => [], 'emptyState' => $this->emptyState( reference: $reference, snapshotItemsCount: $snapshotItems->count(), visibleTenantsCount: $visibleTenants->count(), ), 'hasActiveRuns' => false, ]; if (! $referenceSnapshot instanceof BaselineSnapshot) { return $bundle; } if ($visibleTenants->isEmpty()) { return $bundle; } if ($snapshotItems->isEmpty()) { return $bundle; } $tenantIds = $visibleTenants ->map(static fn (Tenant $tenant): int => (int) $tenant->getKey()) ->values() ->all(); $latestRuns = OperationRun::latestBaselineCompareRunsForProfile( profile: $profile, tenantIds: $tenantIds, workspaceId: (int) $profile->workspace_id, )->keyBy(static fn (OperationRun $run): int => (int) $run->tenant_id); $completedRuns = OperationRun::latestBaselineCompareRunsForProfile( profile: $profile, tenantIds: $tenantIds, workspaceId: (int) $profile->workspace_id, completedOnly: true, )->keyBy(static fn (OperationRun $run): int => (int) $run->tenant_id); $findingMap = $this->findingMap($profile, $tenantIds, $completedRuns); $rows = []; foreach ($snapshotItems as $item) { if (! $item instanceof BaselineSnapshotItem) { continue; } $subjectKey = is_string($item->subject_key) ? trim($item->subject_key) : ''; if ($subjectKey === '') { continue; } $subject = [ 'subjectKey' => $subjectKey, 'policyType' => (string) $item->policy_type, 'displayName' => $this->subjectDisplayName($item), 'baselineExternalId' => is_string($item->subject_external_id) ? $item->subject_external_id : null, ]; $cells = []; foreach ($visibleTenants as $tenant) { $tenantId = (int) $tenant->getKey(); $latestRun = $latestRuns->get($tenantId); $completedRun = $completedRuns->get($tenantId); $cells[] = $this->cellFor( item: $item, tenant: $tenant, referenceSnapshot: $referenceSnapshot, latestRun: $latestRun instanceof OperationRun ? $latestRun : null, completedRun: $completedRun instanceof OperationRun ? $completedRun : null, finding: $findingMap[$this->cellKey($tenantId, $subjectKey)] ?? null, ); } if (! $this->rowMatchesFilters($subject, $cells, $normalizedFilters)) { continue; } $rows[] = [ 'subject' => $this->subjectSummary($subject, $cells), 'cells' => $cells, ]; } $rows = $this->sortRows($rows, $normalizedFilters['subjectSort']); $tenantSummaries = $this->sortTenantSummaries( tenantSummaries: $this->tenantSummaries($visibleTenants, $latestRuns, $completedRuns, $rows, $referenceSnapshot), sort: $normalizedFilters['tenantSort'], ); foreach ($rows as &$row) { $row['cells'] = $this->sortCellsForTenants($row['cells'], $tenantSummaries); } unset($row); $bundle['tenantSummaries'] = $tenantSummaries; $bundle['subjectSummaries'] = array_map( static fn (array $row): array => $row['subject'], $rows, ); $bundle['rows'] = $rows; $bundle['emptyState'] = $this->emptyState( reference: $reference, snapshotItemsCount: $snapshotItems->count(), visibleTenantsCount: $visibleTenants->count(), renderedRowsCount: count($rows), ); $bundle['hasActiveRuns'] = collect($tenantSummaries) ->contains(static fn (array $summary): bool => in_array((string) ($summary['compareRunStatus'] ?? ''), [ OperationRunStatus::Queued->value, OperationRunStatus::Running->value, ], true)); return $bundle; } /** * @param array $filters * @return array{ * policyTypes: list, * states: list, * severities: list, * tenantSort: string, * subjectSort: string, * focusedSubjectKey: ?string * } */ private function normalizeFilters(array $filters): array { $policyTypes = $this->normalizeStringList($filters['policy_type'] ?? $filters['policyTypes'] ?? []); $states = array_values(array_intersect( $this->normalizeStringList($filters['state'] ?? $filters['states'] ?? []), ['match', 'differ', 'missing', 'ambiguous', 'not_compared', 'stale_result'], )); $severities = array_values(array_intersect( $this->normalizeStringList($filters['severity'] ?? $filters['severities'] ?? []), [Finding::SEVERITY_LOW, Finding::SEVERITY_MEDIUM, Finding::SEVERITY_HIGH, Finding::SEVERITY_CRITICAL], )); $tenantSort = in_array((string) ($filters['tenant_sort'] ?? $filters['tenantSort'] ?? 'tenant_name'), [ 'tenant_name', 'deviation_count', 'freshness_urgency', ], true) ? (string) ($filters['tenant_sort'] ?? $filters['tenantSort'] ?? 'tenant_name') : 'tenant_name'; $subjectSort = in_array((string) ($filters['subject_sort'] ?? $filters['subjectSort'] ?? 'deviation_breadth'), [ 'deviation_breadth', 'policy_type', 'display_name', ], true) ? (string) ($filters['subject_sort'] ?? $filters['subjectSort'] ?? 'deviation_breadth') : 'deviation_breadth'; $focusedSubjectKey = $filters['subject_key'] ?? $filters['focusedSubjectKey'] ?? null; $focusedSubjectKey = is_string($focusedSubjectKey) && trim($focusedSubjectKey) !== '' ? trim($focusedSubjectKey) : null; return [ 'policyTypes' => $policyTypes, 'states' => $states, 'severities' => $severities, 'tenantSort' => $tenantSort, 'subjectSort' => $subjectSort, 'focusedSubjectKey' => $focusedSubjectKey, ]; } /** * @return list */ private function normalizeStringList(mixed $value): array { $values = is_array($value) ? $value : [$value]; return array_values(array_unique(array_filter(array_map(static function (mixed $item): ?string { if (! is_string($item)) { return null; } $normalized = trim($item); return $normalized !== '' ? $normalized : null; }, $values)))); } /** * @param Collection $assignments * @return Collection */ private function visibleTenants(Collection $assignments, User $user): Collection { return $assignments ->map(static fn (BaselineTenantAssignment $assignment): ?Tenant => $assignment->tenant) ->filter(fn (?Tenant $tenant): bool => $tenant instanceof Tenant && $this->capabilityResolver->isMember($user, $tenant) && $this->capabilityResolver->can($user, $tenant, Capabilities::TENANT_VIEW)) ->sortBy(static fn (Tenant $tenant): string => Str::lower((string) $tenant->name)) ->values(); } /** * @param array $resolution */ private function resolvedSnapshot(array $resolution): ?BaselineSnapshot { $snapshot = $resolution['effective_snapshot'] ?? $resolution['snapshot'] ?? null; return $snapshot instanceof BaselineSnapshot ? $snapshot : null; } /** * @param array $tenantIds * @param Collection $completedRuns * @return array */ private function findingMap(BaselineProfile $profile, array $tenantIds, Collection $completedRuns): array { $findings = Finding::query() ->baselineCompareForProfile($profile) ->whereIn('tenant_id', $tenantIds) ->orderByDesc('last_seen_at') ->orderByDesc('id') ->get(); $map = []; foreach ($findings as $finding) { if (! $finding instanceof Finding) { continue; } $tenantId = (int) $finding->tenant_id; $subjectKey = $this->subjectKeyForFinding($finding); if ($subjectKey === null) { continue; } $completedRun = $completedRuns->get($tenantId); if ( $completedRun instanceof OperationRun && (int) ($finding->current_operation_run_id ?? 0) !== (int) $completedRun->getKey() ) { continue; } $cellKey = $this->cellKey($tenantId, $subjectKey); if (! array_key_exists($cellKey, $map)) { $map[$cellKey] = $finding; } } return $map; } private function subjectKeyForFinding(Finding $finding): ?string { $subjectKey = data_get($finding->evidence_jsonb, 'subject_key'); if (is_string($subjectKey) && trim($subjectKey) !== '') { return trim($subjectKey); } return null; } private function cellKey(int $tenantId, string $subjectKey): string { return $tenantId.'|'.trim(mb_strtolower($subjectKey)); } private function subjectDisplayName(BaselineSnapshotItem $item): ?string { $displayName = data_get($item->meta_jsonb, 'display_name'); if (is_string($displayName) && trim($displayName) !== '') { return trim($displayName); } return is_string($item->subject_key) && trim($item->subject_key) !== '' ? Str::headline($item->subject_key) : null; } private function cellFor( BaselineSnapshotItem $item, Tenant $tenant, BaselineSnapshot $referenceSnapshot, ?OperationRun $latestRun, ?OperationRun $completedRun, ?Finding $finding, ): array { $subjectKey = (string) $item->subject_key; $policyType = (string) $item->policy_type; $completedAt = $completedRun?->finished_at; $policyTypeCovered = $this->policyTypeCovered($completedRun, $policyType); $subjectReasons = $completedRun instanceof OperationRun ? (BaselineCompareEvidenceGapDetails::subjectReasonsFromOperationRun($completedRun)[BaselineCompareEvidenceGapDetails::subjectCompositeKey($policyType, $subjectKey)] ?? []) : []; $reasonCode = $subjectReasons[0] ?? $this->runReasonCode($completedRun); $changeType = is_string(data_get($finding?->evidence_jsonb, 'change_type')) ? (string) data_get($finding?->evidence_jsonb, 'change_type') : null; $staleResult = $this->isStaleResult($completedRun, $referenceSnapshot); $tenantTrustLevel = $this->tenantTrustLevel($completedRun); $state = match (true) { ! $completedRun instanceof OperationRun => 'not_compared', (string) $completedRun->outcome === OperationRunOutcome::Failed->value => 'not_compared', ! $policyTypeCovered => 'not_compared', $staleResult => 'stale_result', $subjectReasons !== [] => 'ambiguous', $changeType === 'missing_policy' => 'missing', $finding instanceof Finding => 'differ', default => 'match', }; $trustLevel = match ($state) { 'not_compared' => 'unusable', 'stale_result' => 'limited_confidence', 'ambiguous' => 'diagnostic_only', default => $tenantTrustLevel, }; return [ 'tenantId' => (int) $tenant->getKey(), 'subjectKey' => $subjectKey, 'state' => $state, 'severity' => $finding instanceof Finding ? (string) $finding->severity : null, 'trustLevel' => $trustLevel, 'reasonCode' => $reasonCode, 'compareRunId' => $completedRun?->getKey(), 'findingId' => $finding?->getKey(), 'findingWorkflowState' => $finding instanceof Finding ? (string) $finding->status : null, 'lastComparedAt' => $completedAt?->toIso8601String(), 'policyTypeCovered' => $policyTypeCovered, 'latestRunId' => $latestRun?->getKey(), 'latestRunStatus' => $latestRun?->status, ]; } private function policyTypeCovered(?OperationRun $run, string $policyType): bool { if (! $run instanceof OperationRun) { return false; } $coverage = data_get($run->context, 'baseline_compare.coverage'); if (! is_array($coverage)) { return true; } $coveredTypes = is_array($coverage['covered_types'] ?? null) ? array_values(array_filter($coverage['covered_types'], 'is_string')) : []; $uncoveredTypes = is_array($coverage['uncovered_types'] ?? null) ? array_values(array_filter($coverage['uncovered_types'], 'is_string')) : []; if (in_array($policyType, $uncoveredTypes, true)) { return false; } if ($coveredTypes === []) { return true; } return in_array($policyType, $coveredTypes, true); } private function runReasonCode(?OperationRun $run): ?string { $reasonCode = data_get($run?->context, 'baseline_compare.reason_code'); return is_string($reasonCode) && trim($reasonCode) !== '' ? trim($reasonCode) : null; } private function isStaleResult(?OperationRun $run, BaselineSnapshot $referenceSnapshot): bool { if (! $run instanceof OperationRun || ! $run->finished_at) { return false; } $runSnapshotId = data_get($run->context, 'baseline_snapshot_id'); if (is_numeric($runSnapshotId) && (int) $runSnapshotId !== (int) $referenceSnapshot->getKey()) { return true; } if ($referenceSnapshot->captured_at && $run->finished_at->lt($referenceSnapshot->captured_at)) { return true; } return BaselineCompareSummaryAssessor::isStaleComparedAt($run->finished_at); } private function tenantTrustLevel(?OperationRun $run): string { return BadgeCatalog::normalizeState( $this->explanationRegistry->trustLevelForRun($run), ) ?? 'unusable'; } /** * @param array $subject * @param list> $cells * @param array $filters */ private function rowMatchesFilters(array $subject, array $cells, array $filters): bool { if ($filters['policyTypes'] !== [] && ! in_array((string) $subject['policyType'], $filters['policyTypes'], true)) { return false; } if ($filters['focusedSubjectKey'] !== null && (string) $subject['subjectKey'] !== $filters['focusedSubjectKey']) { return false; } foreach ($cells as $cell) { if ($filters['states'] !== [] && ! in_array((string) ($cell['state'] ?? ''), $filters['states'], true)) { continue; } if ($filters['severities'] !== [] && ! in_array((string) ($cell['severity'] ?? ''), $filters['severities'], true)) { continue; } return true; } return $filters['states'] === [] && $filters['severities'] === []; } /** * @param list> $cells * @return array */ private function subjectSummary(array $subject, array $cells): array { return [ 'subjectKey' => $subject['subjectKey'], 'policyType' => $subject['policyType'], 'displayName' => $subject['displayName'], 'baselineExternalId' => $subject['baselineExternalId'], 'deviationBreadth' => $this->countStates($cells, ['differ', 'missing']), 'missingBreadth' => $this->countStates($cells, ['missing']), 'ambiguousBreadth' => $this->countStates($cells, ['ambiguous']), 'notComparedBreadth' => $this->countStates($cells, ['not_compared']), 'maxSeverity' => $this->maxSeverity($cells), 'trustLevel' => $this->worstTrustLevel($cells), ]; } /** * @param Collection $visibleTenants * @param Collection $latestRuns * @param Collection $completedRuns * @param list> $rows * @return list> */ private function tenantSummaries( Collection $visibleTenants, Collection $latestRuns, Collection $completedRuns, array $rows, BaselineSnapshot $referenceSnapshot, ): array { $summaries = []; foreach ($visibleTenants as $tenant) { $tenantId = (int) $tenant->getKey(); $latestRun = $latestRuns->get($tenantId); $completedRun = $completedRuns->get($tenantId); $cells = array_map( static fn (array $row): array => collect($row['cells'])->firstWhere('tenantId', $tenantId) ?? [], $rows, ); $summaries[] = [ 'tenantId' => $tenantId, 'tenantName' => (string) $tenant->name, 'compareRunId' => $latestRun?->getKey(), 'compareRunStatus' => $latestRun?->status, 'compareRunOutcome' => $latestRun?->outcome, 'freshnessState' => $this->freshnessState($completedRun, $referenceSnapshot), 'lastComparedAt' => $completedRun?->finished_at?->toIso8601String(), 'matchedCount' => $this->countStates($cells, ['match']), 'differingCount' => $this->countStates($cells, ['differ']), 'missingCount' => $this->countStates($cells, ['missing']), 'ambiguousCount' => $this->countStates($cells, ['ambiguous']), 'notComparedCount' => $this->countStates($cells, ['not_compared']), 'maxSeverity' => $this->maxSeverity($cells), 'trustLevel' => $this->worstTrustLevel($cells), ]; } return $summaries; } private function freshnessState(?OperationRun $completedRun, BaselineSnapshot $referenceSnapshot): string { if (! $completedRun instanceof OperationRun) { return 'never_compared'; } if ((string) $completedRun->outcome === OperationRunOutcome::Failed->value) { return 'unknown'; } if ($this->isStaleResult($completedRun, $referenceSnapshot)) { return 'stale'; } return 'fresh'; } /** * @param list> $cells * @param array $states */ private function countStates(array $cells, array $states): int { return count(array_filter( $cells, static fn (array $cell): bool => in_array((string) ($cell['state'] ?? ''), $states, true), )); } /** * @param list> $cells */ private function maxSeverity(array $cells): ?string { $ranked = collect($cells) ->map(static fn (array $cell): ?string => is_string($cell['severity'] ?? null) ? (string) $cell['severity'] : null) ->filter() ->sortByDesc(fn (string $severity): int => $this->severityRank($severity)) ->values(); return $ranked->first(); } private function severityRank(string $severity): int { return match ($severity) { Finding::SEVERITY_CRITICAL => 4, Finding::SEVERITY_HIGH => 3, Finding::SEVERITY_MEDIUM => 2, Finding::SEVERITY_LOW => 1, default => 0, }; } /** * @param list> $cells */ private function worstTrustLevel(array $cells): string { return collect($cells) ->map(static fn (array $cell): string => (string) ($cell['trustLevel'] ?? 'unusable')) ->sortByDesc(fn (string $trust): int => $this->trustRank($trust)) ->first() ?? 'unusable'; } private function trustRank(string $trustLevel): int { return match ($trustLevel) { 'unusable' => 4, 'diagnostic_only' => 3, 'limited_confidence' => 2, 'trustworthy' => 1, default => 0, }; } /** * @param list> $rows * @return list> */ private function sortRows(array $rows, string $sort): array { usort($rows, function (array $left, array $right) use ($sort): int { $leftSubject = $left['subject'] ?? []; $rightSubject = $right['subject'] ?? []; return match ($sort) { 'policy_type' => [(string) ($leftSubject['policyType'] ?? ''), Str::lower((string) ($leftSubject['displayName'] ?? ''))] <=> [(string) ($rightSubject['policyType'] ?? ''), Str::lower((string) ($rightSubject['displayName'] ?? ''))], 'display_name' => [Str::lower((string) ($leftSubject['displayName'] ?? '')), (string) ($leftSubject['subjectKey'] ?? '')] <=> [Str::lower((string) ($rightSubject['displayName'] ?? '')), (string) ($rightSubject['subjectKey'] ?? '')], default => [ -1 * (int) ($leftSubject['deviationBreadth'] ?? 0), -1 * (int) ($leftSubject['ambiguousBreadth'] ?? 0), Str::lower((string) ($leftSubject['displayName'] ?? '')), ] <=> [ -1 * (int) ($rightSubject['deviationBreadth'] ?? 0), -1 * (int) ($rightSubject['ambiguousBreadth'] ?? 0), Str::lower((string) ($rightSubject['displayName'] ?? '')), ], }; }); return array_values($rows); } /** * @param list> $tenantSummaries * @return list> */ private function sortTenantSummaries(array $tenantSummaries, string $sort): array { usort($tenantSummaries, function (array $left, array $right) use ($sort): int { return match ($sort) { 'deviation_count' => [ -1 * ((int) ($left['differingCount'] ?? 0) + (int) ($left['missingCount'] ?? 0) + (int) ($left['ambiguousCount'] ?? 0)), Str::lower((string) ($left['tenantName'] ?? '')), ] <=> [ -1 * ((int) ($right['differingCount'] ?? 0) + (int) ($right['missingCount'] ?? 0) + (int) ($right['ambiguousCount'] ?? 0)), Str::lower((string) ($right['tenantName'] ?? '')), ], 'freshness_urgency' => [ -1 * $this->freshnessRank((string) ($left['freshnessState'] ?? 'unknown')), Str::lower((string) ($left['tenantName'] ?? '')), ] <=> [ -1 * $this->freshnessRank((string) ($right['freshnessState'] ?? 'unknown')), Str::lower((string) ($right['tenantName'] ?? '')), ], default => Str::lower((string) ($left['tenantName'] ?? '')) <=> Str::lower((string) ($right['tenantName'] ?? '')), }; }); return array_values($tenantSummaries); } private function freshnessRank(string $freshnessState): int { return match ($freshnessState) { 'stale' => 4, 'unknown' => 3, 'never_compared' => 2, 'fresh' => 1, default => 0, }; } /** * @param list> $cells * @param list> $tenantSummaries * @return list> */ private function sortCellsForTenants(array $cells, array $tenantSummaries): array { $order = collect($tenantSummaries) ->values() ->mapWithKeys(static fn (array $summary, int $index): array => [ (int) ($summary['tenantId'] ?? 0) => $index, ]); usort($cells, static fn (array $left, array $right): int => ($order[(int) ($left['tenantId'] ?? 0)] ?? 9999) <=> ($order[(int) ($right['tenantId'] ?? 0)] ?? 9999)); return array_values($cells); } /** * @param array $reference * @return array{title: string, body: string}|null */ private function emptyState( array $reference, int $snapshotItemsCount, int $visibleTenantsCount, int $renderedRowsCount = 0, ): ?array { if (($reference['referenceState'] ?? null) !== 'ready') { return [ 'title' => 'No usable reference snapshot', 'body' => 'Capture a complete baseline snapshot before using the compare matrix.', ]; } if ((int) ($reference['assignedTenantCount'] ?? 0) === 0) { return [ 'title' => 'No assigned tenants', 'body' => 'Assign tenants to this baseline profile to build the visible compare set.', ]; } if ($visibleTenantsCount === 0) { return [ 'title' => 'No visible assigned tenants', 'body' => 'This baseline has assigned tenants, but none are visible in your current tenant scope.', ]; } if ($snapshotItemsCount === 0) { return [ 'title' => 'No baseline subjects', 'body' => 'The active reference snapshot completed without any baseline subjects to compare.', ]; } if ($renderedRowsCount === 0) { return [ 'title' => 'No rows match the current filters', 'body' => 'Adjust the policy type, state, or severity filters to broaden the matrix view.', ]; } return null; } /** * @param array $values * @return list> */ private function legendSpecs(BadgeDomain $domain, array $values): array { return array_map( static function (string $value) use ($domain): array { $spec = BadgeCatalog::spec($domain, $value); return [ 'value' => $value, 'label' => $spec->label, 'color' => $spec->color, 'icon' => $spec->icon, 'iconColor' => $spec->iconColor, ]; }, $values, ); } }