$baselineSubjects * @param list $currentDescriptors * @return array{ * outcomes: list, * diagnostics: array * } */ public function matchAll(int $workspaceId, int $managedEnvironmentId, array $baselineSubjects, array $currentDescriptors): array { $currentIndex = $this->indexCurrentDescriptors($currentDescriptors); $bindingIndex = $this->activeBindingsByCanonicalKey($workspaceId, $managedEnvironmentId, $baselineSubjects); $outcomes = []; foreach ($baselineSubjects as $subject) { $outcomes[] = $this->matchOne($subject, $currentIndex, $bindingIndex); } return [ 'outcomes' => $outcomes, 'diagnostics' => $this->diagnostics($outcomes, $bindingIndex), ]; } /** * @param array{ * by_canonical_key: array>, * by_identity_key: array> * } $currentIndex * @param array> $bindingIndex */ private function matchOne(BaselineSubjectDescriptor $subject, array $currentIndex, array $bindingIndex): MatchingOutcome { $canonicalKey = $this->subjectCanonicalKey($subject); if ($canonicalKey !== null && isset($bindingIndex[$canonicalKey])) { $bindings = $bindingIndex[$canonicalKey]; if (count($bindings) !== 1) { return MatchingOutcome::ambiguous($subject, [ 'match_stage' => 'active_binding', 'canonical_subject_key' => $canonicalKey, 'binding_count' => count($bindings), ]); } return $this->matchBinding($subject, $bindings[0], $currentIndex); } if ($canonicalKey !== null) { $byCanonical = $currentIndex['by_canonical_key'][$canonicalKey] ?? []; if (count($byCanonical) === 1) { return MatchingOutcome::resolved( subject: $subject, matchedDescriptor: $byCanonical[0], matchedSubjectKey: $canonicalKey, reasonCode: 'canonical_subject_key', trust: 'high', proof: [ 'match_stage' => 'canonical_subject_key', 'canonical_subject_key' => $canonicalKey, ], ); } if (count($byCanonical) > 1) { return MatchingOutcome::ambiguous($subject, [ 'match_stage' => 'canonical_subject_key', 'canonical_subject_key' => $canonicalKey, 'candidate_count' => count($byCanonical), ]); } } if ($subject->providerResourceDescriptor instanceof ProviderResourceDescriptor) { $byCanonical = $this->currentByCanonicalSubject($subject->providerResourceDescriptor, $currentIndex); if (count($byCanonical) === 1) { return MatchingOutcome::resolved( subject: $subject, matchedDescriptor: $byCanonical[0], matchedSubjectKey: $canonicalKey ?? $this->currentCanonicalKey($byCanonical[0]) ?? $subject->comparisonSubjectKey(), reasonCode: 'provider_identity', trust: 'high', proof: [ 'match_stage' => 'provider_identity', 'canonical_subject_key' => $canonicalKey, ], ); } if (count($byCanonical) > 1) { return MatchingOutcome::ambiguous($subject, [ 'match_stage' => 'provider_identity', 'canonical_subject_key' => $canonicalKey, 'candidate_count' => count($byCanonical), ]); } } $coverageOutcome = $this->foundationCoverageOutcome($subject); if ($coverageOutcome instanceof MatchingOutcome) { return $coverageOutcome; } if ($canonicalKey !== null || $subject->providerResourceDescriptor instanceof ProviderResourceDescriptor) { return MatchingOutcome::missingLocalEvidence($subject, [ 'match_stage' => $canonicalKey !== null ? 'canonical_subject_key' : 'provider_identity', 'canonical_subject_key' => $canonicalKey, ]); } return MatchingOutcome::unresolvedIdentity($subject, [ 'match_stage' => 'identity_required', ]); } private function foundationCoverageOutcome(BaselineSubjectDescriptor $subject): ?MatchingOutcome { $coverage = $this->foundationCoverageResolver->coverageFor( $subject->subjectTypeKey, $subject->providerResourceDescriptor?->identity, ); $proof = [ 'match_stage' => 'foundation_coverage', 'coverage' => $coverage['coverage'], 'support_mode' => $coverage['support_mode'], 'source_model_expected' => $coverage['source_model_expected'], 'identity_kind' => $coverage['identity_kind'], ]; return match ($coverage['coverage']) { 'unsupported' => MatchingOutcome::unsupported($subject, $proof + [ 'reason_code' => $coverage['reason_code'] ?? 'unsupported_subject', ]), 'inventory_only' => MatchingOutcome::limited( subject: $subject, reasonCode: $coverage['reason_code'] ?? 'foundation_not_policy_backed', proof: $proof, ), 'identity_only', 'canonical_only' => MatchingOutcome::limited( subject: $subject, reasonCode: $coverage['reason_code'] ?? 'accepted_limitation', proof: $proof, ), default => null, }; } /** * @param array{ * by_canonical_key: array>, * by_identity_key: array> * } $currentIndex */ private function matchBinding(BaselineSubjectDescriptor $subject, ProviderResourceBinding $binding, array $currentIndex): MatchingOutcome { $proof = [ 'match_stage' => 'active_binding', 'provider_resource_binding_id' => (int) $binding->getKey(), 'provider_key' => (string) $binding->provider_key, 'canonical_subject_key' => (string) $binding->canonical_subject_key, 'resolution_mode' => $this->resolutionModeValue($binding), 'binding_status' => $this->bindingStatusValue($binding), ]; return match ($this->resolutionModeValue($binding)) { ProviderResourceResolutionMode::ExcludedNonGoverned->value => MatchingOutcome::excluded($subject, $proof), ProviderResourceResolutionMode::AcceptedLimitation->value => MatchingOutcome::limited($subject, 'accepted_limitation', $proof), ProviderResourceResolutionMode::UnsupportedCoverage->value => MatchingOutcome::unsupported($subject, $proof), ProviderResourceResolutionMode::MissingExpected->value => MatchingOutcome::missingProviderResource($subject, $proof), default => $this->matchActiveBindingToCurrentDescriptor($subject, $binding, $currentIndex, $proof), }; } /** * @param array{ * by_canonical_key: array>, * by_identity_key: array> * } $currentIndex * @param array $proof */ private function matchActiveBindingToCurrentDescriptor(BaselineSubjectDescriptor $subject, ProviderResourceBinding $binding, array $currentIndex, array $proof): MatchingOutcome { $candidates = $currentIndex['by_canonical_key'][(string) $binding->canonical_subject_key] ?? []; if ($candidates === []) { $identityKey = $this->bindingIdentityKey($binding); $candidates = $identityKey !== null ? ($currentIndex['by_identity_key'][$identityKey] ?? []) : []; } if (count($candidates) === 1) { return MatchingOutcome::resolved( subject: $subject, matchedDescriptor: $candidates[0], matchedSubjectKey: (string) $binding->canonical_subject_key, reasonCode: 'active_provider_resource_binding', trust: 'authoritative', proof: $proof, ); } if (count($candidates) > 1) { return MatchingOutcome::ambiguous($subject, $proof + [ 'candidate_count' => count($candidates), ]); } return MatchingOutcome::missingLocalEvidence($subject, $proof); } /** * @param list $currentDescriptors * @return array{ * by_canonical_key: array>, * by_identity_key: array> * } */ private function indexCurrentDescriptors(array $currentDescriptors): array { $index = [ 'by_canonical_key' => [], 'by_identity_key' => [], ]; foreach ($currentDescriptors as $descriptor) { if (! $descriptor instanceof ProviderResourceDescriptor) { continue; } $canonicalKey = $this->currentCanonicalKey($descriptor); if ($canonicalKey !== null) { $index['by_canonical_key'][$canonicalKey][] = $descriptor; } $identityKey = $this->descriptorIdentityKey($descriptor); if ($identityKey !== null) { $index['by_identity_key'][$identityKey][] = $descriptor; } } return $index; } /** * @param list $baselineSubjects * @return array> */ private function activeBindingsByCanonicalKey(int $workspaceId, int $managedEnvironmentId, array $baselineSubjects): array { $keys = []; foreach ($baselineSubjects as $subject) { if (! $subject instanceof BaselineSubjectDescriptor) { continue; } $canonicalKey = $this->subjectCanonicalKey($subject); if ($canonicalKey !== null) { $keys[$canonicalKey] = true; } } if ($keys === []) { return []; } $bindings = ProviderResourceBinding::query() ->where('workspace_id', $workspaceId) ->where('managed_environment_id', $managedEnvironmentId) ->whereIn('canonical_subject_key', array_keys($keys)) ->where('binding_status', ProviderResourceBindingStatus::Active->value) ->orderBy('id') ->get(); $index = []; foreach ($bindings as $binding) { if (! $binding instanceof ProviderResourceBinding) { continue; } $index[(string) $binding->canonical_subject_key][] = $binding; } return $index; } private function subjectCanonicalKey(BaselineSubjectDescriptor $subject): ?string { if (BaselineSubjectKey::isProviderResourceCanonicalKey($subject->canonicalSubjectKey)) { return $subject->canonicalSubjectKey; } if (! $subject->providerResourceDescriptor instanceof ProviderResourceDescriptor) { return null; } return BaselineSubjectKey::forProviderResourceIdentity( subjectDomain: $subject->providerResourceDescriptor->subjectDomain, subjectClass: $subject->providerResourceDescriptor->subjectClass, subjectTypeKey: $subject->providerResourceDescriptor->subjectTypeKey, identity: $subject->providerResourceDescriptor->identity, ); } private function currentCanonicalKey(ProviderResourceDescriptor $descriptor): ?string { return BaselineSubjectKey::forProviderResourceIdentity( subjectDomain: $descriptor->subjectDomain, subjectClass: $descriptor->subjectClass, subjectTypeKey: $descriptor->subjectTypeKey, identity: $descriptor->identity, ); } /** * @param array{ * by_canonical_key: array> * } $currentIndex * @return list */ private function currentByCanonicalSubject(ProviderResourceDescriptor $descriptor, array $currentIndex): array { $canonicalKey = $this->currentCanonicalKey($descriptor); return $canonicalKey !== null ? ($currentIndex['by_canonical_key'][$canonicalKey] ?? []) : []; } private function descriptorIdentityKey(ProviderResourceDescriptor $descriptor): ?string { return $this->identityKey($descriptor->identity); } private function bindingIdentityKey(ProviderResourceBinding $binding): ?string { $identityKind = is_string($binding->provider_resource_identity_kind) ? trim($binding->provider_resource_identity_kind) : ''; $providerKey = is_string($binding->provider_key) ? trim($binding->provider_key) : ''; $resourceType = is_string($binding->provider_resource_type) ? trim($binding->provider_resource_type) : ''; $stableIdentity = $identityKind === ResourceIdentity::ProviderResource ? (is_string($binding->provider_resource_id) ? trim($binding->provider_resource_id) : '') : (is_string($binding->provider_resource_discriminator) ? trim($binding->provider_resource_discriminator) : ''); if ($providerKey === '' || $resourceType === '' || $identityKind === '' || $stableIdentity === '') { return null; } return implode('|', [$providerKey, $resourceType, $identityKind, $stableIdentity]); } private function identityKey(ResourceIdentity $identity): ?string { $stableIdentity = $identity->stableIdentityValue(); if ($identity->providerResourceType === null || $stableIdentity === null) { return null; } return implode('|', [ $identity->providerKey, $identity->providerResourceType, $identity->identityKind, $stableIdentity, ]); } private function resolutionModeValue(ProviderResourceBinding $binding): string { return $binding->resolution_mode instanceof ProviderResourceResolutionMode ? $binding->resolution_mode->value : (string) $binding->resolution_mode; } private function bindingStatusValue(ProviderResourceBinding $binding): string { return $binding->binding_status instanceof ProviderResourceBindingStatus ? $binding->binding_status->value : (string) $binding->binding_status; } /** * @param list $outcomes * @param array> $bindingIndex * @return array */ private function diagnostics(array $outcomes, array $bindingIndex): array { $byStatus = []; $byReason = []; $activeBindingsConsidered = 0; foreach ($bindingIndex as $bindings) { $activeBindingsConsidered += count($bindings); } foreach ($outcomes as $outcome) { if (! $outcome instanceof MatchingOutcome) { continue; } $byStatus[$outcome->status] = ($byStatus[$outcome->status] ?? 0) + 1; $byReason[$outcome->reasonCode] = ($byReason[$outcome->reasonCode] ?? 0) + 1; } ksort($byStatus); ksort($byReason); return [ 'subjects_total' => count($outcomes), 'active_bindings_considered' => $activeBindingsConsidered, 'by_status' => $byStatus, 'by_reason' => $byReason, ]; } }