, * sections: list> * } */ public function compose(EvidenceSnapshot $snapshot, ?EnvironmentReview $review = null): array { $tenant = $snapshot->tenant; if ($tenant === null) { throw new \RuntimeException('Evidence snapshot tenant is required for review composition.'); } $sections = $this->sectionFactory->make($snapshot); $blockers = $this->readinessGate->blockersForSections($sections); $sectionStateCounts = $this->readinessGate->sectionStateCounts($sections); $completeness = $this->readinessGate->completenessForSections($sections); $status = $this->readinessGate->statusForSections($sections); $executiveSummarySection = collect($sections) ->firstWhere('section_key', 'executive_summary'); $controlInterpretationSection = collect($sections) ->firstWhere('section_key', 'control_interpretation'); $openRisksSection = collect($sections) ->firstWhere('section_key', 'open_risks'); $acceptedRisksSection = collect($sections) ->firstWhere('section_key', 'accepted_risks'); $operationsSection = collect($sections) ->firstWhere('section_key', 'operations_health'); if ($review instanceof EnvironmentReview && $review->isPublished()) { $status = EnvironmentReviewStatus::Published; } return [ 'fingerprint' => $this->fingerprint->forSnapshot($tenant, $snapshot), 'completeness_state' => $completeness->value, 'status' => $status->value, 'summary' => [ 'evidence_basis' => [ 'snapshot_id' => (int) $snapshot->getKey(), 'snapshot_fingerprint' => (string) $snapshot->fingerprint, 'snapshot_completeness_state' => (string) $snapshot->completeness_state, 'snapshot_generated_at' => $snapshot->generated_at?->toIso8601String(), ], 'section_count' => count($sections), 'section_state_counts' => $sectionStateCounts, 'publish_blockers' => $blockers, 'has_ready_export' => false, 'finding_count' => (int) data_get($sections, '0.summary_payload.finding_count', 0), 'finding_outcomes' => is_array(data_get($sections, '0.summary_payload.finding_outcomes')) ? data_get($sections, '0.summary_payload.finding_outcomes') : [], 'finding_report_buckets' => is_array(data_get($sections, '0.summary_payload.finding_report_buckets')) ? data_get($sections, '0.summary_payload.finding_report_buckets') : [], 'canonical_controls' => is_array(data_get($sections, '0.summary_payload.canonical_controls')) ? data_get($sections, '0.summary_payload.canonical_controls') : [], 'control_interpretation' => is_array(data_get($controlInterpretationSection, 'summary_payload')) ? array_merge( data_get($controlInterpretationSection, 'summary_payload'), [ 'controls' => is_array(data_get($controlInterpretationSection, 'render_payload.entries')) ? data_get($controlInterpretationSection, 'render_payload.entries') : [], ], ) : [], 'report_count' => 2, 'operation_count' => (int) data_get($operationsSection, 'summary_payload.operation_count', 0), 'highlights' => data_get($sections, '0.render_payload.highlights', []), 'recommended_next_actions' => data_get($sections, '0.render_payload.next_actions', []), 'governance_package' => $this->governancePackageSummary( snapshot: $snapshot, executiveSummarySection: is_array($executiveSummarySection) ? $executiveSummarySection : [], controlInterpretationSection: is_array($controlInterpretationSection) ? $controlInterpretationSection : [], openRisksSection: is_array($openRisksSection) ? $openRisksSection : [], acceptedRisksSection: is_array($acceptedRisksSection) ? $acceptedRisksSection : [], ), 'last_composed_at' => now()->toIso8601String(), ], 'sections' => $sections, ]; } /** * @param array $executiveSummarySection * @param array $controlInterpretationSection * @param array $openRisksSection * @param array $acceptedRisksSection * @return array */ private function governancePackageSummary( EvidenceSnapshot $snapshot, array $executiveSummarySection, array $controlInterpretationSection, array $openRisksSection, array $acceptedRisksSection, ): array { $executiveSummaryPayload = is_array($executiveSummarySection['summary_payload'] ?? null) ? $executiveSummarySection['summary_payload'] : []; $executiveRenderPayload = is_array($executiveSummarySection['render_payload'] ?? null) ? $executiveSummarySection['render_payload'] : []; $controlInterpretationSummary = is_array($controlInterpretationSection['summary_payload'] ?? null) ? $controlInterpretationSection['summary_payload'] : []; $openRiskEntries = collect(data_get($openRisksSection, 'render_payload.entries', [])) ->filter(static fn (mixed $entry): bool => is_array($entry)) ->take(3) ->map(fn (array $entry): array => $this->packageFindingEntry($entry)) ->values() ->all(); $acceptedRiskEntries = collect(data_get($acceptedRisksSection, 'render_payload.entries', [])) ->filter(static fn (mixed $entry): bool => is_array($entry)) ->map(fn (array $entry): array => $this->packageAcceptedRiskEntry($entry)) ->values(); $governanceDecisionEntries = $acceptedRiskEntries ->filter(fn (array $entry): bool => $this->requiresGovernanceDecisionFollowUp($entry)) ->values(); $stableAcceptedRiskEntries = $acceptedRiskEntries ->reject(fn (array $entry): bool => $this->requiresGovernanceDecisionFollowUp($entry)) ->values(); $governanceDecisions = $governanceDecisionEntries ->map(fn (array $entry): array => $this->packageGovernanceDecisionEntry($entry)) ->values() ->all(); $decisionSummary = $this->packageDecisionSummary( snapshot: $snapshot, acceptedRisksSection: $acceptedRisksSection, governanceDecisions: $governanceDecisions, ); return [ 'delivery_artifact_family' => 'review_pack', 'interpretation_version' => is_string($controlInterpretationSummary['version_key'] ?? null) ? $controlInterpretationSummary['version_key'] : null, 'executive_summary' => $this->governancePackageExecutiveSummary( executiveSummaryPayload: $executiveSummaryPayload, executiveRenderPayload: $executiveRenderPayload, controlInterpretationSummary: $controlInterpretationSummary, acceptedRiskCount: $acceptedRiskEntries->count(), ), 'top_findings' => $openRiskEntries, 'accepted_risks' => $stableAcceptedRiskEntries->all(), 'governance_decisions' => $governanceDecisions, 'decision_summary' => $decisionSummary, 'evidence_basis_summary' => $this->governancePackageEvidenceBasisSummary( snapshot: $snapshot, controlInterpretationSummary: $controlInterpretationSummary, ), 'supporting_artifact_links' => [ [ 'artifact_family' => 'evidence_snapshot', 'artifact_key' => 'evidence_snapshot:'.$snapshot->getKey(), 'purpose' => 'evidence_basis', ], [ 'artifact_family' => 'review_pack', 'artifact_key' => 'review_pack:current_export', 'purpose' => 'stakeholder_delivery', ], ], ]; } /** * @param array $entry * @return array */ private function packageFindingEntry(array $entry): array { return [ 'finding_id' => is_numeric($entry['id'] ?? null) ? (int) $entry['id'] : null, 'title' => $this->entryTitle($entry, 'Open finding'), 'severity' => is_string($entry['severity'] ?? null) ? $entry['severity'] : 'unknown', 'status' => is_string($entry['status'] ?? null) ? $entry['status'] : 'unknown', 'summary' => $this->entrySummary($entry, 'This finding remains open in the released review and should be discussed in stakeholder delivery.'), ]; } /** * @param array $entry * @return array */ private function packageAcceptedRiskEntry(array $entry): array { return [ 'finding_id' => is_numeric($entry['id'] ?? null) ? (int) $entry['id'] : null, 'title' => $this->entryTitle($entry, 'Accepted risk'), 'governance_state' => is_string($entry['governance_state'] ?? null) ? $entry['governance_state'] : 'unknown', 'summary' => $this->entrySummary($entry, 'This accepted-risk entry qualifies the current governance position for stakeholder delivery.'), 'customer_safe_summary' => $this->customerSafeSummary($entry), 'owner_label' => $this->ownerLabel($entry), 'status' => is_string($entry['exception_status'] ?? null) ? $entry['exception_status'] : null, 'review_due_at' => is_string($entry['review_due_at'] ?? null) ? $entry['review_due_at'] : null, 'expires_at' => is_string($entry['expires_at'] ?? null) ? $entry['expires_at'] : null, ]; } /** * @param array $entry */ private function requiresGovernanceDecisionFollowUp(array $entry): bool { return in_array((string) ($entry['governance_state'] ?? ''), [ 'expired_exception', 'revoked_exception', 'risk_accepted_without_valid_exception', ], true); } /** * @param array $entry * @return array */ private function packageGovernanceDecisionEntry(array $entry): array { $governanceState = (string) ($entry['governance_state'] ?? 'unknown'); return [ 'finding_id' => $entry['finding_id'] ?? null, 'title' => $entry['title'] ?? 'Governance decision', 'governance_state' => $governanceState, 'awareness_reason' => $this->governanceDecisionAwarenessReason($governanceState), 'summary' => match ($governanceState) { 'expired_exception' => 'The accepted-risk exception has expired and needs follow-up before stakeholder delivery.', 'revoked_exception' => 'The accepted-risk exception was revoked and needs follow-up before stakeholder delivery.', 'risk_accepted_without_valid_exception' => 'The accepted-risk entry has no currently valid exception basis and needs follow-up before stakeholder delivery.', default => 'This governance decision needs follow-up before stakeholder delivery.', }, 'next_action' => $this->governanceDecisionNextAction($governanceState), 'evidence_basis' => 'Included in the anchored released-review evidence basis.', ]; } /** * @param array $acceptedRisksSection * @param list> $governanceDecisions * @return array */ private function packageDecisionSummary( EvidenceSnapshot $snapshot, array $acceptedRisksSection, array $governanceDecisions, ): array { $totalCount = count($governanceDecisions); $evidenceState = is_string($acceptedRisksSection['completeness_state'] ?? null) ? (string) $acceptedRisksSection['completeness_state'] : (string) $snapshot->completeness_state; $decisionDataState = $totalCount > 0 || $this->decisionEvidenceIsAvailable($evidenceState) ? 'available' : 'incomplete'; $status = match (true) { $totalCount > 0 => 'requires_awareness', $decisionDataState === 'incomplete' => 'unavailable', default => 'none', }; return [ 'customer_safe' => true, 'status' => $status, 'decision_data_state' => $decisionDataState, 'evidence_state' => $evidenceState, 'total_count' => $totalCount, 'requires_awareness' => $totalCount > 0, 'summary' => $this->decisionSummaryText($status, $totalCount), 'empty_state' => $this->decisionSummaryEmptyState($status), 'next_action' => $this->decisionSummaryNextAction($status), 'evidence_basis' => sprintf( 'Anchored to evidence snapshot #%d with %s decision-evidence completeness.', (int) $snapshot->getKey(), $evidenceState, ), 'source_section_state' => is_string($acceptedRisksSection['completeness_state'] ?? null) ? $acceptedRisksSection['completeness_state'] : null, 'entries' => $governanceDecisions, ]; } private function decisionEvidenceIsAvailable(string $evidenceState): bool { return in_array($evidenceState, ['complete'], true); } private function decisionSummaryText(string $status, int $totalCount): string { return match ($status) { 'requires_awareness' => sprintf( '%d governance decision%s require%s customer awareness before relying on this released review.', $totalCount, $totalCount === 1 ? '' : 's', $totalCount === 1 ? 's' : '', ), 'unavailable' => 'Decision evidence is incomplete for this released review; no customer-aware decisions can be confirmed from the current evidence basis.', default => 'No governance decisions require customer awareness in this released review.', }; } private function decisionSummaryEmptyState(string $status): string { return match ($status) { 'unavailable' => 'Decision evidence is incomplete in the current released-review basis.', 'requires_awareness' => '', default => 'No governance decisions require awareness in this released review.', }; } private function decisionSummaryNextAction(string $status): string { return match ($status) { 'requires_awareness' => 'Review the accepted-risk decision basis before customer delivery.', 'unavailable' => 'Open the review evidence before treating the decision register summary as complete.', default => 'No customer action is needed for Decision Register follow-up from this review.', }; } private function governanceDecisionAwarenessReason(string $governanceState): string { return match ($governanceState) { 'expired_exception' => 'The accepted-risk approval has expired and needs customer awareness before the review is relied on.', 'revoked_exception' => 'The accepted-risk approval was revoked and needs customer awareness before the review is relied on.', 'risk_accepted_without_valid_exception' => 'The accepted risk has no valid governance backing in the released review evidence.', default => 'This accepted-risk governance decision needs customer awareness before the review is relied on.', }; } private function governanceDecisionNextAction(string $governanceState): string { return match ($governanceState) { 'expired_exception', 'revoked_exception', 'risk_accepted_without_valid_exception' => 'Confirm whether this accepted risk should be renewed, remediated, or removed before relying on the review.', default => 'Confirm the accepted-risk decision before relying on the review.', }; } /** * @param array $executiveSummaryPayload * @param array $executiveRenderPayload * @param array $controlInterpretationSummary */ private function governancePackageExecutiveSummary( array $executiveSummaryPayload, array $executiveRenderPayload, array $controlInterpretationSummary, int $acceptedRiskCount, ): string { $highlights = collect($executiveRenderPayload['highlights'] ?? []) ->filter(static fn (mixed $highlight): bool => is_string($highlight) && trim($highlight) !== '') ->values(); if ($highlights->isNotEmpty()) { return (string) $highlights->first(); } return sprintf( 'This released review summarizes %d mapped control(s), %d open risk(s), and %d accepted-risk item(s) from the anchored evidence basis.', (int) ($controlInterpretationSummary['mapped_control_count'] ?? 0), (int) ($executiveSummaryPayload['open_risk_count'] ?? 0), $acceptedRiskCount, ); } /** * @param array $controlInterpretationSummary */ private function governancePackageEvidenceBasisSummary(EvidenceSnapshot $snapshot, array $controlInterpretationSummary): string { return sprintf( 'Anchored to evidence snapshot #%d with %s completeness and %d mapped control(s).', (int) $snapshot->getKey(), (string) $snapshot->completeness_state, (int) ($controlInterpretationSummary['mapped_control_count'] ?? 0), ); } /** * @param array $entry */ private function entryTitle(array $entry, string $fallback): string { foreach (['title', 'name', 'finding_title'] as $key) { $value = $entry[$key] ?? null; if (is_string($value) && trim($value) !== '') { return $value; } } return $fallback; } /** * @param array $entry */ private function entrySummary(array $entry, string $fallback): string { foreach (['customer_summary', 'summary', 'request_reason'] as $key) { $value = $entry[$key] ?? null; if (is_string($value) && trim($value) !== '') { return $value; } } return $fallback; } /** * @param array $entry */ private function customerSafeSummary(array $entry): ?string { foreach (['customer_safe_summary', 'customer_summary'] as $key) { $value = $entry[$key] ?? null; if (is_string($value) && trim($value) !== '') { return $value; } } return null; } /** * @param array $entry */ private function ownerLabel(array $entry): ?string { $owner = $entry['owner'] ?? null; if (is_array($owner)) { $name = $owner['name'] ?? null; if (is_string($name) && trim($name) !== '') { return $name; } } return null; } }