$filters * @return list> */ public function rows(ManagedEnvironment $environment, array $filters = []): array { $run = $this->resolveRun($environment, $this->intFilter($filters, 'operation_run_id')); if (! $run instanceof OperationRun) { return []; } $outcomes = $this->subjectOutcomes($run); if ($outcomes === []) { return []; } $activeBindings = $this->activeBindings($environment); $inventoryDescriptors = $this->inventoryDescriptors($environment); $rows = collect($outcomes) ->filter(fn (array $outcome): bool => $this->includeOutcome($outcome, $filters)) ->values() ->map(fn (array $outcome, int $index): array => $this->rowFromOutcome( outcome: $outcome, index: $index, run: $run, environment: $environment, activeBindings: $activeBindings, inventoryDescriptors: $inventoryDescriptors, )) ->filter(fn (array $row): bool => $this->matchesFilters($row, $filters)) ->sortBy([ fn (array $row): int => $this->readinessSortWeight((string) ($row['readiness_impact'] ?? '')), fn (array $row): int => $row['active_binding_id'] !== null ? 1 : 0, fn (array $row): string => (string) ($row['subject_label'] ?? ''), ]) ->values(); return $rows->all(); } /** * @param array $filters */ public function row(ManagedEnvironment $environment, string $rowId, array $filters = []): ?array { return collect($this->rows($environment, $filters)) ->first(fn (array $row): bool => (string) ($row['id'] ?? '') === $rowId); } /** * @return array{ * has_run: bool, * source_operation_run_id: int|null, * actionable_count: int, * visible_count: int, * by_actionability: array, * by_readiness_impact: array, * by_reason: array, * legacy_payload_only: bool * } */ public function summary(ManagedEnvironment $environment, ?int $operationRunId = null): array { $run = $this->resolveRun($environment, $operationRunId); $rows = $run instanceof OperationRun ? $this->rows($environment, ['operation_run_id' => (int) $run->getKey()]) : []; $outcomes = $run instanceof OperationRun ? $this->subjectOutcomes($run) : []; return [ 'has_run' => $run instanceof OperationRun, 'source_operation_run_id' => $run instanceof OperationRun ? (int) $run->getKey() : null, 'actionable_count' => count($rows), 'visible_count' => count($rows), 'by_actionability' => $this->countBy($rows, 'actionability'), 'by_readiness_impact' => $this->countBy($rows, 'readiness_impact'), 'by_reason' => $this->countBy($rows, 'reason'), 'legacy_payload_only' => $run instanceof OperationRun && $outcomes === [] && is_array(data_get($run->context, 'baseline_compare.evidence_gaps')), ]; } public function resolveRun(ManagedEnvironment $environment, ?int $operationRunId = null): ?OperationRun { $query = OperationRun::query() ->where('workspace_id', (int) $environment->workspace_id) ->where('managed_environment_id', (int) $environment->getKey()) ->whereIn('type', OperationCatalog::rawValuesForCanonical(OperationRunType::BaselineCompare->value)); if ($operationRunId !== null && $operationRunId > 0) { return $query->whereKey($operationRunId)->first(); } return $query ->latest('completed_at') ->latest('id') ->first(); } /** * @return array */ public function filterOptions(ManagedEnvironment $environment, ?int $operationRunId = null, string $key = 'all'): array { $rows = collect($this->rows($environment, array_filter([ 'operation_run_id' => $operationRunId, 'include_resolved' => true, ], static fn (mixed $value): bool => $value !== null))); $map = match ($key) { 'provider' => $rows->pluck('provider_label', 'provider_key'), 'subject_class' => $rows->pluck('subject_class_label', 'subject_class'), 'resource_type' => $rows->pluck('resource_type_label', 'subject_type_key'), 'actionability' => $rows->pluck('actionability_label', 'actionability'), 'readiness_impact' => $rows->pluck('readiness_label', 'readiness_impact'), 'reason' => $rows->pluck('reason_label', 'reason'), default => collect(), }; return $map ->filter(fn (mixed $label, mixed $value): bool => is_string($value) && $value !== '' && is_string($label) && $label !== '') ->unique() ->sort() ->all(); } /** * @return list> */ private function subjectOutcomes(OperationRun $run): array { $outcomes = data_get($run->context, 'baseline_compare.result_semantics.subject_outcomes'); if (! is_array($outcomes)) { return []; } return collect($outcomes) ->filter(fn (mixed $outcome): bool => is_array($outcome)) ->values() ->all(); } /** * @return EloquentCollection */ private function activeBindings(ManagedEnvironment $environment): EloquentCollection { return ProviderResourceBinding::query() ->where('workspace_id', (int) $environment->workspace_id) ->where('managed_environment_id', (int) $environment->getKey()) ->where('binding_status', ProviderResourceBindingStatus::Active->value) ->latest('decided_at') ->get(); } /** * @return Collection */ private function inventoryDescriptors(ManagedEnvironment $environment): Collection { return InventoryItem::query() ->where('workspace_id', (int) $environment->workspace_id) ->where('managed_environment_id', (int) $environment->getKey()) ->latest('last_seen_at') ->get() ->map(fn (InventoryItem $item): ?ProviderResourceDescriptor => $this->descriptorFromInventoryItem($item)) ->filter() ->values(); } /** * @param EloquentCollection $activeBindings * @param Collection $inventoryDescriptors * @return array */ private function rowFromOutcome( array $outcome, int $index, OperationRun $run, ManagedEnvironment $environment, EloquentCollection $activeBindings, Collection $inventoryDescriptors, ): array { $subject = is_array($outcome['subject'] ?? null) ? $outcome['subject'] : []; $proof = is_array($outcome['proof'] ?? null) ? $outcome['proof'] : []; $subjectTypeKey = $this->stringValue( $subject['subject_type_key'] ?? $subject['policy_type'] ?? $proof['policy_type'] ?? null, ) ?? 'unknown'; $subjectClass = $this->stringValue($subject['subject_class'] ?? null) ?? SubjectClass::PolicyBacked->value; $subjectDomain = $this->stringValue($subject['subject_domain'] ?? $subject['domain_key'] ?? null) ?? 'baseline'; $subjectKey = $this->stringValue($subject['canonical_subject_key'] ?? $subject['subject_key'] ?? null); $canonicalSubjectKey = $this->canonicalSubjectKey($subject); $displayLabel = $this->stringValue( $subject['display_label'] ?? $subject['operator_label'] ?? $subject['subject_key'] ?? $subject['external_subject_id'] ?? null, ); $providerDescriptor = $this->descriptorFromSubject($subject); $decisionIdentity = $providerDescriptor?->identity; $candidates = $this->candidateRows( subject: $subject, subjectTypeKey: $subjectTypeKey, subjectClass: $subjectClass, canonicalSubjectKey: $canonicalSubjectKey, outcomeDescriptors: $this->candidateDescriptorsFromOutcome($outcome), inventoryDescriptors: $inventoryDescriptors, ); $activeBinding = $this->activeBindingFor( activeBindings: $activeBindings, canonicalSubjectKey: $canonicalSubjectKey, decisionIdentity: $decisionIdentity, ); $providerKey = $decisionIdentity?->providerKey ?? ($candidates[0]['provider_key'] ?? null) ?? $this->stringValue($subject['provider_key'] ?? $proof['provider_key'] ?? null) ?? 'unknown'; $reason = $this->stringValue($outcome['reason'] ?? null) ?? 'unknown'; $actionability = $this->stringValue($outcome['actionability'] ?? null) ?? 'unknown'; $readinessImpact = $this->stringValue($outcome['readiness_impact'] ?? null) ?? 'unknown'; $rowId = $this->rowId($run, $index, $reason, $subjectTypeKey, $subjectKey, $canonicalSubjectKey); $sourceReferences = is_array($subject['source_references'] ?? null) ? $subject['source_references'] : []; return [ 'id' => $rowId, 'workspace_id' => (int) $environment->workspace_id, 'managed_environment_id' => (int) $environment->getKey(), 'source_operation_run_id' => (int) $run->getKey(), 'source_baseline_snapshot_id' => is_numeric(data_get($run->context, 'baseline_snapshot_id')) ? (int) data_get($run->context, 'baseline_snapshot_id') : null, 'subject_domain' => $subjectDomain, 'subject_class' => $subjectClass, 'subject_class_label' => $this->label($subjectClass), 'subject_type_key' => $subjectTypeKey, 'resource_type_label' => InventoryPolicyTypeMeta::baselineCompareLabel($subjectTypeKey) ?? InventoryPolicyTypeMeta::label($subjectTypeKey) ?? $this->label($subjectTypeKey), 'subject_key' => $subjectKey, 'canonical_subject_key' => $canonicalSubjectKey, 'subject_label' => $displayLabel ?: ($subjectKey ?: $this->label($subjectTypeKey)), 'provider_key' => (string) $providerKey, 'provider_label' => $this->label((string) $providerKey), 'reason' => $reason, 'reason_label' => $this->label($reason), 'actionability' => $actionability, 'actionability_label' => $this->actionabilityLabel($actionability), 'readiness_impact' => $readinessImpact, 'readiness_label' => $this->readinessLabel($readinessImpact), 'identity_status' => $this->stringValue($outcome['identity_status'] ?? null), 'comparison_status' => $this->stringValue($outcome['comparison_status'] ?? null), 'coverage_status' => $this->stringValue($outcome['coverage_status'] ?? null), 'trust_level' => $this->stringValue($outcome['trust_level'] ?? null), 'candidate_count' => count($candidates), 'candidates' => $candidates, 'has_candidates' => $candidates !== [], 'decision_identity' => $decisionIdentity?->toArray(), 'active_binding_id' => $activeBinding instanceof ProviderResourceBinding ? (int) $activeBinding->getKey() : null, 'active_binding_mode' => $activeBinding instanceof ProviderResourceBinding ? $this->enumValue($activeBinding->resolution_mode) : null, 'current_decision_label' => $activeBinding instanceof ProviderResourceBinding ? $this->label($this->enumValue($activeBinding->resolution_mode)) : 'None recorded', 'source_inventory_item_id' => is_numeric($sourceReferences['inventory_item_id'] ?? null) ? (int) $sourceReferences['inventory_item_id'] : null, 'source_policy_version_id' => is_numeric($sourceReferences['policy_version_id'] ?? null) ? (int) $sourceReferences['policy_version_id'] : null, 'last_seen' => $candidates[0]['last_seen_at'] ?? null, 'search_text' => Str::lower(implode(' ', array_filter([ $displayLabel, $subjectKey, $canonicalSubjectKey, $subjectTypeKey, $subjectClass, $providerKey, $reason, $actionability, $readinessImpact, $activeBinding?->display_label, ]))), ]; } /** * @return Collection */ private function candidateDescriptorsFromOutcome(array $outcome): Collection { return collect([ data_get($outcome, 'candidate_descriptors'), data_get($outcome, 'subject.candidate_descriptors'), data_get($outcome, 'proof.candidate_descriptors'), data_get($outcome, 'candidates'), data_get($outcome, 'subject.candidates'), data_get($outcome, 'proof.candidates'), ]) ->flatMap(function (mixed $payload): array { if (! is_array($payload)) { return []; } return array_is_list($payload) ? $payload : [$payload]; }) ->map(fn (mixed $payload): ?ProviderResourceDescriptor => is_array($payload) ? $this->descriptorFromPayload($payload) : null) ->filter() ->values(); } /** * @param Collection $outcomeDescriptors * @param Collection $inventoryDescriptors * @return list> */ private function candidateRows( array $subject, string $subjectTypeKey, string $subjectClass, ?string $canonicalSubjectKey, Collection $outcomeDescriptors, Collection $inventoryDescriptors, ): array { $subjectProviderKey = $this->stringValue(data_get($subject, 'provider_resource_descriptor.identity.provider_key')); $outcomeRows = $outcomeDescriptors ->filter(fn (ProviderResourceDescriptor $descriptor): bool => $this->descriptorMatchesCandidateScope( descriptor: $descriptor, subjectTypeKey: $subjectTypeKey, subjectClass: $subjectClass, canonicalSubjectKey: $canonicalSubjectKey, subjectProviderKey: $subjectProviderKey, requireCanonicalMatch: false, )) ->map(fn (ProviderResourceDescriptor $descriptor): array => $this->candidateRow($descriptor)) ->values(); $inventoryRows = $inventoryDescriptors ->filter(fn (ProviderResourceDescriptor $descriptor): bool => $this->descriptorMatchesCandidateScope( descriptor: $descriptor, subjectTypeKey: $subjectTypeKey, subjectClass: $subjectClass, canonicalSubjectKey: $canonicalSubjectKey, subjectProviderKey: $subjectProviderKey, requireCanonicalMatch: true, )) ->map(fn (ProviderResourceDescriptor $descriptor): array => $this->candidateRow($descriptor)) ->values(); return $outcomeRows ->merge($inventoryRows) ->unique('candidate_key') ->values() ->all(); } private function descriptorMatchesCandidateScope( ProviderResourceDescriptor $descriptor, string $subjectTypeKey, string $subjectClass, ?string $canonicalSubjectKey, ?string $subjectProviderKey, bool $requireCanonicalMatch, ): bool { if ($descriptor->subjectTypeKey !== $subjectTypeKey) { return false; } $descriptorClass = $descriptor->subjectClass instanceof SubjectClass ? $descriptor->subjectClass->value : (string) $descriptor->subjectClass; if ($subjectClass !== '' && $descriptorClass !== $subjectClass) { return false; } if ($subjectProviderKey !== null && $descriptor->identity->providerKey !== $subjectProviderKey) { return false; } $descriptorCanonicalKey = BaselineSubjectKey::forProviderResourceIdentity( $descriptor->subjectDomain, $descriptor->subjectClass, $descriptor->subjectTypeKey, $descriptor->identity, ); if ($canonicalSubjectKey !== null && $descriptorCanonicalKey === $canonicalSubjectKey) { return true; } return ! $requireCanonicalMatch; } /** * @return array */ private function candidateRow(ProviderResourceDescriptor $descriptor): array { $identity = $descriptor->identity; $sourceReferences = $descriptor->sourceReferences; return [ 'candidate_key' => $identity->fingerprint(), 'identity' => $identity->toArray(), 'display_label' => $descriptor->displayLabel ?: $this->label((string) ($identity->providerResourceType ?? 'resource')), 'provider_key' => $identity->providerKey, 'provider_label' => $this->label($identity->providerKey), 'provider_resource_type' => $identity->providerResourceType, 'identity_kind' => $identity->identityKind, 'stable_identity_preview' => $this->preview($identity->stableIdentityValue()), 'source_inventory_item_id' => is_numeric($sourceReferences['inventory_item_id'] ?? null) ? (int) $sourceReferences['inventory_item_id'] : null, 'source_policy_version_id' => is_numeric($sourceReferences['policy_version_id'] ?? null) ? (int) $sourceReferences['policy_version_id'] : null, 'last_seen_at' => $descriptor->lastSeenAt, ]; } private function descriptorFromSubject(array $subject): ?ProviderResourceDescriptor { $descriptorPayload = $subject['provider_resource_descriptor'] ?? null; return is_array($descriptorPayload) ? $this->descriptorFromPayload($descriptorPayload) : null; } /** * @param array $payload */ private function descriptorFromPayload(array $payload): ?ProviderResourceDescriptor { $descriptorPayload = $payload['provider_resource_descriptor'] ?? $payload['descriptor'] ?? $payload; if (! is_array($descriptorPayload)) { return null; } try { return ProviderResourceDescriptor::fromArray($descriptorPayload); } catch (InvalidArgumentException) { return null; } } private function descriptorFromInventoryItem(InventoryItem $inventoryItem): ?ProviderResourceDescriptor { $metaJsonb = is_array($inventoryItem->meta_jsonb) ? $inventoryItem->meta_jsonb : []; $descriptorPayload = $metaJsonb['provider_resource_descriptor'] ?? null; if (is_array($descriptorPayload)) { try { return ProviderResourceDescriptor::fromArray($descriptorPayload); } catch (InvalidArgumentException) { return null; } } $identity = $this->resourceIdentityFromMeta( metaJsonb: $metaJsonb, fallbackProviderKey: $this->stringValue($metaJsonb['provider_key'] ?? $metaJsonb['provider'] ?? null) ?? 'inventory', fallbackResourceType: $this->stringValue($metaJsonb['provider_resource_type'] ?? $metaJsonb['resource_type'] ?? null) ?? (string) $inventoryItem->policy_type, fallbackResourceId: $this->stringValue($inventoryItem->external_id), ); if (! $identity instanceof ResourceIdentity) { return null; } return ProviderResourceDescriptor::fromIdentity( identity: $identity, subjectDomain: $this->stringValue($metaJsonb['subject_domain'] ?? null) ?? 'baseline', subjectClass: $this->stringValue($metaJsonb['subject_class'] ?? null) ?? SubjectClass::PolicyBacked->value, subjectTypeKey: (string) $inventoryItem->policy_type, displayLabel: $this->stringValue($inventoryItem->display_name) ?? $this->stringValue($metaJsonb['display_name'] ?? null), sourceReferences: [ 'inventory_item_id' => (int) $inventoryItem->getKey(), 'external_id' => (string) $inventoryItem->external_id, ], fingerprint: $this->stringValue($metaJsonb['provider_resource_fingerprint'] ?? null) ?? $identity->fingerprint(), lastSeenAt: $inventoryItem->last_seen_at?->toIso8601String(), ); } /** * @param array $metaJsonb */ private function resourceIdentityFromMeta( array $metaJsonb, ?string $fallbackProviderKey = null, ?string $fallbackResourceType = null, ?string $fallbackResourceId = null, ): ?ResourceIdentity { $descriptorPayload = $metaJsonb['provider_resource_descriptor'] ?? null; $identityPayload = is_array($descriptorPayload) ? ($descriptorPayload['identity'] ?? null) : null; if (! is_array($identityPayload)) { $identityPayload = $metaJsonb['provider_resource_identity'] ?? null; } if (is_array($identityPayload)) { try { return ResourceIdentity::fromArray($identityPayload); } catch (InvalidArgumentException) { return null; } } $providerKey = $this->stringValue($metaJsonb['provider_key'] ?? $metaJsonb['provider'] ?? null) ?? $fallbackProviderKey; $resourceType = $this->stringValue($metaJsonb['provider_resource_type'] ?? $metaJsonb['resource_type'] ?? $metaJsonb['provider_object_type'] ?? null) ?? $fallbackResourceType; $resourceId = $this->stringValue($metaJsonb['provider_resource_id'] ?? $metaJsonb['resource_id'] ?? null) ?? $fallbackResourceId; $discriminator = $this->stringValue($metaJsonb['provider_resource_discriminator'] ?? $metaJsonb['canonical_discriminator'] ?? null); $identityKind = $this->stringValue($metaJsonb['provider_resource_identity_kind'] ?? $metaJsonb['identity_kind'] ?? null) ?? ResourceIdentity::ProviderResource; if ($providerKey === null || $resourceType === null) { return null; } try { return new ResourceIdentity( providerKey: $providerKey, identityKind: $identityKind, providerResourceType: $resourceType, providerResourceId: $identityKind === ResourceIdentity::ProviderResource ? $resourceId : null, canonicalDiscriminator: $identityKind === ResourceIdentity::ProviderResource ? null : $discriminator, ); } catch (InvalidArgumentException) { return null; } } /** * @param EloquentCollection $activeBindings */ private function activeBindingFor( EloquentCollection $activeBindings, ?string $canonicalSubjectKey, ?ResourceIdentity $decisionIdentity, ): ?ProviderResourceBinding { return $activeBindings->first(function (ProviderResourceBinding $binding) use ($canonicalSubjectKey, $decisionIdentity): bool { if ($canonicalSubjectKey !== null && (string) $binding->canonical_subject_key === $canonicalSubjectKey) { return true; } return $decisionIdentity instanceof ResourceIdentity && (string) $binding->provider_key === $decisionIdentity->providerKey && (string) $binding->provider_resource_fingerprint === $decisionIdentity->fingerprint(); }); } private function canonicalSubjectKey(array $subject): ?string { $candidate = $this->stringValue($subject['canonical_subject_key'] ?? $subject['subject_key'] ?? null); return BaselineSubjectKey::isProviderResourceCanonicalKey($candidate) ? $candidate : null; } /** * @param array $filters */ private function includeOutcome(array $outcome, array $filters): bool { if ((bool) ($filters['include_resolved'] ?? false)) { return true; } $actionability = $this->stringValue($outcome['actionability'] ?? null); return ! in_array($actionability, [ CompareResultActionability::None->value, CompareResultActionability::Accepted->value, CompareResultActionability::Excluded->value, ], true); } /** * @param array $filters */ private function matchesFilters(array $row, array $filters): bool { $stringFilters = [ 'provider' => 'provider_key', 'subject_class' => 'subject_class', 'resource_type' => 'subject_type_key', 'actionability' => 'actionability', 'readiness_impact' => 'readiness_impact', 'reason' => 'reason', ]; foreach ($stringFilters as $filterKey => $rowKey) { $filterValue = $this->stringFilter($filters, $filterKey); if ($filterValue !== null && (string) ($row[$rowKey] ?? '') !== $filterValue) { return false; } } $activeBinding = $this->stringFilter($filters, 'active_binding'); if ($activeBinding === 'yes' && $row['active_binding_id'] === null) { return false; } if ($activeBinding === 'no' && $row['active_binding_id'] !== null) { return false; } $candidates = $this->stringFilter($filters, 'candidates'); if ($candidates === 'yes' && ! (bool) ($row['has_candidates'] ?? false)) { return false; } if ($candidates === 'no' && (bool) ($row['has_candidates'] ?? false)) { return false; } return true; } /** * @param list> $rows * @return array */ private function countBy(array $rows, string $key): array { return collect($rows) ->countBy(fn (array $row): string => (string) ($row[$key] ?? 'unknown')) ->sortKeys() ->all(); } private function rowId(OperationRun $run, int $index, string $reason, string $subjectTypeKey, ?string $subjectKey, ?string $canonicalSubjectKey): string { return hash('sha256', implode('|', [ (string) $run->getKey(), (string) $index, $reason, $subjectTypeKey, (string) ($canonicalSubjectKey ?? $subjectKey ?? 'subject'), ])); } private function intFilter(array $filters, string $key): ?int { $value = $filters[$key] ?? null; return is_numeric($value) ? (int) $value : null; } private function stringFilter(array $filters, string $key): ?string { $value = $filters[$key] ?? null; $value = is_array($value) ? ($value['value'] ?? null) : $value; return $this->stringValue($value); } private function stringValue(mixed $value): ?string { if (! is_string($value) && ! is_numeric($value)) { return null; } $value = trim((string) $value); return $value !== '' ? $value : null; } private function enumValue(mixed $value): string { if ($value instanceof \BackedEnum) { return (string) $value->value; } return (string) $value; } private function label(string $value): string { $value = trim($value); return $value === '' ? 'Unknown' : Str::of($value)->replace(['_', '-'], ' ')->headline()->toString(); } private function actionabilityLabel(string $actionability): string { return match ($actionability) { CompareResultActionability::BindingRequired->value => 'Binding required', CompareResultActionability::OperatorActionRequired->value => 'Operator decision required', CompareResultActionability::ProviderDataRefreshRequired->value => 'Refresh provider data', CompareResultActionability::ImplementationGap->value => 'Implementation gap', CompareResultActionability::ScopeDecisionRequired->value => 'Scope decision required', default => $this->label($actionability), }; } private function readinessLabel(string $readinessImpact): string { return match ($readinessImpact) { 'customer_blocker' => 'Customer blocker', 'internal_blocker' => 'Internal blocker', 'customer_limitation' => 'Customer limitation', 'internal_limitation' => 'Internal limitation', 'no_impact' => 'No impact', default => $this->label($readinessImpact), }; } private function readinessSortWeight(string $readinessImpact): int { return match ($readinessImpact) { 'customer_blocker' => 0, 'internal_blocker' => 1, 'customer_limitation' => 2, 'internal_limitation' => 3, default => 4, }; } private function preview(?string $value): ?string { if ($value === null || trim($value) === '') { return null; } $value = trim($value); return Str::length($value) > 16 ? Str::substr($value, 0, 10).'...'.Str::substr($value, -4) : $value; } }