Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 4m44s
Added BaselineReadinessGate, resolution propagation, and disclosure semantics logic per Spec 385. Integrated baseline unreadiness into Customer Review Workspace and Review Packs.
165 lines
5.7 KiB
PHP
165 lines
5.7 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Services\EnvironmentReviews;
|
|
|
|
use App\Models\EnvironmentReview;
|
|
use App\Support\EnvironmentReviewCompletenessState;
|
|
use App\Support\EnvironmentReviewStatus;
|
|
use Illuminate\Support\Collection;
|
|
|
|
final class EnvironmentReviewReadinessGate
|
|
{
|
|
/**
|
|
* @param iterable<array<string, mixed>> $sections
|
|
* @return list<string>
|
|
*/
|
|
public function blockersForSections(iterable $sections): array
|
|
{
|
|
$blockers = [];
|
|
|
|
foreach ($sections as $section) {
|
|
$required = (bool) ($section['required'] ?? false);
|
|
$state = (string) ($section['completeness_state'] ?? EnvironmentReviewCompletenessState::Missing->value);
|
|
$title = (string) ($section['title'] ?? 'Review section');
|
|
|
|
if (! $required) {
|
|
continue;
|
|
}
|
|
|
|
if ($state === EnvironmentReviewCompletenessState::Missing->value) {
|
|
$blockers[] = sprintf('%s is missing.', $title);
|
|
}
|
|
|
|
if ($state === EnvironmentReviewCompletenessState::Stale->value) {
|
|
$blockers[] = sprintf('%s is stale and must be refreshed before publication.', $title);
|
|
}
|
|
|
|
foreach ($this->sectionPublicationBlockers($section) as $sectionBlocker) {
|
|
$blockers[] = sprintf('%s: %s', $title, $sectionBlocker);
|
|
}
|
|
}
|
|
|
|
return array_values(array_unique($blockers));
|
|
}
|
|
|
|
/**
|
|
* @param iterable<array<string, mixed>> $sections
|
|
*/
|
|
public function completenessForSections(iterable $sections): EnvironmentReviewCompletenessState
|
|
{
|
|
$states = collect($sections)
|
|
->map(static fn (array $section): string => (string) ($section['completeness_state'] ?? EnvironmentReviewCompletenessState::Missing->value))
|
|
->values();
|
|
|
|
if ($states->isEmpty()) {
|
|
return EnvironmentReviewCompletenessState::Missing;
|
|
}
|
|
|
|
if ($states->contains(EnvironmentReviewCompletenessState::Missing->value)) {
|
|
return EnvironmentReviewCompletenessState::Missing;
|
|
}
|
|
|
|
if ($states->contains(EnvironmentReviewCompletenessState::Stale->value)) {
|
|
return EnvironmentReviewCompletenessState::Stale;
|
|
}
|
|
|
|
if ($states->contains(EnvironmentReviewCompletenessState::Partial->value)) {
|
|
return EnvironmentReviewCompletenessState::Partial;
|
|
}
|
|
|
|
return EnvironmentReviewCompletenessState::Complete;
|
|
}
|
|
|
|
/**
|
|
* @param iterable<array<string, mixed>> $sections
|
|
*/
|
|
public function statusForSections(iterable $sections): EnvironmentReviewStatus
|
|
{
|
|
return $this->blockersForSections($sections) === []
|
|
? EnvironmentReviewStatus::Ready
|
|
: EnvironmentReviewStatus::Draft;
|
|
}
|
|
|
|
/**
|
|
* @return list<string>
|
|
*/
|
|
public function blockersForReview(EnvironmentReview $review): array
|
|
{
|
|
$sections = $review->relationLoaded('sections')
|
|
? $review->sections
|
|
: $review->sections()->get();
|
|
|
|
return $this->blockersForSections($sections->map(static function ($section): array {
|
|
return [
|
|
'title' => (string) $section->title,
|
|
'required' => (bool) $section->required,
|
|
'completeness_state' => (string) $section->completeness_state,
|
|
'summary_payload' => is_array($section->summary_payload) ? $section->summary_payload : [],
|
|
];
|
|
})->all());
|
|
}
|
|
|
|
public function canPublish(EnvironmentReview $review): bool
|
|
{
|
|
if (! $review->isMutable()) {
|
|
return false;
|
|
}
|
|
|
|
return $this->blockersForReview($review) === [];
|
|
}
|
|
|
|
public function canExport(EnvironmentReview $review): bool
|
|
{
|
|
if (! in_array($review->statusEnum(), [
|
|
EnvironmentReviewStatus::Ready,
|
|
EnvironmentReviewStatus::Published,
|
|
], true)) {
|
|
return false;
|
|
}
|
|
|
|
return $this->blockersForReview($review) === [];
|
|
}
|
|
|
|
/**
|
|
* @param iterable<array<string, mixed>> $sections
|
|
* @return array<string, int>
|
|
*/
|
|
public function sectionStateCounts(iterable $sections): array
|
|
{
|
|
$counts = collect($sections)
|
|
->groupBy(static fn (array $section): string => (string) ($section['completeness_state'] ?? EnvironmentReviewCompletenessState::Missing->value))
|
|
->map(static fn (Collection $group): int => $group->count());
|
|
|
|
return [
|
|
'complete' => (int) ($counts[EnvironmentReviewCompletenessState::Complete->value] ?? 0),
|
|
'partial' => (int) ($counts[EnvironmentReviewCompletenessState::Partial->value] ?? 0),
|
|
'missing' => (int) ($counts[EnvironmentReviewCompletenessState::Missing->value] ?? 0),
|
|
'stale' => (int) ($counts[EnvironmentReviewCompletenessState::Stale->value] ?? 0),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $section
|
|
* @return list<string>
|
|
*/
|
|
private function sectionPublicationBlockers(array $section): array
|
|
{
|
|
$summary = is_array($section['summary_payload'] ?? null) ? $section['summary_payload'] : [];
|
|
$blockers = is_array($summary['publication_blockers'] ?? null) ? $summary['publication_blockers'] : [];
|
|
|
|
if ($blockers === []) {
|
|
$baselineReadiness = is_array($summary['baseline_readiness'] ?? null) ? $summary['baseline_readiness'] : [];
|
|
$blockers = is_array($baselineReadiness['publication_blockers'] ?? null)
|
|
? $baselineReadiness['publication_blockers']
|
|
: [];
|
|
}
|
|
|
|
return collect($blockers)
|
|
->filter(static fn (mixed $blocker): bool => is_string($blocker) && trim($blocker) !== '')
|
|
->values()
|
|
->all();
|
|
}
|
|
}
|