type !== 'environment.review.compose') { return ReconciliationResult::unsupported( reasonCode: LifecycleReconciliationReason::AdapterOutOfSync->value, reasonMessage: 'This adapter only supports environment review composition runs.', evidence: [ 'adapter' => self::ADAPTER, 'type' => (string) $run->type, ], ); } $context = is_array($run->context) ? $run->context : []; $workspaceId = (int) ($run->workspace_id ?? $context['workspace_id'] ?? 0); $tenantId = (int) ($run->managed_environment_id ?? $context['managed_environment_id'] ?? 0); $fingerprint = is_string($context['review_fingerprint'] ?? null) ? trim((string) $context['review_fingerprint']) : ''; $explicitReviewId = is_numeric($context['review_id'] ?? null) ? (int) $context['review_id'] : null; $evidence = [ 'adapter' => self::ADAPTER, 'operation_run_id' => (int) $run->getKey(), 'workspace_id' => $workspaceId, 'managed_environment_id' => $tenantId, 'fingerprint' => $fingerprint, 'explicit_review_id' => $explicitReviewId, ]; if ($workspaceId <= 0 || $tenantId <= 0 || $fingerprint === '') { return ReconciliationResult::attentionRequired( reasonCode: LifecycleReconciliationReason::AdapterOutOfSync->value, reasonMessage: 'The run no longer carries enough review scope to reconcile it safely.', evidence: $evidence, ); } $candidates = EnvironmentReview::query() ->forWorkspace($workspaceId) ->forTenant($tenantId) ->where('fingerprint', $fingerprint) ->orderByDesc('id') ->get(); $evidence['considered_review_ids'] = $candidates->modelKeys(); $evidence['considered_reviews'] = $candidates ->map(fn (EnvironmentReview $review): array => $this->reviewReference($review)) ->values() ->all(); $explicitReview = $explicitReviewId !== null ? $candidates->firstWhere('id', $explicitReviewId) : null; if ($explicitReviewId !== null && ! $explicitReview instanceof EnvironmentReview) { return ReconciliationResult::attentionRequired( reasonCode: LifecycleReconciliationReason::AdapterOutOfSync->value, reasonMessage: 'The originally queued review no longer matches the current review truth safely.', evidence: $evidence + ['explicit_review_missing' => true], ); } if ($explicitReview instanceof EnvironmentReview) { $lineageDecision = $this->evaluateExplicitReviewLineage($explicitReview, $candidates, $evidence); if ($lineageDecision instanceof ReconciliationResult) { return $lineageDecision; } } $usable = $candidates->filter(fn (EnvironmentReview $review): bool => $this->isUsableReview($review))->values(); if ($usable->count() === 1) { /** @var EnvironmentReview $review */ $review = $usable->first(); return ReconciliationResult::reconciledSucceeded( reasonCode: LifecycleReconciliationReason::AdapterOutOfSync->value, reasonMessage: $review->status === EnvironmentReviewStatus::Published->value ? 'A matching published review was already available for this run.' : 'A matching ready review was already available for this run.', evidence: $evidence + ['chosen_review_id' => (int) $review->getKey()], related: $this->relatedReviewMetadata($review), summaryCounts: $this->summaryCounts($review), ); } if ($usable->count() > 1) { return ReconciliationResult::attentionRequired( reasonCode: LifecycleReconciliationReason::AdapterOutOfSync->value, reasonMessage: 'Multiple matching reviews are available, so this run needs manual review.', evidence: $evidence + ['ambiguous_review_ids' => $usable->modelKeys()], ); } if ($candidates->isEmpty()) { return ReconciliationResult::notReconciled( reasonCode: LifecycleReconciliationReason::AdapterOutOfSync->value, reasonMessage: 'No matching review truth is available yet for this run.', evidence: $evidence, ); } if ($candidates->contains(fn (EnvironmentReview $review): bool => $this->isBlockingState($review))) { $blockingReview = $candidates->first(fn (EnvironmentReview $review): bool => $this->isBlockingState($review)); if ($blockingReview instanceof EnvironmentReview && (int) ($blockingReview->operation_run_id ?? 0) === (int) $run->getKey()) { return ReconciliationResult::notReconciled( reasonCode: LifecycleReconciliationReason::AdapterOutOfSync->value, reasonMessage: 'The queued review is still composing and does not prove final review truth yet.', evidence: $evidence + [ 'chosen_review_id' => (int) $blockingReview->getKey(), ], related: $this->relatedReviewMetadata($blockingReview), ); } return ReconciliationResult::blocked( reasonCode: LifecycleReconciliationReason::AdapterOutOfSync->value, reasonMessage: 'A matching review exists, but it is not ready yet.', evidence: $evidence + [ 'chosen_review_id' => $blockingReview instanceof EnvironmentReview ? (int) $blockingReview->getKey() : null, ], related: $blockingReview instanceof EnvironmentReview ? $this->relatedReviewMetadata($blockingReview) : [], summaryCounts: $blockingReview instanceof EnvironmentReview ? $this->summaryCounts($blockingReview) : [], ); } return ReconciliationResult::attentionRequired( reasonCode: LifecycleReconciliationReason::AdapterOutOfSync->value, reasonMessage: 'Review lineage needs operator confirmation before this run can be trusted.', evidence: $evidence, ); } /** * @param Collection $candidates * @param array $evidence */ private function evaluateExplicitReviewLineage( EnvironmentReview $review, Collection $candidates, array $evidence, ): ?ReconciliationResult { if ($this->isUsableReview($review)) { return ReconciliationResult::reconciledSucceeded( reasonCode: LifecycleReconciliationReason::AdapterOutOfSync->value, reasonMessage: $review->status === EnvironmentReviewStatus::Published->value ? 'The queued review was already published before the run finished updating.' : 'The queued review was already ready before the run finished updating.', evidence: $evidence + ['chosen_review_id' => (int) $review->getKey()], related: $this->relatedReviewMetadata($review), summaryCounts: $this->summaryCounts($review), ); } $successorId = is_numeric($review->superseded_by_review_id) ? (int) $review->superseded_by_review_id : null; if ($successorId === null) { if ($this->isBlockingState($review)) { if ($candidates->contains(fn (EnvironmentReview $candidate): bool => $this->isUsableReview($candidate))) { return null; } if ((int) ($review->operation_run_id ?? 0) === (int) ($evidence['operation_run_id'] ?? 0)) { return ReconciliationResult::notReconciled( reasonCode: LifecycleReconciliationReason::AdapterOutOfSync->value, reasonMessage: 'The queued review is still composing and does not prove final review truth yet.', evidence: $evidence + ['chosen_review_id' => (int) $review->getKey()], related: $this->relatedReviewMetadata($review), ); } return ReconciliationResult::blocked( reasonCode: LifecycleReconciliationReason::AdapterOutOfSync->value, reasonMessage: 'The queued review still exists, but it is not ready yet.', evidence: $evidence + ['chosen_review_id' => (int) $review->getKey()], related: $this->relatedReviewMetadata($review), summaryCounts: $this->summaryCounts($review), ); } if ($review->status === EnvironmentReviewStatus::Superseded->value) { return ReconciliationResult::attentionRequired( reasonCode: LifecycleReconciliationReason::AdapterOutOfSync->value, reasonMessage: 'This run points at superseded review lineage that cannot be resolved safely.', evidence: $evidence + ['lineage_review_id' => (int) $review->getKey()], related: $this->relatedReviewMetadata($review), ); } return null; } /** @var EnvironmentReview|null $successor */ $successor = $candidates->firstWhere('id', $successorId); if (! $successor instanceof EnvironmentReview) { return ReconciliationResult::attentionRequired( reasonCode: LifecycleReconciliationReason::AdapterOutOfSync->value, reasonMessage: 'A successor review was recorded, but the matching review truth could not be resolved safely.', evidence: $evidence + [ 'lineage_review_id' => (int) $review->getKey(), 'lineage_successor_review_id' => $successorId, ], related: $this->relatedReviewMetadata($review), ); } if ($this->isUsableReview($successor)) { return ReconciliationResult::reconciledSucceeded( reasonCode: LifecycleReconciliationReason::AdapterOutOfSync->value, reasonMessage: 'A ready successor review was already available for this run.', evidence: $evidence + [ 'lineage_review_id' => (int) $review->getKey(), 'chosen_review_id' => (int) $successor->getKey(), ], related: $this->relatedReviewMetadata($successor) + [ 'lineage_review_id' => (int) $review->getKey(), ], summaryCounts: $this->summaryCounts($successor), ); } if ($this->isBlockingState($successor)) { return ReconciliationResult::blocked( reasonCode: LifecycleReconciliationReason::AdapterOutOfSync->value, reasonMessage: 'A successor review exists, but it is not ready yet.', evidence: $evidence + [ 'lineage_review_id' => (int) $review->getKey(), 'chosen_review_id' => (int) $successor->getKey(), ], related: $this->relatedReviewMetadata($successor) + [ 'lineage_review_id' => (int) $review->getKey(), ], summaryCounts: $this->summaryCounts($successor), ); } return ReconciliationResult::attentionRequired( reasonCode: LifecycleReconciliationReason::AdapterOutOfSync->value, reasonMessage: 'The successor review needs operator review before this run can be trusted.', evidence: $evidence + [ 'lineage_review_id' => (int) $review->getKey(), 'chosen_review_id' => (int) $successor->getKey(), ], related: $this->relatedReviewMetadata($successor) + [ 'lineage_review_id' => (int) $review->getKey(), ], ); } private function isUsableReview(EnvironmentReview $review): bool { return in_array((string) $review->status, [ EnvironmentReviewStatus::Ready->value, EnvironmentReviewStatus::Published->value, ], true); } private function isBlockingState(EnvironmentReview $review): bool { return in_array((string) $review->status, [ EnvironmentReviewStatus::Draft->value, EnvironmentReviewStatus::Failed->value, ], true); } /** * @return array */ private function relatedReviewMetadata(EnvironmentReview $review): array { return array_filter([ 'type' => 'environment_review', 'id' => (int) $review->getKey(), 'status' => (string) $review->status, 'fingerprint' => (string) $review->fingerprint, 'operation_run_id' => is_numeric($review->operation_run_id) ? (int) $review->operation_run_id : null, 'superseded_by_review_id' => is_numeric($review->superseded_by_review_id) ? (int) $review->superseded_by_review_id : null, ], static fn (mixed $value): bool => $value !== null && $value !== []); } /** * @return array */ private function reviewReference(EnvironmentReview $review): array { return [ 'id' => (int) $review->getKey(), 'status' => (string) $review->status, 'operation_run_id' => is_numeric($review->operation_run_id) ? (int) $review->operation_run_id : null, 'superseded_by_review_id' => is_numeric($review->superseded_by_review_id) ? (int) $review->superseded_by_review_id : null, ]; } /** * @return array */ private function summaryCounts(EnvironmentReview $review): array { $summary = is_array($review->summary) ? $review->summary : []; $counts = [ 'finding_count' => is_numeric($summary['finding_count'] ?? null) ? (int) $summary['finding_count'] : null, 'report_count' => is_numeric($summary['report_count'] ?? null) ? (int) $summary['report_count'] : null, 'operation_count' => is_numeric($summary['operation_count'] ?? null) ? (int) $summary['operation_count'] : null, 'errors_recorded' => 0, ]; return array_filter($counts, static fn (mixed $value): bool => is_int($value)); } }