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.
232 lines
8.7 KiB
PHP
232 lines
8.7 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Support\ReviewPacks;
|
|
|
|
use App\Support\EnvironmentReviewCompletenessState;
|
|
|
|
final class ReviewPackOutputReadiness
|
|
{
|
|
public const string STATE_CUSTOMER_SAFE_READY = 'customer_safe_ready';
|
|
|
|
public const string STATE_PUBLISHED_WITH_LIMITATIONS = 'published_with_limitations';
|
|
|
|
public const string STATE_INTERNAL_REVIEW_PACKAGE_AVAILABLE = 'internal_review_package_available';
|
|
|
|
public const string STATE_EXPORT_NOT_READY = 'export_not_ready';
|
|
|
|
/**
|
|
* @param array<string, int> $sectionStateCounts
|
|
* @param array<string, int> $requiredSectionStateCounts
|
|
* @param list<mixed> $publishBlockers
|
|
* @return array{
|
|
* review_status: string,
|
|
* review_completeness_state: string,
|
|
* evidence_completeness_state: string,
|
|
* has_ready_export: bool,
|
|
* section_state_counts: array<string, int>,
|
|
* required_section_count: int,
|
|
* required_section_state_counts: array<string, int>,
|
|
* required_section_limited_count: int,
|
|
* contains_pii: bool,
|
|
* protected_values_hidden: bool,
|
|
* disclosure_present: bool,
|
|
* customer_safe_state: string,
|
|
* readiness_state: string,
|
|
* primary_reason: string,
|
|
* primary_action: string,
|
|
* limitations: list<array{code: string}>,
|
|
* baseline_readiness: array<string, mixed>,
|
|
* section_summary: array{
|
|
* required_total: int,
|
|
* required_complete: int,
|
|
* required_limited: int,
|
|
* partial: int,
|
|
* missing: int,
|
|
* stale: int
|
|
* }
|
|
* }
|
|
*/
|
|
public static function derive(
|
|
string $reviewStatus,
|
|
string $reviewCompletenessState,
|
|
string $evidenceCompletenessState,
|
|
array $sectionStateCounts,
|
|
int $requiredSectionCount,
|
|
array $requiredSectionStateCounts,
|
|
array $publishBlockers,
|
|
bool $hasReadyExport,
|
|
bool $includePii,
|
|
bool $protectedValuesHidden = true,
|
|
bool $disclosurePresent = true,
|
|
array $baselineReadiness = [],
|
|
): array {
|
|
$sectionStateCounts = self::normalizeCounts($sectionStateCounts);
|
|
$requiredSectionStateCounts = self::normalizeCounts($requiredSectionStateCounts);
|
|
$baselineReadiness = is_array($baselineReadiness) ? $baselineReadiness : [];
|
|
|
|
$requiredLimitedCount = max(
|
|
0,
|
|
(int) ($requiredSectionStateCounts[EnvironmentReviewCompletenessState::Partial->value] ?? 0)
|
|
+ (int) ($requiredSectionStateCounts[EnvironmentReviewCompletenessState::Missing->value] ?? 0)
|
|
+ (int) ($requiredSectionStateCounts[EnvironmentReviewCompletenessState::Stale->value] ?? 0)
|
|
);
|
|
|
|
$limitations = [];
|
|
|
|
if (! $hasReadyExport) {
|
|
$limitations[] = ['code' => 'export_not_ready'];
|
|
}
|
|
|
|
if ($evidenceCompletenessState !== EnvironmentReviewCompletenessState::Complete->value) {
|
|
$limitations[] = ['code' => match ($evidenceCompletenessState) {
|
|
EnvironmentReviewCompletenessState::Missing->value => 'evidence_basis_missing',
|
|
EnvironmentReviewCompletenessState::Stale->value => 'evidence_basis_stale',
|
|
default => 'evidence_basis_incomplete',
|
|
}];
|
|
}
|
|
|
|
if ($requiredLimitedCount > 0) {
|
|
$limitations[] = ['code' => 'required_sections_incomplete'];
|
|
}
|
|
|
|
if ($publishBlockers !== []) {
|
|
$limitations[] = ['code' => 'publish_blockers_present'];
|
|
}
|
|
|
|
if ($includePii) {
|
|
$limitations[] = ['code' => 'contains_pii'];
|
|
}
|
|
|
|
if (! $disclosurePresent) {
|
|
$limitations[] = ['code' => 'disclosure_missing'];
|
|
}
|
|
|
|
array_push($limitations, ...self::baselineLimitations($baselineReadiness));
|
|
|
|
$readinessState = match (true) {
|
|
! $hasReadyExport => self::STATE_EXPORT_NOT_READY,
|
|
self::hasMaterialLimitations($limitations) => self::STATE_PUBLISHED_WITH_LIMITATIONS,
|
|
$includePii => self::STATE_INTERNAL_REVIEW_PACKAGE_AVAILABLE,
|
|
default => self::STATE_CUSTOMER_SAFE_READY,
|
|
};
|
|
|
|
$customerSafeState = match ($readinessState) {
|
|
self::STATE_CUSTOMER_SAFE_READY => 'customer_safe_ready',
|
|
self::STATE_INTERNAL_REVIEW_PACKAGE_AVAILABLE => 'internal_only',
|
|
self::STATE_EXPORT_NOT_READY => 'not_ready',
|
|
default => 'requires_review',
|
|
};
|
|
|
|
$primaryReason = $limitations[0]['code'] ?? 'customer_safe_ready';
|
|
$primaryAction = match ($readinessState) {
|
|
self::STATE_CUSTOMER_SAFE_READY => 'download_customer_safe_review_pack',
|
|
self::STATE_INTERNAL_REVIEW_PACKAGE_AVAILABLE => 'review_package_contents',
|
|
self::STATE_EXPORT_NOT_READY => 'open_evidence_basis',
|
|
default => 'review_output_limitations',
|
|
};
|
|
|
|
$requiredComplete = max(0, min(
|
|
$requiredSectionCount,
|
|
(int) ($requiredSectionStateCounts[EnvironmentReviewCompletenessState::Complete->value] ?? 0)
|
|
));
|
|
|
|
return [
|
|
'review_status' => $reviewStatus,
|
|
'review_completeness_state' => $reviewCompletenessState,
|
|
'evidence_completeness_state' => $evidenceCompletenessState,
|
|
'has_ready_export' => $hasReadyExport,
|
|
'section_state_counts' => $sectionStateCounts,
|
|
'required_section_count' => $requiredSectionCount,
|
|
'required_section_state_counts' => $requiredSectionStateCounts,
|
|
'required_section_limited_count' => $requiredLimitedCount,
|
|
'contains_pii' => $includePii,
|
|
'protected_values_hidden' => $protectedValuesHidden,
|
|
'disclosure_present' => $disclosurePresent,
|
|
'customer_safe_state' => $customerSafeState,
|
|
'readiness_state' => $readinessState,
|
|
'primary_reason' => $primaryReason,
|
|
'primary_action' => $primaryAction,
|
|
'limitations' => $limitations,
|
|
'baseline_readiness' => $baselineReadiness,
|
|
'section_summary' => [
|
|
'required_total' => $requiredSectionCount,
|
|
'required_complete' => $requiredComplete,
|
|
'required_limited' => $requiredLimitedCount,
|
|
'partial' => (int) ($requiredSectionStateCounts[EnvironmentReviewCompletenessState::Partial->value] ?? 0),
|
|
'missing' => (int) ($requiredSectionStateCounts[EnvironmentReviewCompletenessState::Missing->value] ?? 0),
|
|
'stale' => (int) ($requiredSectionStateCounts[EnvironmentReviewCompletenessState::Stale->value] ?? 0),
|
|
],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @param array<string, int> $counts
|
|
* @return array<string, int>
|
|
*/
|
|
private static function normalizeCounts(array $counts): array
|
|
{
|
|
return collect($counts)
|
|
->mapWithKeys(static fn (mixed $count, string|int $key): array => [(string) $key => max(0, (int) $count)])
|
|
->all();
|
|
}
|
|
|
|
/**
|
|
* @param list<array{code: string}> $limitations
|
|
*/
|
|
private static function hasMaterialLimitations(array $limitations): bool
|
|
{
|
|
return collect($limitations)
|
|
->pluck('code')
|
|
->contains(static fn (string $code): bool => $code !== 'contains_pii');
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $baselineReadiness
|
|
* @return list<array{code: string}>
|
|
*/
|
|
private static function baselineLimitations(array $baselineReadiness): array
|
|
{
|
|
if ($baselineReadiness === []) {
|
|
return [];
|
|
}
|
|
|
|
$limitations = [];
|
|
$blockers = is_array($baselineReadiness['publication_blockers'] ?? null)
|
|
? $baselineReadiness['publication_blockers']
|
|
: [];
|
|
$readinessState = (string) ($baselineReadiness['readiness_state'] ?? '');
|
|
|
|
if ($blockers !== []) {
|
|
$limitations[] = ['code' => 'baseline_publication_blockers_present'];
|
|
}
|
|
|
|
if ($readinessState === 'baseline_compare_unproven') {
|
|
$limitations[] = ['code' => 'baseline_compare_unproven'];
|
|
}
|
|
|
|
if ($readinessState === 'baseline_compare_stale') {
|
|
$limitations[] = ['code' => 'baseline_compare_stale'];
|
|
}
|
|
|
|
if ($readinessState === 'baseline_compare_failed') {
|
|
$limitations[] = ['code' => 'baseline_compare_failed'];
|
|
}
|
|
|
|
$codes = collect($baselineReadiness['limitation_codes'] ?? [])
|
|
->filter(static fn (mixed $code): bool => is_string($code) && trim($code) !== '')
|
|
->values()
|
|
->all();
|
|
|
|
foreach ($codes as $code) {
|
|
$limitations[] = ['code' => $code];
|
|
}
|
|
|
|
return collect($limitations)
|
|
->unique('code')
|
|
->values()
|
|
->all();
|
|
}
|
|
}
|