## Summary\n- Implements the ReviewPublicationResolutionWorkflow for Spec 386.\n- Adds resolution case/step persistence, policies, services, audit action IDs, and Filament integration.\n- Updates specs, UI/UX documentation, screenshots, and Pest coverage.\n\n## Tests\n- Not run during this handoff; branch was already clean and pushed.\n\n## Target\n- Base: platform-dev\n- Head/topic: 386-review-publication-resolution-workflow-v1 Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #457
289 lines
13 KiB
PHP
289 lines
13 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Support\ReviewPublicationResolution;
|
|
|
|
use App\Models\EnvironmentReview;
|
|
use App\Models\ReviewPublicationResolutionCase;
|
|
use App\Models\ReviewPublicationResolutionStep;
|
|
use App\Support\OperationRunOutcome;
|
|
use App\Support\OperationRunStatus;
|
|
use Illuminate\Support\Collection;
|
|
|
|
final class ReviewPublicationStepPlanner
|
|
{
|
|
public function __construct(
|
|
private readonly ReviewPublicationProofResolver $proofResolver,
|
|
) {}
|
|
|
|
/**
|
|
* @param array<string, mixed> $readiness
|
|
* @return array{
|
|
* steps:list<array<string, mixed>>,
|
|
* 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<string, mixed> $readiness
|
|
* @return array<string, mixed>
|
|
*/
|
|
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<string, mixed> $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<string, ReviewPublicationResolutionStep> $existing
|
|
* @return list<ReviewPublicationResolutionStepKey>
|
|
*/
|
|
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<string, ReviewPublicationResolutionStep> $existing
|
|
*/
|
|
private function hasExistingStep(Collection $existing, ReviewPublicationResolutionStepKey $stepKey): bool
|
|
{
|
|
$step = $existing->get($stepKey->value);
|
|
|
|
return $step instanceof ReviewPublicationResolutionStep
|
|
&& $step->statusEnum() !== ReviewPublicationResolutionStepStatus::Superseded;
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $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<array<string, mixed>> $steps
|
|
* @return list<array<string, mixed>>
|
|
*/
|
|
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<array<string, mixed>> $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<string, mixed> $readiness
|
|
* @return array<string, mixed>
|
|
*/
|
|
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),
|
|
];
|
|
}
|
|
}
|