normalizer->supports($canonicalType); } /** * @param array $payload */ public function canBuild(string $canonicalType, array $payload): bool { if (! $this->supports($canonicalType)) { return false; } $normalized = $this->normalizer->normalize($canonicalType, $payload); return ($normalized['supported'] ?? false) === true; } /** * @param array $payload * @param array $context * @return array|null */ public function build(string $canonicalType, array $payload, array $context = []): ?array { if (! $this->supports($canonicalType)) { return null; } $normalized = $this->normalizer->normalize($canonicalType, $payload); if (($normalized['supported'] ?? false) !== true) { return null; } $readiness = $this->readinessEvaluator->evaluate($canonicalType, $normalized, [ 'evidence_state' => $this->stringContext($context, 'evidence_state') ?? 'content_backed', 'coverage_level' => $this->stringContext($context, 'coverage_level') ?? 'renderable', 'identity_state' => $this->stringContext($context, 'identity_state') ?? 'stable', 'capture_outcome' => $this->stringContext($context, 'capture_outcome') ?? 'captured', ], $this->arrayContext($context, 'compare_result')); return match ($canonicalType) { 'retentionCompliancePolicy' => $this->retentionPolicySummary($normalized, $context, $readiness), 'labelPolicy' => $this->labelPolicySummary($normalized, $context, $readiness), 'dlpCompliancePolicy' => $this->dlpPolicySummary($normalized, $context, $readiness), default => null, }; } /** * @param array $summary * @param array $payload * @param array $context * @param array|null $compareResult * @return array */ public function withReadiness( string $canonicalType, array $summary, array $payload, array $context, ?array $compareResult, ): array { if (! $this->supports($canonicalType)) { return $summary; } $normalized = $this->normalizer->normalize($canonicalType, $payload); $readiness = $this->readinessEvaluator->evaluate($canonicalType, $normalized, [ 'evidence_state' => $this->stringContext($context, 'evidence_state'), 'coverage_level' => $this->stringContext($context, 'coverage_level'), 'identity_state' => $this->stringContext($context, 'identity_state'), 'capture_outcome' => $this->stringContext($context, 'capture_outcome'), ], $compareResult); $summaryFields = array_values(array_filter( $summary['summary_fields'] ?? [], static fn (mixed $field): bool => is_array($field) && ($field['label'] ?? null) !== 'Review readiness', )); $summary['readiness'] = $readiness; $summary['summary_fields'] = [ ['label' => 'Review readiness', 'value' => $readiness['label']], ...$summaryFields, ]; return $summary; } /** * @param array $normalized * @param array $context * @param array $readiness * @return array */ private function retentionPolicySummary(array $normalized, array $context, array $readiness): array { $duration = trim(implode(' ', array_filter([ $normalized['retention']['duration'] ?? null, $normalized['retention']['duration_unit'] ?? null, ], static fn (mixed $value): bool => filled($value)))); return $this->baseSummary('Retention compliance policy', $normalized, $context, $readiness, [ ['label' => 'Display name', 'value' => $normalized['display_name'] ?? 'Unnamed retention policy'], ['label' => 'Enabled/state', 'value' => $normalized['enabled_state'] ?? null], ['label' => 'Retention period', 'value' => $duration !== '' ? $duration : null], ['label' => 'Disposition action', 'value' => $normalized['retention']['disposition_action'] ?? null], ['label' => 'Included locations', 'value' => $this->listSummary(data_get($normalized, 'scope.included_locations', []))], ['label' => 'Excluded locations', 'value' => $this->listSummary(data_get($normalized, 'scope.excluded_locations', []))], ], state: $normalized['enabled_state'] ?? null); } /** * @param array $normalized * @param array $context * @param array $readiness * @return array */ private function labelPolicySummary(array $normalized, array $context, array $readiness): array { return $this->baseSummary('Label policy', $normalized, $context, $readiness, [ ['label' => 'Display name', 'value' => $normalized['display_name'] ?? 'Unnamed label policy'], ['label' => 'Published labels', 'value' => $this->listSummary(data_get($normalized, 'labeling.published_labels', []))], ['label' => 'Default label', 'value' => data_get($normalized, 'labeling.default_label')], ['label' => 'Mandatory labeling', 'value' => data_get($normalized, 'labeling.mandatory')], ['label' => 'Included groups', 'value' => $this->listSummary(data_get($normalized, 'scope.included_groups', []))], ], state: $normalized['state'] ?? null); } /** * @param array $normalized * @param array $context * @param array $readiness * @return array */ private function dlpPolicySummary(array $normalized, array $context, array $readiness): array { return $this->baseSummary('DLP compliance policy', $normalized, $context, $readiness, [ ['label' => 'Display name', 'value' => $normalized['display_name'] ?? 'Unnamed DLP policy'], ['label' => 'Mode / enforcement', 'value' => $normalized['mode'] ?? null], ['label' => 'State', 'value' => $normalized['state'] ?? null], ['label' => 'Locations', 'value' => $this->listSummary(data_get($normalized, 'scope.locations', []))], ['label' => 'Actions', 'value' => $this->listSummary($normalized['actions'] ?? [])], ['label' => 'Rules', 'value' => $this->ruleSummary($normalized['rules'] ?? [])], ], state: $normalized['mode'] ?? $normalized['state'] ?? null); } /** * @param array $normalized * @param array $context * @param array $readiness * @param list $fields * @return array */ private function baseSummary( string $resourceType, array $normalized, array $context, array $readiness, array $fields, ?string $state = null, ): array { return [ 'resource_type' => $resourceType, 'display_name' => $normalized['display_name'] ?? 'Unnamed '.$resourceType, 'state' => $state, 'readiness' => $readiness, 'summary_fields' => array_values(array_filter( array_map(fn (array $field): array => [ 'label' => $field['label'], 'value' => $this->summaryValue($field['value']), ], [ ['label' => 'Review readiness', 'value' => $readiness['label'] ?? null], ...$fields, ]), static fn (array $field): bool => filled($field['value'] ?? null), )), 'targets' => [], 'conditions' => [], 'claim_state' => $this->stringContext($context, 'claim_state'), 'identity_state' => $this->stringContext($context, 'identity_state'), 'last_captured' => $this->stringContext($context, 'last_captured'), 'unsupported_fields' => data_get($normalized, 'diagnostics.unsupported_fields', []), 'redacted_fields' => data_get($normalized, 'diagnostics.redacted_fields', []), ]; } /** * @param list $values */ private function listSummary(array $values): ?string { $values = array_values(array_filter( array_map(static fn (mixed $value): string => is_scalar($value) ? trim((string) $value) : '', $values), static fn (string $value): bool => $value !== '', )); return $values === [] ? null : implode(', ', $values); } /** * @param list> $rules */ private function ruleSummary(array $rules): ?string { $parts = []; foreach ($rules as $rule) { if (! is_array($rule)) { continue; } $name = $this->summaryValue($rule['name'] ?? null); $actions = $this->listSummary(is_array($rule['actions'] ?? null) ? $rule['actions'] : []); $parts[] = trim(implode(': ', array_filter([$name, $actions], static fn (?string $value): bool => filled($value)))); } $parts = array_values(array_filter($parts)); return $parts === [] ? null : implode('; ', $parts); } private function summaryValue(mixed $value): ?string { if ($value === null || $value === '' || $value === []) { return null; } if (is_bool($value)) { return $value ? 'yes' : 'no'; } if (is_scalar($value)) { $value = trim((string) $value); return $value !== '' ? $value : null; } if (is_array($value)) { return json_encode($value, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR); } return null; } /** * @param array $context */ private function stringContext(array $context, string $key): ?string { $value = $context[$key] ?? null; if ($value instanceof \BackedEnum) { return (string) $value->value; } if (! is_scalar($value)) { return null; } $value = trim((string) $value); return $value !== '' ? $value : null; } /** * @param array $context * @return array|null */ private function arrayContext(array $context, string $key): ?array { $value = $context[$key] ?? null; return is_array($value) ? $value : null; } }