, * sections: list> * } */ public function compose(EvidenceSnapshot $snapshot, ?TenantReview $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 TenantReview && $review->isPublished()) { $status = TenantReviewStatus::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(); 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, '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.'), 'owner_label' => $this->ownerLabel($entry), ]; } /** * @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, '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.', }, ]; } /** * @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 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; } }