$readiness * @return array{ * steps:list>, * case_status:ReviewPublicationResolutionCaseStatus, * current_step_key:?string * } */ public function plan(EnvironmentReview $review, array $readiness, ?ReviewPublicationResolutionCase $case = null): array { $existing = $case instanceof ReviewPublicationResolutionCase ? $case->loadMissing(['steps.operationRun'])->steps->keyBy('step_key') : new Collection; $steps = []; foreach ($this->relevantStepKeys($readiness, $existing) as $index => $stepKey) { $steps[] = $this->stepPlan( review: $review, readiness: $readiness, stepKey: $stepKey, position: $index + 1, existingStep: $existing->get($stepKey->value), ); } $steps = $this->activateFirstIncompleteStep($steps); $currentStep = collect($steps)->first( static fn (array $step): bool => in_array($step['status'], [ ReviewPublicationResolutionStepStatus::Actionable->value, ReviewPublicationResolutionStepStatus::Running->value, ReviewPublicationResolutionStepStatus::Failed->value, ], true), ); $caseStatus = $this->caseStatus($steps, is_array($currentStep) ? (string) $currentStep['step_key'] : null); return [ 'steps' => $steps, 'case_status' => $caseStatus, 'current_step_key' => is_array($currentStep) ? (string) $currentStep['step_key'] : null, ]; } /** * @param array $readiness * @return array */ private function stepPlan( EnvironmentReview $review, array $readiness, ReviewPublicationResolutionStepKey $stepKey, int $position, ?ReviewPublicationResolutionStep $existingStep, ): array { $status = $this->baseStatus($stepKey, $readiness, $existingStep); $proof = $this->proofResolver->proofFor($stepKey, $review); if ($existingStep instanceof ReviewPublicationResolutionStep && in_array($status, [ ReviewPublicationResolutionStepStatus::Running, ReviewPublicationResolutionStepStatus::Failed, ], true)) { $proof = [ 'proof_type' => is_string($existingStep->proof_type) ? $existingStep->proof_type : $proof['proof_type'], 'proof_id' => is_numeric($existingStep->proof_id) ? (int) $existingStep->proof_id : $proof['proof_id'], 'proof_status' => is_string($existingStep->proof_status) ? $existingStep->proof_status : $proof['proof_status'], 'operation_run_id' => is_numeric($existingStep->operation_run_id) ? (int) $existingStep->operation_run_id : $proof['operation_run_id'], ]; } return [ 'position' => $position, 'step_key' => $stepKey->value, 'status' => $status->value, 'primary_action_key' => $stepKey->primaryActionKey(), 'operation_run_id' => $proof['operation_run_id'], 'proof_type' => $proof['proof_type'], 'proof_id' => $proof['proof_id'], 'proof_status' => $proof['proof_status'], 'summary' => $this->summary($stepKey, $readiness, $status), 'metadata' => [ 'readiness_fingerprint' => (string) $readiness['fingerprint'], 'planned_at' => now()->toIso8601String(), ], ]; } /** * @param array $readiness */ private function baseStatus( ReviewPublicationResolutionStepKey $stepKey, array $readiness, ?ReviewPublicationResolutionStep $existingStep, ): ReviewPublicationResolutionStepStatus { $readinessStatus = $this->readinessStatus($stepKey, $readiness); if ($readinessStatus === ReviewPublicationResolutionStepStatus::Completed) { return ReviewPublicationResolutionStepStatus::Completed; } if ($existingStep instanceof ReviewPublicationResolutionStep) { if ($stepKey === ReviewPublicationResolutionStepKey::ReturnToPublication && $existingStep->statusEnum() === ReviewPublicationResolutionStepStatus::Completed) { return ReviewPublicationResolutionStepStatus::Completed; } if ($existingStep->statusEnum() === ReviewPublicationResolutionStepStatus::Running && $existingStep->operationRun?->status !== OperationRunStatus::Completed->value) { return ReviewPublicationResolutionStepStatus::Running; } if ($existingStep->operationRun?->status === OperationRunStatus::Completed->value && in_array((string) $existingStep->operationRun->outcome, [OperationRunOutcome::Failed->value, OperationRunOutcome::Blocked->value], true)) { return ReviewPublicationResolutionStepStatus::Failed; } } return $readinessStatus; } /** * @param Collection $existing * @return list */ private function relevantStepKeys(array $readiness, Collection $existing): array { $keys = [ ReviewPublicationResolutionStepKey::ValidateReviewReadiness, ]; if (((array) ($readiness['missing_report_dimensions'] ?? [])) !== [] || $this->hasExistingStep($existing, ReviewPublicationResolutionStepKey::CompleteRequiredReports)) { $keys[] = ReviewPublicationResolutionStepKey::CompleteRequiredReports; } if ((bool) ($readiness['evidence_incomplete'] ?? true) || $this->hasExistingStep($existing, ReviewPublicationResolutionStepKey::CollectEvidenceSnapshot)) { $keys[] = ReviewPublicationResolutionStepKey::CollectEvidenceSnapshot; } if ((bool) ($readiness['review_requires_refresh'] ?? true) || $this->hasExistingStep($existing, ReviewPublicationResolutionStepKey::RefreshReviewComposition)) { $keys[] = ReviewPublicationResolutionStepKey::RefreshReviewComposition; } if (! (bool) ($readiness['has_ready_export'] ?? false) || $this->hasExistingStep($existing, ReviewPublicationResolutionStepKey::GenerateReviewPack)) { $keys[] = ReviewPublicationResolutionStepKey::GenerateReviewPack; } $keys[] = ReviewPublicationResolutionStepKey::ReturnToPublication; return $keys; } /** * @param Collection $existing */ private function hasExistingStep(Collection $existing, ReviewPublicationResolutionStepKey $stepKey): bool { $step = $existing->get($stepKey->value); return $step instanceof ReviewPublicationResolutionStep && $step->statusEnum() !== ReviewPublicationResolutionStepStatus::Superseded; } /** * @param array $readiness */ private function readinessStatus( ReviewPublicationResolutionStepKey $stepKey, array $readiness, ): ReviewPublicationResolutionStepStatus { return match ($stepKey) { ReviewPublicationResolutionStepKey::ValidateReviewReadiness => ReviewPublicationResolutionStepStatus::Completed, ReviewPublicationResolutionStepKey::CompleteRequiredReports => ((array) ($readiness['missing_report_dimensions'] ?? [])) === [] ? ReviewPublicationResolutionStepStatus::Completed : ReviewPublicationResolutionStepStatus::Pending, ReviewPublicationResolutionStepKey::CollectEvidenceSnapshot => (bool) ($readiness['evidence_incomplete'] ?? true) ? ReviewPublicationResolutionStepStatus::Pending : ReviewPublicationResolutionStepStatus::Completed, ReviewPublicationResolutionStepKey::RefreshReviewComposition => (bool) ($readiness['review_requires_refresh'] ?? true) ? ReviewPublicationResolutionStepStatus::Pending : ReviewPublicationResolutionStepStatus::Completed, ReviewPublicationResolutionStepKey::GenerateReviewPack => (bool) ($readiness['has_ready_export'] ?? false) ? ReviewPublicationResolutionStepStatus::Completed : ReviewPublicationResolutionStepStatus::Pending, ReviewPublicationResolutionStepKey::ReturnToPublication => ReviewPublicationResolutionStepStatus::Pending, }; } /** * @param list> $steps * @return list> */ private function activateFirstIncompleteStep(array $steps): array { foreach ($steps as $index => $step) { if ($step['status'] !== ReviewPublicationResolutionStepStatus::Pending->value) { continue; } $steps[$index]['status'] = ReviewPublicationResolutionStepStatus::Actionable->value; $steps[$index]['summary']['state_description'] = 'Ready for operator action.'; break; } return $steps; } /** * @param list> $steps */ private function caseStatus(array $steps, ?string $currentStepKey): ReviewPublicationResolutionCaseStatus { if ($currentStepKey === null) { return ReviewPublicationResolutionCaseStatus::Completed; } $currentStep = collect($steps)->firstWhere('step_key', $currentStepKey); if (is_array($currentStep) && $currentStep['status'] === ReviewPublicationResolutionStepStatus::Running->value) { return ReviewPublicationResolutionCaseStatus::WaitingForRun; } if (is_array($currentStep) && $currentStep['status'] === ReviewPublicationResolutionStepStatus::Failed->value) { return ReviewPublicationResolutionCaseStatus::Blocked; } if ($currentStepKey === ReviewPublicationResolutionStepKey::ReturnToPublication->value) { return ReviewPublicationResolutionCaseStatus::ReadyToContinue; } return ReviewPublicationResolutionCaseStatus::InProgress; } /** * @param array $readiness * @return array */ private function summary( ReviewPublicationResolutionStepKey $stepKey, array $readiness, ReviewPublicationResolutionStepStatus $status, ): array { return [ 'label' => $stepKey->label(), 'description' => match ($stepKey) { ReviewPublicationResolutionStepKey::ValidateReviewReadiness => 'Review readiness has been evaluated from current evidence and section state.', ReviewPublicationResolutionStepKey::CompleteRequiredReports => 'Required report-backed evidence dimensions must be current before publication can continue.', ReviewPublicationResolutionStepKey::CollectEvidenceSnapshot => 'The evidence snapshot must be complete and current for the review output.', ReviewPublicationResolutionStepKey::RefreshReviewComposition => 'Review sections must be recomposed from current evidence before publication.', ReviewPublicationResolutionStepKey::GenerateReviewPack => 'A current review pack is required before returning to publication.', ReviewPublicationResolutionStepKey::ReturnToPublication => 'Return to the review publication action after blockers are resolved.', }, 'state_description' => $status === ReviewPublicationResolutionStepStatus::Completed ? 'Requirement is satisfied.' : 'Waiting for prerequisite steps.', 'publication_blocker_count' => count((array) ($readiness['publication_blockers'] ?? [])), 'missing_report_dimensions' => array_values((array) ($readiness['missing_report_dimensions'] ?? [])), 'evidence_state' => (string) ($readiness['evidence_state'] ?? ''), 'review_status' => (string) ($readiness['review_status'] ?? ''), 'has_ready_export' => (bool) ($readiness['has_ready_export'] ?? false), ]; } }