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.
255 lines
11 KiB
PHP
255 lines
11 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Support\ReviewPacks;
|
|
|
|
final class ReportDisclosurePolicy
|
|
{
|
|
public const string PROOF_VERIFIED = 'verified';
|
|
|
|
public const string PROOF_ASSUMED = 'assumed';
|
|
|
|
public const string PROOF_MISSING = 'missing';
|
|
|
|
public const string PROOF_UNKNOWN = 'unknown';
|
|
|
|
public const string PROOF_NOT_APPLICABLE = 'not_applicable';
|
|
|
|
/**
|
|
* @param array<string, mixed> $profile
|
|
* @param array<string, mixed> $readiness
|
|
* @param array<string, mixed> $metadata
|
|
* @return array{
|
|
* mandatory_disclosures:list<array{key:string,label:string,summary:string,proof_state:string}>,
|
|
* warnings:list<array{key:string,label:string,summary:string}>,
|
|
* blocking_reasons:list<array{key:string,label:string,summary:string}>,
|
|
* proof_states:array{audience_boundary:string,evidence_basis:string,baseline_readiness:string,protected_values:string,non_certification:string},
|
|
* show_section_appendix:bool,
|
|
* show_technical_details:bool
|
|
* }
|
|
*/
|
|
public static function evaluate(array $profile, array $readiness, array $metadata = []): array
|
|
{
|
|
$isCustomerFacing = (bool) ($profile['is_customer_facing'] ?? false);
|
|
$containsPii = (bool) ($readiness['contains_pii'] ?? false);
|
|
$protectedValuesHidden = (bool) ($readiness['protected_values_hidden'] ?? false);
|
|
$disclosurePresent = (bool) ($readiness['disclosure_present'] ?? false);
|
|
$baselineReadiness = is_array($readiness['baseline_readiness'] ?? null) ? $readiness['baseline_readiness'] : [];
|
|
$displayedDisclosure = self::plainText(
|
|
$metadata['non_certification_disclosure'] ?? null,
|
|
__('localization.review.non_certification_disclosure_text'),
|
|
);
|
|
|
|
$proofStates = [
|
|
'audience_boundary' => self::PROOF_VERIFIED,
|
|
'evidence_basis' => self::evidenceBasisProofState((string) ($readiness['evidence_completeness_state'] ?? '')),
|
|
'baseline_readiness' => self::baselineReadinessProofState($baselineReadiness),
|
|
'protected_values' => self::protectedValuesProofState(
|
|
isCustomerFacing: $isCustomerFacing,
|
|
containsPii: $containsPii,
|
|
protectedValuesHidden: $protectedValuesHidden,
|
|
),
|
|
'non_certification' => $disclosurePresent
|
|
? self::PROOF_ASSUMED
|
|
: self::PROOF_MISSING,
|
|
];
|
|
|
|
$blockingReasons = [];
|
|
|
|
if ($isCustomerFacing && $containsPii) {
|
|
$blockingReasons[] = [
|
|
'key' => 'customer_profile_internal_only',
|
|
'label' => __('localization.review.report_disclosure_customer_profile_internal_only'),
|
|
'summary' => __('localization.review.report_disclosure_customer_profile_internal_only_summary'),
|
|
];
|
|
}
|
|
|
|
if ($isCustomerFacing && self::baselineBlockers($baselineReadiness) !== []) {
|
|
$blockingReasons[] = [
|
|
'key' => 'baseline_readiness_blocked',
|
|
'label' => __('localization.review.baseline_publication_blocked'),
|
|
'summary' => __('localization.review.report_disclosure_baseline_readiness_blocked'),
|
|
];
|
|
}
|
|
|
|
$warnings = [];
|
|
|
|
if ((bool) ($profile['is_fallback'] ?? false)) {
|
|
$warnings[] = [
|
|
'key' => 'profile_fallback',
|
|
'label' => __('localization.review.report_profile_fallback_notice'),
|
|
'summary' => __('localization.review.report_profile_fallback_summary'),
|
|
];
|
|
}
|
|
|
|
if ($isCustomerFacing && (string) ($readiness['customer_safe_state'] ?? '') !== ReviewPackOutputReadiness::STATE_CUSTOMER_SAFE_READY) {
|
|
$warnings[] = [
|
|
'key' => 'customer_profile_requires_review',
|
|
'label' => __('localization.review.report_external_sharing_warning'),
|
|
'summary' => __('localization.review.report_disclosure_customer_profile_requires_review'),
|
|
];
|
|
}
|
|
|
|
if ($proofStates['non_certification'] === self::PROOF_MISSING) {
|
|
$warnings[] = [
|
|
'key' => 'non_certification_missing',
|
|
'label' => __('localization.review.non_certification_disclosure'),
|
|
'summary' => __('localization.review.report_disclosure_non_certification_missing'),
|
|
];
|
|
}
|
|
|
|
if ($isCustomerFacing && self::baselineLimitations($baselineReadiness) !== []) {
|
|
$warnings[] = [
|
|
'key' => 'baseline_limitations_present',
|
|
'label' => __('localization.review.baseline_limitations_short_reason'),
|
|
'summary' => __('localization.review.report_disclosure_baseline_limitations_present'),
|
|
];
|
|
}
|
|
|
|
$showDetailedContent = ! ($isCustomerFacing && $containsPii);
|
|
|
|
return [
|
|
'mandatory_disclosures' => [
|
|
[
|
|
'key' => 'audience_boundary',
|
|
'label' => __('localization.review.report_disclosure_audience_boundary'),
|
|
'summary' => __('localization.review.report_disclosure_audience_boundary_summary', [
|
|
'audience' => (string) ($profile['audience_label'] ?? __('localization.review.unavailable')),
|
|
]),
|
|
'proof_state' => $proofStates['audience_boundary'],
|
|
],
|
|
[
|
|
'key' => 'evidence_basis',
|
|
'label' => __('localization.review.report_disclosure_evidence_basis'),
|
|
'summary' => match ($proofStates['evidence_basis']) {
|
|
self::PROOF_VERIFIED => __('localization.review.report_disclosure_evidence_verified'),
|
|
self::PROOF_MISSING => __('localization.review.report_disclosure_evidence_missing'),
|
|
default => __('localization.review.report_disclosure_evidence_unknown'),
|
|
},
|
|
'proof_state' => $proofStates['evidence_basis'],
|
|
],
|
|
[
|
|
'key' => 'protected_values',
|
|
'label' => __('localization.review.report_disclosure_protected_values'),
|
|
'summary' => match ($proofStates['protected_values']) {
|
|
self::PROOF_ASSUMED => __('localization.review.report_disclosure_protected_values_assumed'),
|
|
self::PROOF_NOT_APPLICABLE => __('localization.review.report_disclosure_protected_values_not_applicable'),
|
|
self::PROOF_MISSING => __('localization.review.report_disclosure_protected_values_missing'),
|
|
default => __('localization.review.report_disclosure_protected_values_unknown'),
|
|
},
|
|
'proof_state' => $proofStates['protected_values'],
|
|
],
|
|
[
|
|
'key' => 'baseline_readiness',
|
|
'label' => __('localization.review.report_disclosure_baseline_readiness'),
|
|
'summary' => match ($proofStates['baseline_readiness']) {
|
|
self::PROOF_VERIFIED => __('localization.review.report_disclosure_baseline_verified'),
|
|
self::PROOF_ASSUMED => __('localization.review.report_disclosure_baseline_limited'),
|
|
self::PROOF_MISSING => __('localization.review.report_disclosure_baseline_missing'),
|
|
default => __('localization.review.report_disclosure_baseline_unknown'),
|
|
},
|
|
'proof_state' => $proofStates['baseline_readiness'],
|
|
],
|
|
[
|
|
'key' => 'non_certification',
|
|
'label' => __('localization.review.non_certification_disclosure'),
|
|
'summary' => $displayedDisclosure,
|
|
'proof_state' => $proofStates['non_certification'],
|
|
],
|
|
],
|
|
'warnings' => $warnings,
|
|
'blocking_reasons' => $blockingReasons,
|
|
'proof_states' => $proofStates,
|
|
'show_section_appendix' => (bool) ($profile['show_section_appendix'] ?? false) && $showDetailedContent,
|
|
'show_technical_details' => (bool) ($profile['show_technical_details'] ?? false) && $showDetailedContent,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $baselineReadiness
|
|
*/
|
|
private static function baselineReadinessProofState(array $baselineReadiness): string
|
|
{
|
|
if ($baselineReadiness === []) {
|
|
return self::PROOF_UNKNOWN;
|
|
}
|
|
|
|
if (self::baselineBlockers($baselineReadiness) !== []) {
|
|
return self::PROOF_MISSING;
|
|
}
|
|
|
|
if (self::baselineLimitations($baselineReadiness) !== []) {
|
|
return self::PROOF_ASSUMED;
|
|
}
|
|
|
|
return (string) ($baselineReadiness['state'] ?? '') === 'complete'
|
|
? self::PROOF_VERIFIED
|
|
: self::PROOF_MISSING;
|
|
}
|
|
|
|
private static function evidenceBasisProofState(string $evidenceCompletenessState): string
|
|
{
|
|
return match ($evidenceCompletenessState) {
|
|
'complete' => self::PROOF_VERIFIED,
|
|
'missing', 'partial', 'stale' => self::PROOF_MISSING,
|
|
default => self::PROOF_UNKNOWN,
|
|
};
|
|
}
|
|
|
|
private static function protectedValuesProofState(
|
|
bool $isCustomerFacing,
|
|
bool $containsPii,
|
|
bool $protectedValuesHidden,
|
|
): string {
|
|
if (! $isCustomerFacing) {
|
|
return self::PROOF_NOT_APPLICABLE;
|
|
}
|
|
|
|
if ($containsPii || ! $protectedValuesHidden) {
|
|
return self::PROOF_MISSING;
|
|
}
|
|
|
|
return self::PROOF_ASSUMED;
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $baselineReadiness
|
|
* @return list<string>
|
|
*/
|
|
private static function baselineBlockers(array $baselineReadiness): array
|
|
{
|
|
return collect($baselineReadiness['publication_blockers'] ?? [])
|
|
->filter(static fn (mixed $blocker): bool => is_string($blocker) && trim($blocker) !== '')
|
|
->values()
|
|
->all();
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $baselineReadiness
|
|
* @return list<string>
|
|
*/
|
|
private static function baselineLimitations(array $baselineReadiness): array
|
|
{
|
|
return collect($baselineReadiness['limitation_codes'] ?? [])
|
|
->filter(static fn (mixed $code): bool => is_string($code) && trim($code) !== '')
|
|
->values()
|
|
->all();
|
|
}
|
|
|
|
private static function plainText(mixed $value, string $fallback): string
|
|
{
|
|
if (! is_scalar($value) && $value !== null) {
|
|
return $fallback;
|
|
}
|
|
|
|
$text = preg_replace('/\s+/', ' ', trim((string) $value));
|
|
|
|
if (! is_string($text) || $text === '') {
|
|
return $fallback;
|
|
}
|
|
|
|
return str_starts_with($text, 'localization.') ? $fallback : $text;
|
|
}
|
|
}
|