*/ public function deriveForEnvironment( ManagedEnvironment $tenant, ?OperationRun $latestCompareRun, int $driftCount, int $openDriftCount, ?CarbonInterface $measuredAt, bool $isStale, ): array { return $this->derive( latestCompareRun: $latestCompareRun, driftCount: $driftCount, openDriftCount: $openDriftCount, bindingDecisionCounts: $this->bindingDecisionCounts($tenant, $latestCompareRun), measuredAt: $measuredAt, isStale: $isStale, ); } /** * @param array $bindingDecisionCounts * @return array */ public function derive( ?OperationRun $latestCompareRun, int $driftCount, int $openDriftCount, array $bindingDecisionCounts = [], ?CarbonInterface $measuredAt = null, bool $isStale = false, ): array { $bindingDecisionCounts = $this->normalizeCounts($bindingDecisionCounts); $semantics = $this->structuredCompareSemantics($latestCompareRun); $counts = $this->semanticCounts($semantics, $driftCount, $bindingDecisionCounts); $publicationBlockers = []; $limitations = []; $readinessState = 'customer_ready'; $state = EvidenceCompletenessState::Complete; $nextAction = 'download_customer_safe_review_pack'; $proofState = $semantics === [] ? 'missing_structured_compare' : 'structured_compare'; if (! $latestCompareRun instanceof OperationRun) { if ($driftCount > 0) { $readinessState = 'drift_findings_present'; $proofState = 'drift_findings_only'; $nextAction = 'review_baseline_drift_findings'; } else { $state = EvidenceCompletenessState::Missing; $readinessState = 'baseline_compare_unproven'; $publicationBlockers[] = 'Baseline compare proof is missing; refresh evidence before presenting a no-drift claim.'; $nextAction = 'open_evidence_basis'; } } elseif ((string) $latestCompareRun->status !== OperationRunStatus::Completed->value) { $state = EvidenceCompletenessState::Missing; $readinessState = 'baseline_compare_not_completed'; $publicationBlockers[] = 'Baseline compare has not completed; rerun or wait for completion before publication.'; $nextAction = 'open_operation_proof'; } elseif ((string) $latestCompareRun->outcome === OperationRunOutcome::Failed->value || $counts['failed_subject_count'] > 0) { $state = EvidenceCompletenessState::Missing; $readinessState = 'baseline_compare_failed'; $publicationBlockers[] = 'Baseline compare failed; rerun or investigate before publication.'; $nextAction = 'open_operation_proof'; } elseif ($isStale) { $state = EvidenceCompletenessState::Stale; $readinessState = 'baseline_compare_stale'; $publicationBlockers[] = 'Baseline compare evidence is stale and must be refreshed before publication.'; $nextAction = 'open_evidence_basis'; } elseif ($semantics === []) { if ($driftCount > 0) { $readinessState = 'drift_findings_present'; $proofState = 'drift_findings_only'; $nextAction = 'review_baseline_drift_findings'; } else { $state = EvidenceCompletenessState::Missing; $readinessState = 'baseline_compare_unproven'; $publicationBlockers[] = 'Baseline compare did not produce structured readiness proof; refresh evidence before publication.'; $nextAction = 'open_evidence_basis'; } } else { [$publicationBlockers, $limitations, $nextAction] = $this->reasonsToReadinessActions($counts); if ($publicationBlockers !== []) { $state = $counts['missing_local_evidence_subject_count'] > 0 || $counts['failed_subject_count'] > 0 ? EvidenceCompletenessState::Missing : EvidenceCompletenessState::Partial; $readinessState = $this->blockingReadinessState($counts); } elseif ($limitations !== []) { $state = EvidenceCompletenessState::Partial; $readinessState = 'baseline_compare_limited'; } elseif ($counts['drift_subject_count'] > 0 || $driftCount > 0) { $readinessState = 'trusted_drift_detected'; $nextAction = 'review_baseline_drift_findings'; } } $limitationCodes = array_values(array_unique(array_map( static fn (array $limitation): string => (string) $limitation['code'], $limitations, ))); return [ 'version' => self::VERSION, 'state' => $state->value, 'readiness_state' => $readinessState, 'proof_state' => $proofState, 'customer_safe_claim' => $this->customerSafeClaim($state, $readinessState, $publicationBlockers, $limitations), 'publication_blockers' => array_values(array_unique($publicationBlockers)), 'limitations' => $limitations, 'limitation_codes' => $limitationCodes, 'next_action' => $nextAction, 'counts' => $counts, 'customer_safe_summary' => [ 'state' => $state->value, 'readiness_state' => $readinessState, 'verified_subject_count' => $counts['verified_subject_count'], 'drift_subject_count' => max($counts['drift_subject_count'], $driftCount), 'open_drift_count' => $openDriftCount, 'blocker_count' => count(array_unique($publicationBlockers)), 'limitation_count' => count($limitationCodes), 'excluded_subject_count' => $counts['excluded_subject_count'], ], 'internal_diagnostics' => [ 'latest_compare_run_id' => $latestCompareRun instanceof OperationRun ? (int) $latestCompareRun->getKey() : null, 'latest_compare_status' => $latestCompareRun instanceof OperationRun ? (string) $latestCompareRun->status : null, 'latest_compare_outcome' => $latestCompareRun instanceof OperationRun ? (string) $latestCompareRun->outcome : null, 'latest_compare_completed_at' => $latestCompareRun?->completed_at?->toIso8601String(), 'measured_at' => $measuredAt?->toIso8601String(), 'has_structured_compare_semantics' => $semantics !== [], 'run_outcome' => is_string($semantics['run_outcome'] ?? null) ? $semantics['run_outcome'] : null, 'operation_outcome' => is_string($semantics['operation_outcome'] ?? null) ? $semantics['operation_outcome'] : null, 'binding_decision_counts' => $bindingDecisionCounts, 'semantic_counts' => is_array($semantics['counts'] ?? null) ? $semantics['counts'] : [], ], ]; } /** * @return array */ private function bindingDecisionCounts(ManagedEnvironment $tenant, ?OperationRun $latestCompareRun): array { $counts = ProviderResourceBinding::query() ->active() ->where('managed_environment_id', (int) $tenant->getKey()) ->where('workspace_id', (int) $tenant->workspace_id) ->selectRaw('resolution_mode, count(*) as aggregate') ->groupBy('resolution_mode') ->pluck('aggregate', 'resolution_mode') ->map(static fn (mixed $count): int => max(0, (int) $count)) ->all(); $compareAt = $latestCompareRun?->completed_at ?? $latestCompareRun?->updated_at ?? $latestCompareRun?->created_at; if ($compareAt !== null) { $counts['revoked_after_latest_compare'] = ProviderResourceBinding::query() ->where('managed_environment_id', (int) $tenant->getKey()) ->where('workspace_id', (int) $tenant->workspace_id) ->where('binding_status', ProviderResourceBindingStatus::Revoked->value) ->where('ended_at', '>', $compareAt) ->count(); } return $counts; } /** * @return array */ private function structuredCompareSemantics(?OperationRun $operationRun): array { if (! $operationRun instanceof OperationRun) { return []; } $context = is_array($operationRun->context) ? $operationRun->context : []; $semantics = data_get($context, 'baseline_compare.result_semantics'); if (! is_array($semantics)) { return []; } $version = $semantics['version'] ?? null; $counts = $semantics['counts'] ?? null; if (! is_string($version) || ! is_array($counts)) { return []; } return $semantics; } /** * @param array $semantics * @return array */ private function semanticCounts(array $semantics, int $driftCount, array $bindingDecisionCounts): array { $byReason = $this->normalizeCounts(data_get($semantics, 'counts.by_reason', [])); $byReadiness = $this->normalizeCounts(data_get($semantics, 'counts.by_readiness_impact', [])); $identityBlockerCount = $this->sumReasons($byReason, [ CompareResultReason::IdentityRequired, CompareResultReason::UnresolvedDuplicateCandidates, CompareResultReason::UnresolvedLowTrustMatch, CompareResultReason::UnresolvedAmbiguousIdentity, ]); $foundationLimitationCount = $this->sumReasons($byReason, [ CompareResultReason::FoundationInventoryOnly, CompareResultReason::FoundationIdentityOnly, CompareResultReason::FoundationCanonicalOnly, ]); $unsupportedCount = $this->sumReasons($byReason, [ CompareResultReason::UnsupportedResourceClass, CompareResultReason::CompareNotSupported, ]); $bindingVerifiedCount = $this->sumResolutionModes($bindingDecisionCounts, [ ProviderResourceResolutionMode::ExactProviderIdentity, ProviderResourceResolutionMode::CanonicalBuiltin, ProviderResourceResolutionMode::CanonicalVirtualTarget, ProviderResourceResolutionMode::ManualBinding, ]); $bindingAcceptedLimitationCount = (int) ($bindingDecisionCounts[ProviderResourceResolutionMode::AcceptedLimitation->value] ?? 0); $bindingExcludedCount = (int) ($bindingDecisionCounts[ProviderResourceResolutionMode::ExcludedNonGoverned->value] ?? 0); $bindingUnsupportedCount = (int) ($bindingDecisionCounts[ProviderResourceResolutionMode::UnsupportedCoverage->value] ?? 0); $bindingMissingExpectedCount = (int) ($bindingDecisionCounts[ProviderResourceResolutionMode::MissingExpected->value] ?? 0); return [ 'verified_subject_count' => $this->sumReasons($byReason, [ CompareResultReason::VerifiedNoDrift, CompareResultReason::ResolvedActiveBinding, CompareResultReason::ResolvedCanonicalIdentity, CompareResultReason::ResolvedProviderIdentity, ]) + $bindingVerifiedCount, 'drift_subject_count' => max(0, (int) ($byReason[CompareResultReason::VerifiedDriftDetected->value] ?? 0), $driftCount), 'identity_blocker_subject_count' => $identityBlockerCount, 'missing_local_evidence_subject_count' => (int) ($byReason[CompareResultReason::MissingLocalEvidence->value] ?? 0), 'missing_provider_resource_subject_count' => max((int) ($byReason[CompareResultReason::MissingProviderResource->value] ?? 0), $bindingMissingExpectedCount), 'unsupported_subject_count' => max($unsupportedCount, $bindingUnsupportedCount), 'foundation_limited_subject_count' => $foundationLimitationCount, 'accepted_limitation_subject_count' => max((int) ($byReason[CompareResultReason::AcceptedLimitation->value] ?? 0), $bindingAcceptedLimitationCount), 'excluded_subject_count' => max((int) ($byReason[CompareResultReason::ExcludedNonGoverned->value] ?? 0), $bindingExcludedCount), 'failed_subject_count' => (int) ($byReason[CompareResultReason::CompareFailed->value] ?? 0), 'customer_blocker_subject_count' => (int) ($byReadiness[CompareResultReadinessImpact::CustomerBlocker->value] ?? 0), 'internal_blocker_subject_count' => (int) ($byReadiness[CompareResultReadinessImpact::InternalBlocker->value] ?? 0), 'customer_limitation_subject_count' => (int) ($byReadiness[CompareResultReadinessImpact::CustomerLimitation->value] ?? 0), 'internal_limitation_subject_count' => (int) ($byReadiness[CompareResultReadinessImpact::InternalLimitation->value] ?? 0), 'revoked_binding_after_compare_count' => (int) ($bindingDecisionCounts['revoked_after_latest_compare'] ?? 0), ]; } /** * @param array $counts * @return array{0:list,1:list,2:string} */ private function reasonsToReadinessActions(array $counts): array { $blockers = []; $limitations = []; $nextAction = 'download_customer_safe_review_pack'; if ($counts['identity_blocker_subject_count'] > 0) { $blockers[] = 'Baseline subject identity must be resolved before customer-ready publication.'; $nextAction = 'open_baseline_subject_resolution'; } if ($counts['missing_local_evidence_subject_count'] > 0) { $blockers[] = 'Baseline local evidence is missing and must be refreshed before publication.'; $nextAction = 'open_evidence_basis'; } if ($counts['missing_provider_resource_subject_count'] > 0) { $blockers[] = 'Baseline provider resources are missing and need operator review before publication.'; $nextAction = 'open_baseline_subject_resolution'; } if ($counts['unsupported_subject_count'] > 0) { $blockers[] = 'Required baseline coverage is unsupported and must be accepted or resolved before publication.'; $nextAction = 'review_output_limitations'; } if ($counts['failed_subject_count'] > 0 || $counts['internal_blocker_subject_count'] > 0) { $blockers[] = 'Baseline compare contains failed subjects and must be rerun or investigated before publication.'; $nextAction = 'open_operation_proof'; } if ($counts['revoked_binding_after_compare_count'] > 0) { $blockers[] = 'Baseline subject decisions changed after the latest compare; refresh evidence before publication.'; $nextAction = 'open_evidence_basis'; } if ($counts['foundation_limited_subject_count'] > 0) { $limitations[] = [ 'code' => 'baseline_foundation_limitations', 'summary' => 'Some baseline subjects are supported only by inventory, identity, or canonical foundation evidence.', ]; } if ($counts['accepted_limitation_subject_count'] > 0) { $limitations[] = [ 'code' => 'baseline_accepted_limitations', 'summary' => 'Accepted baseline limitations qualify the customer-ready claim.', ]; } if ($counts['excluded_subject_count'] > 0) { $limitations[] = [ 'code' => 'baseline_exclusions_present', 'summary' => 'Excluded non-governed baseline subjects are outside the governed no-drift claim.', ]; } if ($blockers === [] && $limitations !== []) { $nextAction = 'review_output_limitations'; } return [$blockers, $limitations, $nextAction]; } /** * @param array $counts */ private function blockingReadinessState(array $counts): string { if ($counts['missing_local_evidence_subject_count'] > 0) { return 'baseline_local_evidence_missing'; } if ($counts['identity_blocker_subject_count'] > 0) { return 'baseline_identity_unresolved'; } if ($counts['missing_provider_resource_subject_count'] > 0) { return 'baseline_provider_resource_missing'; } if ($counts['unsupported_subject_count'] > 0) { return 'baseline_required_coverage_unsupported'; } return 'baseline_compare_blocked'; } /** * @param array $counts * @param list $reasons */ private function sumReasons(array $counts, array $reasons): int { return collect($reasons) ->sum(static fn (CompareResultReason $reason): int => (int) ($counts[$reason->value] ?? 0)); } /** * @param array $counts * @param list $modes */ private function sumResolutionModes(array $counts, array $modes): int { return collect($modes) ->sum(static fn (ProviderResourceResolutionMode $mode): int => (int) ($counts[$mode->value] ?? 0)); } /** * @return array */ private function normalizeCounts(mixed $counts): array { if (! is_array($counts)) { return []; } return collect($counts) ->filter(static fn (mixed $count, mixed $key): bool => is_string($key) && $key !== '') ->map(static fn (mixed $count): int => max(0, (int) $count)) ->all(); } /** * @param list $publicationBlockers * @param list $limitations */ private function customerSafeClaim( EvidenceCompletenessState $state, string $readinessState, array $publicationBlockers, array $limitations, ): string { if ($publicationBlockers !== [] || in_array($state, [EvidenceCompletenessState::Missing, EvidenceCompletenessState::Stale], true)) { return 'not_customer_ready'; } if ($limitations !== []) { return 'customer_ready_with_disclosed_limitations'; } return match ($readinessState) { 'trusted_drift_detected', 'drift_findings_present' => 'customer_ready_with_findings', default => 'customer_ready', }; } }