diff --git a/apps/platform/app/Filament/Pages/Monitoring/EvidenceOverview.php b/apps/platform/app/Filament/Pages/Monitoring/EvidenceOverview.php index 95c5dcf8..34d40668 100644 --- a/apps/platform/app/Filament/Pages/Monitoring/EvidenceOverview.php +++ b/apps/platform/app/Filament/Pages/Monitoring/EvidenceOverview.php @@ -5,6 +5,7 @@ namespace App\Filament\Pages\Monitoring; use App\Filament\Concerns\ClearsWorkspaceHubEnvironmentFilterState; +use App\Filament\Pages\Reviews\CustomerReviewWorkspace; use App\Filament\Resources\EvidenceSnapshotResource; use App\Filament\Resources\ReviewPackResource; use App\Filament\Resources\StoredReportResource; @@ -16,12 +17,19 @@ use App\Models\StoredReport; use App\Models\User; use App\Models\Workspace; +use App\Services\ReviewPackService; +use App\Support\Auth\Capabilities; use App\Support\Badges\BadgeDomain; use App\Support\Badges\BadgeRenderer; use App\Support\EnvironmentReviewStatus; +use App\Support\Evidence\EvidenceCompletenessState; +use App\Support\Evidence\EvidenceSnapshotStatus; use App\Support\Filament\TablePaginationProfiles; use App\Support\Navigation\WorkspaceHubEnvironmentFilter; use App\Support\OperationRunLinks; +use App\Support\OperationRunOutcome; +use App\Support\OperationRunStatus; +use App\Support\ReviewPackStatus; use App\Support\Ui\ActionSurface\ActionSurfaceDeclaration; use App\Support\Ui\ActionSurface\Enums\ActionSurfaceInspectAffordance; use App\Support\Ui\ActionSurface\Enums\ActionSurfaceProfile; @@ -290,17 +298,48 @@ public function environmentFilterChip(): ?array public function evidenceDisclosurePayload(): array { $snapshots = $this->scopedSnapshots(); - $primarySnapshot = $snapshots->first(); - $primaryTenant = $primarySnapshot?->tenant; + $filteredTenant = $this->filteredTenant(); + $primarySnapshot = $this->primarySnapshotForScope($snapshots, $filteredTenant); + $primaryTenant = $filteredTenant instanceof ManagedEnvironment + ? $filteredTenant + : $primarySnapshot?->tenant; $primaryReviewPack = $primarySnapshot instanceof EvidenceSnapshot ? $this->latestReviewPackForSnapshot($primarySnapshot) : null; $primaryStoredReport = $primaryTenant instanceof ManagedEnvironment ? $this->latestStoredReportForTenant($primaryTenant) : null; + $primaryReview = $primarySnapshot instanceof EvidenceSnapshot + ? $this->latestEnvironmentReviewForSnapshot($primarySnapshot) + : null; $primaryOperationRun = $this->primaryOperationRun($primarySnapshot, $primaryReviewPack); - $primaryAction = $this->primaryEvidenceAction($primarySnapshot, $primaryReviewPack, $primaryStoredReport, $primaryOperationRun); - $primaryState = $this->primaryProofState($primarySnapshot, $primaryReviewPack, $primaryStoredReport, $primaryOperationRun); + $decisionState = $this->evidenceReviewPackDecisionState( + $primarySnapshot, + $primaryReviewPack, + $primaryStoredReport, + $primaryReview, + $primaryTenant, + $primaryOperationRun, + ); + $primaryAction = $this->primaryEvidenceAction( + $decisionState, + $primarySnapshot, + $primaryReviewPack, + $primaryStoredReport, + $primaryReview, + $primaryTenant, + $primaryOperationRun, + ); + $decisionCard = $this->evidenceReviewPackDecisionCard( + $decisionState, + $primarySnapshot, + $primaryReviewPack, + $primaryStoredReport, + $primaryReview, + $primaryTenant, + $primaryOperationRun, + $primaryAction, + ); return [ 'scope_label' => $this->evidenceScopeLabel(), @@ -308,12 +347,37 @@ public function evidenceDisclosurePayload(): array 'snapshot_count' => $snapshots->count(), 'primary_title' => $primaryTenant instanceof ManagedEnvironment ? $primaryTenant->name - : 'No evidence for this scope', + : 'Select an environment to evaluate evidence readiness', 'primary_summary' => $primarySnapshot instanceof EvidenceSnapshot ? $this->productSafeEvidenceReason($this->snapshotOutcome($primarySnapshot)->primaryReason) - : 'No evidence snapshot has been generated for the active workspace scope.', - 'primary_state' => $primaryState, + : ($primaryTenant instanceof ManagedEnvironment + ? 'No evidence snapshot has been generated for the active workspace scope.' + : 'Choose an authorized environment from the scope control before building a customer-safe proof path.'), + 'primary_proof_state' => $this->primaryProofState( + $primarySnapshot, + $primaryReviewPack, + $primaryStoredReport, + $primaryOperationRun, + ), + 'decision_card' => $decisionCard, + 'readiness_flow' => $this->evidenceReviewPackReadinessFlow( + $decisionState, + $primarySnapshot, + $primaryReviewPack, + $primaryStoredReport, + $primaryReview, + $primaryTenant, + ), 'primary_action' => $primaryAction, + 'review_pack_coverage' => $this->reviewPackCoverageSummary($primaryReviewPack), + 'proof_items' => $this->evidenceReviewPackProofItems( + $primarySnapshot, + $primaryReviewPack, + $primaryStoredReport, + $primaryReview, + $primaryTenant, + $primaryOperationRun, + ), 'cards' => [ $this->snapshotProofCard($primarySnapshot), $this->reviewPackProofCard($primaryReviewPack, $primarySnapshot), @@ -329,6 +393,668 @@ public function evidenceDisclosurePayload(): array ]; } + /** + * @param Collection $snapshots + */ + private function primarySnapshotForScope(Collection $snapshots, ?ManagedEnvironment $filteredTenant): ?EvidenceSnapshot + { + if ($filteredTenant instanceof ManagedEnvironment) { + return $this->latestEvidenceSnapshotForTenant($filteredTenant) ?? $snapshots->first(); + } + + return $snapshots->first(); + } + + private function latestEvidenceSnapshotForTenant(ManagedEnvironment $tenant): ?EvidenceSnapshot + { + return EvidenceSnapshot::query() + ->with([ + 'tenant', + 'operationRun', + 'reviewPacks.operationRun', + 'reviewPacks.environmentReview.currentExportReviewPack', + 'items', + ]) + ->where('workspace_id', (int) $tenant->workspace_id) + ->where('managed_environment_id', (int) $tenant->getKey()) + ->whereIn('status', [ + EvidenceSnapshotStatus::Queued->value, + EvidenceSnapshotStatus::Generating->value, + EvidenceSnapshotStatus::Active->value, + EvidenceSnapshotStatus::Failed->value, + EvidenceSnapshotStatus::Expired->value, + ]) + ->orderByRaw('COALESCE(generated_at, created_at) DESC') + ->first(); + } + + private function latestEnvironmentReviewForSnapshot(EvidenceSnapshot $snapshot): ?EnvironmentReview + { + return EnvironmentReview::query() + ->with(['currentExportReviewPack.operationRun', 'operationRun']) + ->where('workspace_id', (int) $snapshot->workspace_id) + ->where('managed_environment_id', (int) $snapshot->managed_environment_id) + ->where('evidence_snapshot_id', (int) $snapshot->getKey()) + ->orderByRaw('COALESCE(generated_at, created_at) DESC') + ->first(); + } + + private function evidenceReviewPackDecisionState( + ?EvidenceSnapshot $snapshot, + ?ReviewPack $reviewPack, + ?StoredReport $storedReport, + ?EnvironmentReview $review, + ?ManagedEnvironment $tenant, + ?OperationRun $operationRun, + ): string { + if (! $tenant instanceof ManagedEnvironment && ! $snapshot instanceof EvidenceSnapshot) { + return 'source_unavailable'; + } + + if (! $snapshot instanceof EvidenceSnapshot) { + return 'no_snapshot'; + } + + $snapshotStatus = (string) $snapshot->status; + + if (in_array($snapshotStatus, [EvidenceSnapshotStatus::Queued->value, EvidenceSnapshotStatus::Generating->value], true)) { + return 'snapshot_generating'; + } + + if ($snapshotStatus === EvidenceSnapshotStatus::Failed->value) { + return 'snapshot_failed'; + } + + if ($this->snapshotIsStale($snapshot)) { + return 'snapshot_stale'; + } + + if ($reviewPack instanceof ReviewPack) { + $packStatus = (string) $reviewPack->status; + + if (in_array($packStatus, [ReviewPackStatus::Queued->value, ReviewPackStatus::Generating->value], true)) { + return 'pack_generating'; + } + + if ($packStatus === ReviewPackStatus::Failed->value) { + return 'pack_failed'; + } + } + + if (! $storedReport instanceof StoredReport) { + return 'report_missing'; + } + + if (! $reviewPack instanceof ReviewPack) { + return 'pack_required'; + } + + if (! $this->reviewPackHasExportArtifact($reviewPack)) { + return 'export_unavailable'; + } + + if ($this->customerSafeOutputReady($review, $reviewPack)) { + return $this->canDownloadReviewPack($reviewPack, $tenant) ? 'export_available' : 'customer_safe_ready'; + } + + return $operationRun instanceof OperationRun && (string) $operationRun->outcome === OperationRunOutcome::Failed->value + ? 'pack_failed' + : 'customer_review_required'; + } + + /** + * @param array{label:string,url:string}|null $primaryAction + * @return array + */ + private function evidenceReviewPackDecisionCard( + string $state, + ?EvidenceSnapshot $snapshot, + ?ReviewPack $reviewPack, + ?StoredReport $storedReport, + ?EnvironmentReview $review, + ?ManagedEnvironment $tenant, + ?OperationRun $operationRun, + ?array $primaryAction, + ): array { + $base = match ($state) { + 'no_snapshot' => [ + 'status' => 'Evidence snapshot required', + 'tone' => 'warning', + 'reason' => 'No evidence snapshot is available for the selected review scope.', + 'impact' => 'Review pack output cannot be trusted or exported yet.', + ], + 'snapshot_generating' => [ + 'status' => 'Evidence generation in progress', + 'tone' => 'info', + 'reason' => 'Evidence snapshot generation is currently running.', + 'impact' => 'Review pack output is not final yet.', + ], + 'snapshot_failed' => [ + 'status' => 'Evidence generation failed', + 'tone' => 'danger', + 'reason' => 'Evidence snapshot generation ended with errors.', + 'impact' => 'Review pack output cannot be generated from this evidence yet.', + ], + 'snapshot_stale' => [ + 'status' => 'Evidence refresh required', + 'tone' => 'warning', + 'reason' => 'Evidence exists, but its freshness is outside the acceptable window or the snapshot is expired or stale.', + 'impact' => 'Review pack output should not be treated as current until evidence is refreshed.', + ], + 'report_missing' => [ + 'status' => 'Stored report required', + 'tone' => 'warning', + 'reason' => 'Evidence snapshot exists, but no stored report is available for this review output.', + 'impact' => 'Evidence is present but not yet packaged for consumption.', + ], + 'pack_required' => [ + 'status' => 'Review pack required', + 'tone' => 'warning', + 'reason' => 'Stored report exists, but a review pack has not been generated.', + 'impact' => 'Customer-safe delivery is not ready yet.', + ], + 'pack_generating' => [ + 'status' => 'Review pack generation in progress', + 'tone' => 'info', + 'reason' => 'Review pack generation is currently running.', + 'impact' => 'Customer output is not final yet.', + ], + 'pack_failed' => [ + 'status' => 'Review pack generation failed', + 'tone' => 'danger', + 'reason' => 'Review pack generation ended with errors.', + 'impact' => 'Customer-safe output cannot be generated from this pack yet.', + ], + 'customer_review_required' => [ + 'status' => 'Customer-safe review required', + 'tone' => 'warning', + 'reason' => 'A review pack exists, but customer-safe output has not been confirmed by repo-backed review/package readiness.', + 'impact' => 'Do not share the pack externally until it has been reviewed.', + ], + 'customer_safe_ready' => [ + 'status' => 'Customer-safe output ready', + 'tone' => 'success', + 'reason' => 'Review pack output is backed by a published review and its current export pack.', + 'impact' => 'Authorized users can use the pack; external sharing remains governed by workspace policy.', + ], + 'export_available' => [ + 'status' => 'Review pack export available', + 'tone' => 'success', + 'reason' => 'A generated export artifact is available for authorized download.', + 'impact' => 'Download is available from this surface; external sharing remains governed by workspace policy.', + ], + 'export_unavailable' => [ + 'status' => 'Export unavailable', + 'tone' => 'warning', + 'reason' => 'No generated export artifact is available, the pack is not ready, the file is missing or expired, or external delivery is not configured.', + 'impact' => 'Evidence package cannot be downloaded or delivered from this surface yet.', + ], + default => [ + 'status' => 'Evidence source unavailable', + 'tone' => 'gray', + 'reason' => 'No repo-backed source data is selected for this proof scope.', + 'impact' => 'No customer or auditor consumption decision should rely on this surface yet.', + ], + }; + + return $base + [ + 'question' => 'Is this evidence package ready for customer or auditor consumption?', + 'statusLabel' => 'Status', + 'reasonLabel' => 'Reason', + 'impactLabel' => 'Impact', + 'nextActionLabel' => 'Primary next action', + 'evidenceLabel' => 'Evidence path', + 'evidence' => $this->decisionEvidenceSummary($snapshot, $reviewPack, $storedReport, $review, $tenant, $operationRun), + 'actionLabel' => $primaryAction['label'] ?? ($state === 'source_unavailable' ? 'Select environment scope' : 'No authorized action available'), + 'actionUrl' => $primaryAction['url'] ?? null, + 'helperText' => $this->decisionActionHelper($state, $tenant, $primaryAction), + 'actionDescription' => $this->decisionActionDescription($state, $primaryAction), + ]; + } + + /** + * @return list> + */ + private function evidenceReviewPackReadinessFlow( + string $state, + ?EvidenceSnapshot $snapshot, + ?ReviewPack $reviewPack, + ?StoredReport $storedReport, + ?EnvironmentReview $review, + ?ManagedEnvironment $tenant, + ): array { + $sourceState = $tenant instanceof ManagedEnvironment ? 'Available' : 'Unavailable'; + $snapshotState = $this->snapshotFlowState($snapshot); + $storedReportState = $this->storedReportFlowState($snapshot, $storedReport); + $reviewPackState = $this->reviewPackFlowState($snapshot, $storedReport, $reviewPack); + $customerSafeState = $this->customerSafeFlowState($snapshot, $reviewPack, $review); + $exportState = $this->exportFlowState($snapshot, $reviewPack, $tenant); + + return [ + $this->flowStep('Source data selected', $sourceState, $sourceState === 'Available' ? 'Environment scope is established from the workspace context.' : 'Select an authorized environment before building customer-safe evidence.', $this->flowTone($sourceState), $state === 'source_unavailable'), + $this->flowStep('Evidence snapshot', $snapshotState, $this->snapshotFlowDescription($snapshotState), $this->flowTone($snapshotState), in_array($state, ['no_snapshot', 'snapshot_generating', 'snapshot_failed', 'snapshot_stale'], true)), + $this->flowStep('Stored report', $storedReportState, $this->storedReportFlowDescription($storedReportState), $this->flowTone($storedReportState), $state === 'report_missing'), + $this->flowStep('Review pack', $reviewPackState, $this->reviewPackFlowDescription($reviewPackState), $this->flowTone($reviewPackState), in_array($state, ['pack_required', 'pack_generating', 'pack_failed'], true)), + $this->flowStep('Customer-safe output', $customerSafeState, $this->customerSafeFlowDescription($customerSafeState), $this->flowTone($customerSafeState), $state === 'customer_review_required'), + $this->flowStep('Export / delivery', $exportState, $this->exportFlowDescription($exportState), $this->flowTone($exportState), in_array($state, ['export_available', 'export_unavailable'], true)), + ]; + } + + /** + * @return array{description:string,items:list} + */ + private function reviewPackCoverageSummary(?ReviewPack $reviewPack): array + { + if (! $reviewPack instanceof ReviewPack) { + return [ + 'description' => 'Review pack coverage details are not available for this artifact.', + 'items' => [], + ]; + } + + $summary = is_array($reviewPack->summary) ? $reviewPack->summary : []; + $items = []; + + foreach ([ + 'finding_count' => 'Findings included', + 'report_count' => 'Reports included', + 'operation_count' => 'Operations included', + 'section_count' => 'Review sections', + ] as $key => $label) { + if (array_key_exists($key, $summary) && is_numeric($summary[$key])) { + $items[] = ['label' => $label, 'value' => (string) ((int) $summary[$key])]; + } + } + + $requiredDimensions = data_get($summary, 'evidence_resolution.required_dimensions'); + + if (is_array($requiredDimensions)) { + $items[] = ['label' => 'Evidence dimensions', 'value' => (string) count($requiredDimensions)]; + } + + if (filled($reviewPack->file_path)) { + $items[] = ['label' => 'Generated files', 'value' => '1']; + } + + return [ + 'description' => $items === [] + ? 'Review pack coverage details are not available for this artifact.' + : 'Coverage values are derived from the generated review-pack summary and file metadata.', + 'items' => $items, + ]; + } + + /** + * @return list> + */ + private function evidenceReviewPackProofItems( + ?EvidenceSnapshot $snapshot, + ?ReviewPack $reviewPack, + ?StoredReport $storedReport, + ?EnvironmentReview $review, + ?ManagedEnvironment $tenant, + ?OperationRun $operationRun, + ): array { + $operationDescription = $operationRun instanceof OperationRun + ? sprintf( + '%s · %s · Started %s · Completed %s · Requested by %s', + (string) $operationRun->type, + Str::headline((string) $operationRun->outcome), + $operationRun->started_at?->toDateTimeString() ?? '—', + $operationRun->completed_at?->toDateTimeString() ?? '—', + (string) ($operationRun->initiator_name ?: 'Unknown'), + ) + : 'Operation proof unavailable. No generation operation is linked to this artifact.'; + + return [ + $this->proofItem('Source data', $tenant instanceof ManagedEnvironment ? 'Available' : 'Unavailable', $tenant instanceof ManagedEnvironment ? 'Workspace and environment scope are established.' : 'No environment scope is selected.', $tenant instanceof ManagedEnvironment ? 'success' : 'gray'), + $this->proofItemFromCard($this->snapshotProofCard($snapshot)), + $this->proofItemFromCard($this->storedReportProofCard($storedReport, $tenant)), + $this->proofItemFromCard($this->reviewPackProofCard($reviewPack, $snapshot)), + $this->proofItem('Operation proof', $operationRun instanceof OperationRun ? $this->operationProofState($operationRun) : 'Unavailable', $operationDescription, $operationRun instanceof OperationRun ? $this->operationProofTone($operationRun) : 'gray', $operationRun instanceof OperationRun ? OperationRunLinks::tenantlessView($operationRun) : null, $operationRun instanceof OperationRun ? OperationRunLinks::openLabel() : null), + $this->proofItem('Export artifact', $this->exportFlowState($snapshot, $reviewPack, $tenant), $this->exportProofDescription($reviewPack, $tenant), $this->flowTone($this->exportFlowState($snapshot, $reviewPack, $tenant)), $this->canDownloadReviewPack($reviewPack, $tenant) ? app(ReviewPackService::class)->generateDownloadUrl($reviewPack) : null, $this->canDownloadReviewPack($reviewPack, $tenant) ? 'Download export' : null), + $this->proofItem('Customer-safe state', $this->customerSafeFlowState($snapshot, $reviewPack, $review), $this->customerSafeFlowDescription($this->customerSafeFlowState($snapshot, $reviewPack, $review)), $this->flowTone($this->customerSafeFlowState($snapshot, $reviewPack, $review)), $tenant instanceof ManagedEnvironment ? CustomerReviewWorkspace::environmentFilterUrl($tenant) : null, $tenant instanceof ManagedEnvironment ? 'Open customer workspace' : null), + $this->proofItem('Diagnostics', 'Collapsed', 'Raw report metadata, raw evidence payloads, generation diagnostics, export diagnostics, provider diagnostics, stack traces, and internal exceptions stay collapsed by default.', 'gray'), + ]; + } + + /** + * @return array{label:string,state:string,description:string,tone:string,currentBlocker:bool} + */ + private function flowStep(string $label, string $state, string $description, string $tone, bool $currentBlocker = false): array + { + return [ + 'label' => $label, + 'state' => $state, + 'description' => $description, + 'tone' => $tone, + 'currentBlocker' => $currentBlocker, + ]; + } + + private function snapshotFlowState(?EvidenceSnapshot $snapshot): string + { + if (! $snapshot instanceof EvidenceSnapshot) { + return 'Missing'; + } + + return match ((string) $snapshot->status) { + EvidenceSnapshotStatus::Queued->value, EvidenceSnapshotStatus::Generating->value => 'Generating', + EvidenceSnapshotStatus::Failed->value => 'Failed', + default => $this->snapshotIsStale($snapshot) ? 'Stale' : 'Available', + }; + } + + private function storedReportFlowState(?EvidenceSnapshot $snapshot, ?StoredReport $storedReport): string + { + if (! $snapshot instanceof EvidenceSnapshot || in_array($this->snapshotFlowState($snapshot), ['Generating', 'Failed'], true)) { + return 'Unavailable'; + } + + return $storedReport instanceof StoredReport ? 'Available' : 'Missing'; + } + + private function reviewPackFlowState(?EvidenceSnapshot $snapshot, ?StoredReport $storedReport, ?ReviewPack $reviewPack): string + { + if ($reviewPack instanceof ReviewPack) { + return match ((string) $reviewPack->status) { + ReviewPackStatus::Queued->value, ReviewPackStatus::Generating->value => 'Generating', + ReviewPackStatus::Failed->value => 'Failed', + ReviewPackStatus::Ready->value => 'Available', + default => 'Unavailable', + }; + } + + if (! $snapshot instanceof EvidenceSnapshot) { + return 'Unavailable'; + } + + return $storedReport instanceof StoredReport ? 'Required' : 'Unavailable'; + } + + private function customerSafeFlowState(?EvidenceSnapshot $snapshot, ?ReviewPack $reviewPack, ?EnvironmentReview $review): string + { + if (! $snapshot instanceof EvidenceSnapshot) { + return 'Not ready'; + } + + if (! $reviewPack instanceof ReviewPack || ! $reviewPack->isReady()) { + return 'Not ready'; + } + + return $this->customerSafeOutputReady($review, $reviewPack) ? 'Ready' : 'Needs review'; + } + + private function exportFlowState(?EvidenceSnapshot $snapshot, ?ReviewPack $reviewPack, ?ManagedEnvironment $tenant): string + { + if (! $snapshot instanceof EvidenceSnapshot || ! $reviewPack instanceof ReviewPack) { + return 'Unavailable'; + } + + if ((string) $reviewPack->status === ReviewPackStatus::Failed->value) { + return 'Failed'; + } + + if (! $reviewPack->isReady()) { + return 'Unavailable'; + } + + return $this->reviewPackHasExportArtifact($reviewPack) && $this->canDownloadReviewPack($reviewPack, $tenant) + ? 'Available' + : 'Required'; + } + + private function snapshotFlowDescription(string $state): string + { + return match ($state) { + 'Available' => 'Snapshot proof exists.', + 'Missing' => 'No snapshot in scope.', + 'Generating' => 'Generation is running.', + 'Failed' => 'Generation failed.', + 'Stale' => 'Evidence is stale or expired.', + default => 'Evidence snapshot state is unavailable.', + }; + } + + private function storedReportFlowDescription(string $state): string + { + return match ($state) { + 'Available' => 'Stored report exists.', + 'Missing' => 'No report for this output.', + default => 'Depends on snapshot availability.', + }; + } + + private function reviewPackFlowDescription(string $state): string + { + return match ($state) { + 'Available' => 'Review pack exists.', + 'Required' => 'Generate a review pack.', + 'Generating' => 'Generation is running.', + 'Failed' => 'Generation failed.', + default => 'Blocked by earlier proof.', + }; + } + + private function customerSafeFlowDescription(string $state): string + { + return match ($state) { + 'Ready' => 'Published review backs current export pack.', + 'Needs review' => 'Readiness is not confirmed.', + 'Not ready' => 'Customer-safe output is not ready.', + default => 'Customer-safe output readiness is unavailable.', + }; + } + + private function exportFlowDescription(string $state): string + { + return match ($state) { + 'Available' => 'Authorized download is available.', + 'Required' => 'Export file is missing or unauthorized.', + 'Failed' => 'The linked review-pack export failed.', + default => 'No generated export is available.', + }; + } + + private function flowTone(string $state): string + { + return match ($state) { + 'Available', 'Ready', 'Generated' => 'success', + 'Generating' => 'info', + 'Missing', 'Required', 'Stale', 'Needs review', 'Not ready' => 'warning', + 'Failed' => 'danger', + default => 'gray', + }; + } + + private function snapshotIsStale(EvidenceSnapshot $snapshot): bool + { + if ((string) $snapshot->status === EvidenceSnapshotStatus::Expired->value) { + return true; + } + + if ($snapshot->expires_at instanceof \DateTimeInterface && $snapshot->expires_at->isPast()) { + return true; + } + + return $snapshot->completenessState() === EvidenceCompletenessState::Stale + || (int) data_get($snapshot->summary ?? [], 'stale_dimensions', 0) > 0; + } + + private function reviewPackHasExportArtifact(?ReviewPack $reviewPack): bool + { + if (! $reviewPack instanceof ReviewPack || ! $reviewPack->isReady()) { + return false; + } + + if ($reviewPack->expires_at instanceof \DateTimeInterface && $reviewPack->expires_at->isPast()) { + return false; + } + + return filled($reviewPack->file_disk) && filled($reviewPack->file_path); + } + + private function customerSafeOutputReady(?EnvironmentReview $review, ?ReviewPack $reviewPack): bool + { + if (! $review instanceof EnvironmentReview || ! $reviewPack instanceof ReviewPack) { + return false; + } + + return (string) $review->status === EnvironmentReviewStatus::Published->value + && (int) $review->current_export_review_pack_id === (int) $reviewPack->getKey() + && $this->reviewPackHasExportArtifact($reviewPack); + } + + private function canDownloadReviewPack(?ReviewPack $reviewPack, ?ManagedEnvironment $tenant): bool + { + $user = auth()->user(); + + return $reviewPack instanceof ReviewPack + && $tenant instanceof ManagedEnvironment + && $user instanceof User + && $this->reviewPackHasExportArtifact($reviewPack) + && (int) $reviewPack->managed_environment_id === (int) $tenant->getKey() + && $user->can(Capabilities::REVIEW_PACK_VIEW, $tenant); + } + + /** + * @return array + */ + private function proofItem( + string $label, + string $state, + string $description, + string $color, + ?string $url = null, + ?string $actionLabel = null, + ): array { + return [ + 'label' => $label, + 'state' => $state, + 'description' => $description, + 'color' => $color, + 'url' => $url, + 'actionLabel' => $actionLabel, + ]; + } + + /** + * @param array $card + * @return array + */ + private function proofItemFromCard(array $card): array + { + return $this->proofItem( + (string) $card['label'], + (string) ($card['path_state'] ?? $card['value']), + (string) $card['description'], + (string) $card['color'], + is_string($card['url'] ?? null) ? $card['url'] : null, + is_string($card['url'] ?? null) ? 'Open proof' : null, + ); + } + + private function operationProofState(OperationRun $operationRun): string + { + if ((string) $operationRun->outcome === OperationRunOutcome::Failed->value) { + return 'Failed'; + } + + return in_array((string) $operationRun->status, [OperationRunStatus::Queued->value, OperationRunStatus::Running->value], true) + ? 'Generating' + : 'Available'; + } + + private function operationProofTone(OperationRun $operationRun): string + { + return $this->flowTone($this->operationProofState($operationRun)); + } + + private function exportProofDescription(?ReviewPack $reviewPack, ?ManagedEnvironment $tenant): string + { + if (! $reviewPack instanceof ReviewPack) { + return 'No review pack is linked, so no export artifact is available.'; + } + + if (! $this->reviewPackHasExportArtifact($reviewPack)) { + return 'External delivery is not configured or the review pack file metadata is missing, expired, or not ready.'; + } + + return $this->canDownloadReviewPack($reviewPack, $tenant) + ? 'Signed download is available for authorized users.' + : 'A file-backed export exists, but this user cannot download it from the current scope.'; + } + + private function decisionEvidenceSummary( + ?EvidenceSnapshot $snapshot, + ?ReviewPack $reviewPack, + ?StoredReport $storedReport, + ?EnvironmentReview $review, + ?ManagedEnvironment $tenant, + ?OperationRun $operationRun, + ): string { + $parts = []; + $parts[] = $tenant instanceof ManagedEnvironment ? 'Environment scope selected' : 'No environment selected'; + $parts[] = 'Snapshot: '.$this->snapshotFlowState($snapshot); + $parts[] = 'Stored report: '.$this->storedReportFlowState($snapshot, $storedReport); + $parts[] = 'Review pack: '.$this->reviewPackFlowState($snapshot, $storedReport, $reviewPack); + $parts[] = 'Customer-safe output: '.$this->customerSafeFlowState($snapshot, $reviewPack, $review); + $parts[] = 'Export: '.$this->exportFlowState($snapshot, $reviewPack, $tenant); + + if ($operationRun instanceof OperationRun) { + $parts[] = OperationRunLinks::identifier($operationRun); + } + + return implode(' · ', $parts); + } + + /** + * @param array{label:string,url:string}|null $primaryAction + */ + private function decisionActionHelper(string $state, ?ManagedEnvironment $tenant, ?array $primaryAction): ?string + { + if ($primaryAction !== null) { + return null; + } + + if (! $tenant instanceof ManagedEnvironment) { + return 'Use the Environment scope control in the top bar to choose an authorized environment.'; + } + + return match ($state) { + 'no_snapshot' => 'Evidence generation requires evidence management capability.', + 'pack_required' => 'Review pack generation requires review-pack management capability.', + 'export_available' => 'Download requires review-pack view capability.', + default => 'No authorized repo-backed primary action is available for this state.', + }; + } + + /** + * @param array{label:string,url:string}|null $primaryAction + */ + private function decisionActionDescription(string $state, ?array $primaryAction): string + { + if ($primaryAction === null) { + return $state === 'source_unavailable' + ? 'Choose an environment before evaluating evidence, reports, review packs, or export readiness.' + : 'No capability-backed action is available for the current state.'; + } + + return match ($state) { + 'no_snapshot' => 'Creates the evidence snapshot required before reports or review packs can be trusted.', + 'snapshot_generating', 'pack_generating' => 'Opens the linked operation so progress can be verified before using the output.', + 'snapshot_failed', 'pack_failed' => 'Opens failed operation proof before retrying or sharing any output.', + 'snapshot_stale' => 'Opens the stale snapshot so evidence can be refreshed from the correct scope.', + 'report_missing' => 'Opens snapshot proof; report generation remains on the supported report surface.', + 'pack_required' => 'Opens the environment review-pack surface where generation stays capability-gated.', + 'customer_review_required' => 'Opens the customer review workspace before any external sharing decision.', + 'customer_safe_ready' => 'Opens the customer review workspace that backs the readiness claim.', + 'export_available' => 'Downloads the signed export for authorized users.', + 'export_unavailable' => 'Opens review-pack proof to inspect missing, expired, or unauthorized export state.', + default => 'Opens the most relevant repo-backed proof surface for this state.', + }; + } + /** * @return Collection */ @@ -382,7 +1108,7 @@ private function evidenceScopeLabel(): string private function latestReviewPackForSnapshot(EvidenceSnapshot $snapshot): ?ReviewPack { $reviewPack = $snapshot->reviewPacks - ->sortByDesc(fn (ReviewPack $pack): int => $pack->generated_at?->getTimestamp() ?? 0) + ->sortByDesc(fn (ReviewPack $pack): int => $pack->generated_at?->getTimestamp() ?? $pack->created_at?->getTimestamp() ?? 0) ->first(); return $reviewPack instanceof ReviewPack ? $reviewPack : null; @@ -419,6 +1145,17 @@ private function latestStoredReportForTenant(ManagedEnvironment $tenant): ?Store private function primaryOperationRun(?EvidenceSnapshot $snapshot, ?ReviewPack $reviewPack): ?OperationRun { + $run = $reviewPack?->operationRun; + + if ( + $reviewPack instanceof ReviewPack + && $run instanceof OperationRun + && in_array((string) $reviewPack->status, [ReviewPackStatus::Queued->value, ReviewPackStatus::Generating->value, ReviewPackStatus::Failed->value], true) + && $this->canViewOperationRun($run) + ) { + return $run; + } + $run = $snapshot?->operationRun; if ($run instanceof OperationRun && $this->canViewOperationRun($run)) { @@ -442,8 +1179,94 @@ private function canViewOperationRun(OperationRun $run): bool /** * @return array{label:string,url:string}|null */ - private function primaryEvidenceAction(?EvidenceSnapshot $snapshot, ?ReviewPack $reviewPack, ?StoredReport $storedReport, ?OperationRun $operationRun): ?array - { + private function primaryEvidenceAction( + string $state, + ?EvidenceSnapshot $snapshot, + ?ReviewPack $reviewPack, + ?StoredReport $storedReport, + ?EnvironmentReview $review, + ?ManagedEnvironment $tenant, + ?OperationRun $operationRun, + ): ?array { + if (in_array($state, ['snapshot_generating', 'snapshot_failed', 'pack_generating', 'pack_failed'], true) && $operationRun instanceof OperationRun) { + return [ + 'label' => $state === 'snapshot_generating' || $state === 'pack_generating' + ? 'View operation progress' + : 'Review operation', + 'url' => OperationRunLinks::tenantlessView($operationRun), + ]; + } + + if ($state === 'no_snapshot' && $tenant instanceof ManagedEnvironment && $this->canManageEvidence($tenant)) { + return [ + 'label' => 'Generate evidence snapshot', + 'url' => EvidenceSnapshotResource::getUrl('index', tenant: $tenant, panel: 'admin'), + ]; + } + + if ($state === 'snapshot_stale' && $snapshot instanceof EvidenceSnapshot && $snapshot->tenant instanceof ManagedEnvironment && $this->canManageEvidence($snapshot->tenant)) { + return [ + 'label' => 'Refresh evidence snapshot', + 'url' => EvidenceSnapshotResource::getUrl('view', ['record' => $snapshot], tenant: $snapshot->tenant, panel: 'admin'), + ]; + } + + if ($state === 'pack_required' && $tenant instanceof ManagedEnvironment && $this->canManageReviewPacks($tenant)) { + return [ + 'label' => 'Generate review pack', + 'url' => ReviewPackResource::getUrl('index', tenant: $tenant, panel: 'admin'), + ]; + } + + if ($snapshot instanceof EvidenceSnapshot && $this->isEmptyEvidenceSnapshot($snapshot) && $snapshot->tenant instanceof ManagedEnvironment) { + return [ + 'label' => 'Open evidence snapshot', + 'url' => EvidenceSnapshotResource::getUrl('view', ['record' => $snapshot], tenant: $snapshot->tenant, panel: 'admin'), + ]; + } + + if ($state === 'customer_review_required' && $tenant instanceof ManagedEnvironment) { + return [ + 'label' => 'Review customer output', + 'url' => CustomerReviewWorkspace::environmentFilterUrl($tenant), + ]; + } + + if ($state === 'export_available' && $this->canDownloadReviewPack($reviewPack, $tenant)) { + return [ + 'label' => 'Download export', + 'url' => app(ReviewPackService::class)->generateDownloadUrl($reviewPack), + ]; + } + + if ($state === 'customer_safe_ready' && $tenant instanceof ManagedEnvironment) { + return [ + 'label' => 'Open customer workspace', + 'url' => CustomerReviewWorkspace::environmentFilterUrl($tenant), + ]; + } + + if ($state === 'report_missing' && $snapshot instanceof EvidenceSnapshot && $snapshot->tenant instanceof ManagedEnvironment) { + return [ + 'label' => 'Open evidence snapshot', + 'url' => EvidenceSnapshotResource::getUrl('view', ['record' => $snapshot], tenant: $snapshot->tenant, panel: 'admin'), + ]; + } + + if ($state === 'export_unavailable' && $reviewPack instanceof ReviewPack && $reviewPack->tenant instanceof ManagedEnvironment) { + return [ + 'label' => 'Open review pack', + 'url' => ReviewPackResource::getUrl('view', ['record' => $reviewPack], tenant: $reviewPack->tenant, panel: 'admin'), + ]; + } + + if ($review instanceof EnvironmentReview && $tenant instanceof ManagedEnvironment) { + return [ + 'label' => 'Open customer workspace', + 'url' => CustomerReviewWorkspace::environmentFilterUrl($tenant), + ]; + } + if ($snapshot instanceof EvidenceSnapshot && $snapshot->tenant instanceof ManagedEnvironment) { return [ 'label' => 'Open evidence snapshot', @@ -475,6 +1298,20 @@ private function primaryEvidenceAction(?EvidenceSnapshot $snapshot, ?ReviewPack return null; } + private function canManageEvidence(ManagedEnvironment $tenant): bool + { + $user = auth()->user(); + + return $user instanceof User && $user->can(Capabilities::EVIDENCE_MANAGE, $tenant); + } + + private function canManageReviewPacks(ManagedEnvironment $tenant): bool + { + $user = auth()->user(); + + return $user instanceof User && $user->can(Capabilities::REVIEW_PACK_MANAGE, $tenant); + } + /** * @return array */ @@ -828,6 +1665,7 @@ private function latestAccessibleSnapshots(): Collection 'tenant', 'operationRun', 'reviewPacks.operationRun', + 'reviewPacks.environmentReview.currentExportReviewPack', 'items', ]) ->where('workspace_id', $this->workspaceId()) diff --git a/apps/platform/resources/views/filament/pages/monitoring/evidence-overview.blade.php b/apps/platform/resources/views/filament/pages/monitoring/evidence-overview.blade.php index 60781ee9..93e84e08 100644 --- a/apps/platform/resources/views/filament/pages/monitoring/evidence-overview.blade.php +++ b/apps/platform/resources/views/filament/pages/monitoring/evidence-overview.blade.php @@ -2,6 +2,15 @@ @php $environmentFilterChip = $this->environmentFilterChip(); $payload = $this->evidenceDisclosurePayload(); + $decisionCard = $payload['decision_card']; + $statusBadgeClasses = static fn (?string $tone): string => 'inline-flex items-center rounded-md border px-2 py-0.5 text-left text-xs font-medium leading-5 whitespace-normal break-words '.match ($tone) { + 'danger' => 'border-danger-200 bg-danger-50 text-danger-700 dark:border-danger-800 dark:bg-danger-500/10 dark:text-danger-300', + 'success' => 'border-success-200 bg-success-50 text-success-700 dark:border-success-800 dark:bg-success-500/10 dark:text-success-300', + 'warning' => 'border-warning-200 bg-warning-50 text-warning-700 dark:border-warning-800 dark:bg-warning-500/10 dark:text-warning-300', + 'info' => 'border-info-200 bg-info-50 text-info-700 dark:border-info-800 dark:bg-info-500/10 dark:text-info-300', + 'primary' => 'border-primary-200 bg-primary-50 text-primary-700 dark:border-primary-800 dark:bg-primary-500/10 dark:text-primary-300', + default => 'border-gray-200 bg-gray-50 text-gray-700 dark:border-white/10 dark:bg-white/5 dark:text-gray-200', + }; @endphp
@@ -18,11 +27,11 @@

- What proof is available for this scope? + {{ $decisionCard['question'] }}

- {{ $payload['scope_description'] }} + What proof is available for this scope? {{ $payload['scope_description'] }}

@@ -40,97 +49,162 @@
-
-
-
- Primary proof path +
+
+
+
+ Primary proof path +
+ + {{ $decisionCard['status'] }} +
-

- {{ $payload['primary_title'] }} -

+
+

+ {{ $payload['primary_title'] }} +

-

- {{ $payload['primary_summary'] }} -

+

+ {{ $payload['primary_summary'] }} +

+
- @if ($payload['primary_state'] !== null) -
- - {{ $payload['primary_state']['label'] }} - + @if ($payload['primary_proof_state'] !== null) +
+
+ {{ $payload['primary_proof_state']['reason'] }} +
-
-
-
- Reason -
-

- {{ $payload['primary_state']['reason'] }} -

-
- -
-
- Impact -
-

- {{ $payload['primary_state']['impact'] }} -

-
+
+ {{ $payload['primary_proof_state']['impact'] }}
@endif + +
+
+
+ {{ $decisionCard['statusLabel'] }} +
+
+ {{ $decisionCard['status'] }} +
+
+ +
+
+ {{ $decisionCard['reasonLabel'] }} +
+
+ {{ $decisionCard['reason'] }} +
+
+ +
+
+ {{ $decisionCard['impactLabel'] }} +
+
+ {{ $decisionCard['impact'] }} +
+
+ +
+
+ {{ $decisionCard['evidenceLabel'] }} +
+
+ {{ $decisionCard['evidence'] }} +
+
+ +
+
+ {{ $decisionCard['nextActionLabel'] }} +
+
+
+ {{ $decisionCard['actionLabel'] }} +
+

+ {{ $decisionCard['actionDescription'] }} +

+
+
+
- @if ($payload['primary_action']) + @if ($decisionCard['actionUrl']) - {{ $payload['primary_action']['label'] }} + {{ $decisionCard['actionLabel'] }} @else - - No open proof action - +
+ + {{ $decisionCard['actionLabel'] }} + + + @if ($decisionCard['helperText']) +

+ {{ $decisionCard['helperText'] }} +

+ @endif +
@endif
-
- @foreach ($payload['cards'] as $card) -
-
-
- {{ $card['label'] }} + @if (! empty($payload['readiness_flow'])) +
+ @include('filament.components.product-process-flow-horizontal', [ + 'title' => 'Evidence readiness flow', + 'subtitle' => 'Customer-safe evidence requires source data, evidence snapshot, stored report, review pack, and export readiness.', + 'ariaLabel' => 'Evidence readiness pipeline', + 'steps' => $payload['readiness_flow'], + 'flowTestId' => 'evidence-readiness-flow', + 'stepTestId' => 'evidence-readiness-step', + 'connectorTestId' => 'evidence-readiness-connector', + 'badgeTestId' => 'evidence-review-pack-status-badge', + 'statusBadgeClasses' => $statusBadgeClasses, + ]) +
+ @endif + +
+
+

+ Review pack contents / coverage +

+

+ Repo-backed values only. +

+
+ +

+ {{ $payload['review_pack_coverage']['description'] }} +

+ + @if (! empty($payload['review_pack_coverage']['items'])) +
+ @foreach ($payload['review_pack_coverage']['items'] as $coverageItem) +
+
+ {{ $coverageItem['label'] }} +
+
+ {{ $coverageItem['value'] }} +
- -
- - {{ $card['value'] }} - -
- -

- {{ $card['description'] }} -

- -
- {{ $card['meta'] }} - - @if ($card['url']) - - Open - - @endif -
-
+ @endforeach
- @endforeach + @endif
@@ -138,16 +212,16 @@ class="shrink-0 self-start whitespace-nowrap"

- Evidence path + Evidence proof

- Only repo-supported proof sources are listed. Missing pieces stay explicit. + Evidence path rows list only repo-supported proof sources. Missing pieces stay explicit.

- @foreach ($payload['path_items'] as $item) -
+ @foreach ($payload['proof_items'] as $item) +
@@ -157,19 +231,14 @@ class="shrink-0 self-start whitespace-nowrap" {{ $item['description'] }}

- + {{ $item['state'] }} - +
@if ($item['url']) - Open proof + {{ $item['actionLabel'] ?? 'Open proof' }} @endif
diff --git a/apps/platform/tests/Browser/Spec337EvidenceReviewPackProductFlowSmokeTest.php b/apps/platform/tests/Browser/Spec337EvidenceReviewPackProductFlowSmokeTest.php new file mode 100644 index 00000000..042dd39b --- /dev/null +++ b/apps/platform/tests/Browser/Spec337EvidenceReviewPackProductFlowSmokeTest.php @@ -0,0 +1,352 @@ +browser()->timeout(60_000); + +function spec337BrowserScreenshotName(string $name): string +{ + return 'spec337-evidence-review-pack-'.$name; +} + +function spec337CopyBrowserScreenshot(string $name): void +{ + $filename = spec337BrowserScreenshotName($name).'.png'; + $source = base_path('tests/Browser/Screenshots/'.$filename); + $targetDirectory = repo_path('specs/337-evidence-review-pack-product-process-flow-alignment/artifacts/screenshots'); + + if (! is_dir($targetDirectory)) { + @mkdir($targetDirectory, 0755, true); + } + + if (! is_file($source)) { + $source = \Pest\Browser\Support\Screenshot::path($filename); + } + + for ($attempt = 0; $attempt < 10 && ! is_file($source); $attempt++) { + usleep(100_000); + clearstatcache(true, $source); + } + + if (is_file($source) && is_dir($targetDirectory) && is_writable($targetDirectory)) { + @copy($source, $targetDirectory.DIRECTORY_SEPARATOR.$name.'.png'); + } +} + +function spec337AuthenticateBrowser(mixed $test, User $user, ManagedEnvironment $environment): void +{ + $workspaceId = (int) $environment->workspace_id; + + $test->actingAs($user)->withSession([ + WorkspaceContext::SESSION_KEY => $workspaceId, + WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => [ + (string) $workspaceId => (int) $environment->getKey(), + ], + ]); + + session()->put(WorkspaceContext::SESSION_KEY, $workspaceId); + session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [ + (string) $workspaceId => (int) $environment->getKey(), + ]); + + setAdminPanelContext($environment); +} + +function spec337BrowserEnvironmentFor(User $user, ManagedEnvironment $baseEnvironment, string $name): ManagedEnvironment +{ + $environment = ManagedEnvironment::factory()->active()->create([ + 'workspace_id' => (int) $baseEnvironment->workspace_id, + 'name' => $name, + ]); + + createUserWithTenant(tenant: $environment, user: $user, role: 'owner', workspaceRole: 'manager'); + + return $environment; +} + +it('Spec337 smokes Evidence Review Pack product process flow states', function (): void { + [$user, $missingEnvironment] = createUserWithTenant(role: 'owner', workspaceRole: 'manager'); + $missingEnvironment->forceFill(['name' => 'Spec337 Evidence Snapshot Required'])->save(); + + $generatingEnvironment = spec337BrowserEnvironmentFor($user, $missingEnvironment, 'Spec337 Evidence Generating'); + $generatingRun = OperationRun::factory()->forTenant($generatingEnvironment)->create([ + 'type' => OperationRunType::EvidenceSnapshotGenerate->value, + 'status' => OperationRunStatus::Running->value, + 'outcome' => OperationRunOutcome::Pending->value, + 'started_at' => now()->subMinute(), + 'initiator_name' => 'Spec337 Browser Operator', + ]); + spec337BrowserCreateEvidenceSnapshot($generatingEnvironment, [ + 'operation_run_id' => (int) $generatingRun->getKey(), + 'status' => EvidenceSnapshotStatus::Generating->value, + 'generated_at' => null, + ]); + + $storedReportRequiredEnvironment = spec337BrowserEnvironmentFor($user, $missingEnvironment, 'Spec337 Stored Report Required'); + spec337BrowserCreateEvidenceSnapshot($storedReportRequiredEnvironment); + + $reviewPackRequiredEnvironment = spec337BrowserEnvironmentFor($user, $missingEnvironment, 'Spec337 Review Pack Required'); + spec337BrowserCreateEvidenceSnapshot($reviewPackRequiredEnvironment); + spec337BrowserCreateStoredReport($reviewPackRequiredEnvironment); + + $availableEnvironment = spec337BrowserEnvironmentFor($user, $missingEnvironment, 'Spec337 Review Pack Available'); + $availableSnapshot = spec337BrowserCreateEvidenceSnapshot($availableEnvironment); + spec337BrowserCreateStoredReport($availableEnvironment); + spec337BrowserCreatePublishedReviewWithReadyPack($availableEnvironment, $user, $availableSnapshot); + + $exportUnavailableEnvironment = spec337BrowserEnvironmentFor($user, $missingEnvironment, 'Spec337 Export Unavailable'); + $exportUnavailableSnapshot = spec337BrowserCreateEvidenceSnapshot($exportUnavailableEnvironment); + spec337BrowserCreateStoredReport($exportUnavailableEnvironment); + ReviewPack::factory()->ready()->create([ + 'managed_environment_id' => (int) $exportUnavailableEnvironment->getKey(), + 'workspace_id' => (int) $exportUnavailableEnvironment->workspace_id, + 'evidence_snapshot_id' => (int) $exportUnavailableSnapshot->getKey(), + 'file_disk' => null, + 'file_path' => null, + 'file_size' => null, + 'sha256' => null, + ]); + + spec337AuthenticateBrowser($this, $user, $missingEnvironment); + + $page = visit(route('admin.evidence.overview', [ + 'environment_id' => (int) $missingEnvironment->getKey(), + ])) + ->resize(1440, 1100) + ->waitForText('Evidence snapshot required') + ->assertSee('Is this evidence package ready for customer or auditor consumption?') + ->assertSee('Generate evidence snapshot') + ->assertSee('Evidence readiness flow') + ->assertScript('document.querySelectorAll("[data-testid=\"evidence-readiness-step\"]").length === 6', true) + ->assertScript('document.querySelector("[data-step-label=\"Evidence snapshot\"]")?.dataset.stepState === "Missing"', true) + ->assertScript('document.querySelector("[data-step-label=\"Evidence snapshot\"]")?.dataset.stepCurrentBlocker === "true"', true) + ->assertScript('document.querySelector("[data-testid=\"evidence-disclosure-diagnostics\"]")?.open === false', true) + ->assertDontSee('raw payload should stay hidden') + ->assertDontSee('provider response should stay hidden') + ->assertNoJavaScriptErrors() + ->assertNoConsoleLogs(); + $page->screenshot(true, spec337BrowserScreenshotName('01-evidence-snapshot-required')); + spec337CopyBrowserScreenshot('01-evidence-snapshot-required'); + + $page = visit(route('admin.evidence.overview', [ + 'environment_id' => (int) $generatingEnvironment->getKey(), + ])) + ->waitForText('Evidence generation in progress') + ->assertSee('View operation progress') + ->assertSee('Operation proof') + ->assertSee('Spec337 Browser Operator') + ->assertScript('document.querySelector("[data-step-label=\"Evidence snapshot\"]")?.dataset.stepState === "Generating"', true) + ->assertScript('document.querySelector("[data-step-label=\"Evidence snapshot\"]")?.dataset.stepCurrentBlocker === "true"', true) + ->assertNoJavaScriptErrors() + ->assertNoConsoleLogs(); + $page->screenshot(true, spec337BrowserScreenshotName('02-evidence-generating')); + spec337CopyBrowserScreenshot('02-evidence-generating'); + + $page = visit(route('admin.evidence.overview', [ + 'environment_id' => (int) $storedReportRequiredEnvironment->getKey(), + ])) + ->waitForText('Stored report required') + ->assertSee('Open evidence snapshot') + ->assertScript('document.querySelector("[data-step-label=\"Stored report\"]")?.dataset.stepState === "Missing"', true) + ->assertScript('document.querySelector("[data-step-label=\"Stored report\"]")?.dataset.stepCurrentBlocker === "true"', true) + ->assertDontSee('Download export') + ->assertNoJavaScriptErrors() + ->assertNoConsoleLogs(); + $page->screenshot(true, spec337BrowserScreenshotName('03-stored-report-required')); + spec337CopyBrowserScreenshot('03-stored-report-required'); + + $page = visit(route('admin.evidence.overview', [ + 'environment_id' => (int) $reviewPackRequiredEnvironment->getKey(), + ])) + ->waitForText('Review pack required') + ->assertSee('Generate review pack') + ->assertScript('document.querySelector("[data-step-label=\"Review pack\"]")?.dataset.stepState === "Required"', true) + ->assertScript('document.querySelector("[data-step-label=\"Review pack\"]")?.dataset.stepCurrentBlocker === "true"', true) + ->assertScript('document.querySelector("[data-testid=\"evidence-primary-proof-action\"]") instanceof HTMLAnchorElement', true) + ->assertNoJavaScriptErrors() + ->assertNoConsoleLogs(); + $page->screenshot(true, spec337BrowserScreenshotName('04-review-pack-required')); + spec337CopyBrowserScreenshot('04-review-pack-required'); + + $page = visit(route('admin.evidence.overview', [ + 'environment_id' => (int) $availableEnvironment->getKey(), + ])) + ->waitForText('Review pack export available') + ->assertSee('Download export') + ->assertSee('Customer-safe output') + ->assertSee('Review pack contents / coverage') + ->assertScript('document.querySelector("[data-step-label=\"Review pack\"]")?.dataset.stepState === "Available"', true) + ->assertScript('document.querySelector("[data-step-label=\"Customer-safe output\"]")?.dataset.stepState === "Ready"', true) + ->assertScript('document.querySelector("[data-step-label=\"Export / delivery\"]")?.dataset.stepState === "Available"', true) + ->assertScript('Array.from(document.querySelectorAll("[data-testid=\"evidence-review-pack-status-badge\"]")).every((badge) => !badge.innerText.includes("...") && getComputedStyle(badge).overflow !== "hidden" && getComputedStyle(badge).textOverflow !== "ellipsis")', true) + ->assertDontSee('Auditor-ready') + ->assertDontSee('environment is healthy') + ->assertNoJavaScriptErrors() + ->assertNoConsoleLogs(); + $page->screenshot(true, spec337BrowserScreenshotName('05-review-pack-available')); + spec337CopyBrowserScreenshot('05-review-pack-available'); + + $page->screenshot(true, spec337BrowserScreenshotName('06-customer-safe-output-state')); + spec337CopyBrowserScreenshot('06-customer-safe-output-state'); + + $page = visit(route('admin.evidence.overview', [ + 'environment_id' => (int) $exportUnavailableEnvironment->getKey(), + ])) + ->waitForText('Export unavailable') + ->assertSee('Open review pack') + ->assertScript('document.querySelector("[data-step-label=\"Export / delivery\"]")?.dataset.stepState === "Required"', true) + ->assertDontSee('Download export') + ->assertNoJavaScriptErrors() + ->assertNoConsoleLogs(); + $page->screenshot(true, spec337BrowserScreenshotName('07-export-unavailable')); + spec337CopyBrowserScreenshot('07-export-unavailable'); + + $page->assertScript('document.querySelector("[data-testid=\"evidence-disclosure-diagnostics\"]")?.open === false', true); + $page->screenshot(true, spec337BrowserScreenshotName('08-diagnostics-collapsed')); + spec337CopyBrowserScreenshot('08-diagnostics-collapsed'); + + $page->script("document.documentElement.classList.add('dark');"); + $page->script('window.scrollTo(0, 0);'); + $page->assertScript('document.documentElement.classList.contains("dark")', true); + $page->screenshot(true, spec337BrowserScreenshotName('09-dark-mode')); + spec337CopyBrowserScreenshot('09-dark-mode'); +}); + +/** + * @param array $attributes + */ +function spec337BrowserCreateEvidenceSnapshot(ManagedEnvironment $environment, array $attributes = []): EvidenceSnapshot +{ + $snapshot = EvidenceSnapshot::query()->create(array_replace([ + 'managed_environment_id' => (int) $environment->getKey(), + 'workspace_id' => (int) $environment->workspace_id, + 'status' => EvidenceSnapshotStatus::Active->value, + 'fingerprint' => sha1('spec337-browser-snapshot-'.$environment->getKey().'-'.microtime(true)), + 'completeness_state' => EvidenceCompletenessState::Complete->value, + 'summary' => [ + 'dimension_count' => 5, + 'missing_dimensions' => 0, + 'stale_dimensions' => 0, + 'raw_payload' => 'raw payload should stay hidden', + 'provider_response' => 'provider response should stay hidden', + ], + 'generated_at' => now(), + 'expires_at' => now()->addDays(30), + ], $attributes)); + + $snapshot->items()->create([ + 'managed_environment_id' => (int) $environment->getKey(), + 'workspace_id' => (int) $environment->workspace_id, + 'dimension_key' => 'permission_posture', + 'state' => (string) ($snapshot->completeness_state ?: EvidenceCompletenessState::Complete->value), + 'required' => true, + 'source_kind' => 'stored_report', + 'source_record_type' => StoredReport::class, + 'source_record_id' => null, + 'source_fingerprint' => null, + 'measured_at' => now(), + 'freshness_at' => now(), + 'summary_payload' => [ + 'label' => 'Permission posture', + 'state' => 'complete', + ], + 'sort_order' => 1, + ]); + + return $snapshot->refresh(); +} + +function spec337BrowserCreateStoredReport(ManagedEnvironment $environment): StoredReport +{ + return StoredReport::factory()->permissionPosture([ + 'raw_payload' => 'raw payload should stay hidden', + 'provider_response' => 'provider response should stay hidden', + ])->create([ + 'managed_environment_id' => (int) $environment->getKey(), + 'workspace_id' => (int) $environment->workspace_id, + 'fingerprint' => sha1('spec337-browser-report-'.$environment->getKey().'-'.microtime(true)), + ]); +} + +function spec337BrowserCreatePublishedReviewWithReadyPack( + ManagedEnvironment $environment, + User $user, + EvidenceSnapshot $snapshot, +): ReviewPack { + $review = EnvironmentReview::query()->create([ + 'workspace_id' => (int) $environment->workspace_id, + 'managed_environment_id' => (int) $environment->getKey(), + 'evidence_snapshot_id' => (int) $snapshot->getKey(), + 'initiated_by_user_id' => (int) $user->getKey(), + 'published_by_user_id' => (int) $user->getKey(), + 'fingerprint' => sha1('spec337-browser-review-'.$environment->getKey().'-'.microtime(true)), + 'status' => EnvironmentReviewStatus::Published->value, + 'completeness_state' => EnvironmentReviewCompletenessState::Complete->value, + 'summary' => [ + 'governance_package' => [ + 'decision_summary' => [ + 'status' => 'ready', + 'evidence_state' => EnvironmentReviewCompletenessState::Complete->value, + 'decision_data_state' => 'complete', + ], + ], + ], + 'generated_at' => now()->subMinutes(5), + 'published_at' => now()->subMinutes(3), + ]); + + $run = OperationRun::factory()->forTenant($environment)->create([ + 'type' => OperationRunType::ReviewPackGenerate->value, + 'status' => OperationRunStatus::Completed->value, + 'outcome' => OperationRunOutcome::Succeeded->value, + 'started_at' => now()->subMinutes(4), + 'completed_at' => now()->subMinutes(2), + 'initiator_name' => $user->name, + ]); + + $pack = ReviewPack::factory()->ready()->create([ + 'managed_environment_id' => (int) $environment->getKey(), + 'workspace_id' => (int) $environment->workspace_id, + 'evidence_snapshot_id' => (int) $snapshot->getKey(), + 'environment_review_id' => (int) $review->getKey(), + 'operation_run_id' => (int) $run->getKey(), + 'initiated_by_user_id' => (int) $user->getKey(), + 'status' => ReviewPackStatus::Ready->value, + 'summary' => [ + 'finding_count' => 3, + 'report_count' => 2, + 'operation_count' => 1, + 'section_count' => 4, + 'evidence_resolution' => [ + 'required_dimensions' => [ + 'findings_summary', + 'permission_posture', + 'entra_admin_roles', + ], + ], + ], + ]); + + $review->forceFill([ + 'current_export_review_pack_id' => (int) $pack->getKey(), + ])->save(); + + return $pack->refresh(); +} diff --git a/apps/platform/tests/Feature/Filament/Spec337EvidenceReviewPackProductFlowTest.php b/apps/platform/tests/Feature/Filament/Spec337EvidenceReviewPackProductFlowTest.php new file mode 100644 index 00000000..794ddd4b --- /dev/null +++ b/apps/platform/tests/Feature/Filament/Spec337EvidenceReviewPackProductFlowTest.php @@ -0,0 +1,361 @@ +assertSee('Is this evidence package ready for customer or auditor consumption?') + ->assertSee('Evidence snapshot required') + ->assertSee('Review pack output cannot be trusted or exported yet.') + ->assertSee('Generate evidence snapshot') + ->assertSee('Evidence readiness flow') + ->assertSee('Evidence proof') + ->assertSee('Diagnostics - Collapsed') + ->assertDontSee('raw payload should stay hidden') + ->assertDontSee('provider response should stay hidden') + ->assertDontSee('stack trace should stay hidden'); + + $content = $component->html(); + + spec337AssertFlowStep($content, 'Source data selected', 'Available', false); + spec337AssertFlowStep($content, 'Evidence snapshot', 'Missing', true); + spec337AssertFlowStep($content, 'Stored report', 'Unavailable', false); + spec337AssertFlowStep($content, 'Review pack', 'Unavailable', false); + spec337AssertFlowStep($content, 'Customer-safe output', 'Not ready', false); + spec337AssertFlowStep($content, 'Export / delivery', 'Unavailable', false); + + expect(substr_count($content, 'data-testid="evidence-readiness-step"'))->toBe(6) + ->and($content)->not->toContain('data-testid="evidence-disclosure-diagnostics" open'); +}); + +it('renders stored report missing without inventing review-pack or export readiness', function (): void { + [$user, $environment] = createUserWithTenant(role: 'owner', workspaceRole: 'manager'); + + spec337CreateEvidenceSnapshot($environment); + + $component = spec337EvidenceOverviewLivewire($user, $environment) + ->assertSee('Stored report required') + ->assertSee('Evidence snapshot exists, but no stored report is available for this review output.') + ->assertSee('Open evidence snapshot') + ->assertDontSee('Review pack export available') + ->assertDontSee('Customer-safe output ready') + ->assertDontSee('Download export'); + + $content = $component->html(); + + spec337AssertFlowStep($content, 'Evidence snapshot', 'Available', false); + spec337AssertFlowStep($content, 'Stored report', 'Missing', true); + spec337AssertFlowStep($content, 'Review pack', 'Unavailable', false); + spec337AssertFlowStep($content, 'Customer-safe output', 'Not ready', false); + spec337AssertFlowStep($content, 'Export / delivery', 'Unavailable', false); +}); + +it('renders review pack required with capability-aware primary action', function (): void { + [$owner, $environment] = createUserWithTenant(role: 'owner', workspaceRole: 'manager'); + [$readonly] = createUserWithTenant(tenant: $environment, role: 'readonly', workspaceRole: 'readonly'); + + $snapshot = spec337CreateEvidenceSnapshot($environment); + spec337CreateStoredReport($environment); + + $ownerComponent = spec337EvidenceOverviewLivewire($owner, $environment) + ->assertSee('Review pack required') + ->assertSee('Stored report exists, but a review pack has not been generated.') + ->assertSee('Generate review pack') + ->assertDontSee('Customer-safe output ready') + ->assertDontSee('Review pack export available'); + + spec337AssertFlowStep($ownerComponent->html(), 'Review pack', 'Required', true); + + spec337EvidenceOverviewLivewire($readonly, $environment) + ->assertSee('Review pack required') + ->assertDontSee('Generate review pack') + ->assertSee('Open evidence snapshot') + ->assertSee((string) $snapshot->tenant?->name); +}); + +it('renders repo-backed customer-safe and export-ready output without broad readiness claims', function (): void { + [$user, $environment] = createUserWithTenant(role: 'owner', workspaceRole: 'manager'); + + $snapshot = spec337CreateEvidenceSnapshot($environment); + spec337CreateStoredReport($environment); + [$review, $pack] = spec337CreatePublishedReviewWithReadyPack($environment, $user, $snapshot); + + $component = spec337EvidenceOverviewLivewire($user, $environment) + ->assertSee('Review pack export available') + ->assertSee('A generated export artifact is available for authorized download.') + ->assertSee('external sharing remains governed by workspace policy') + ->assertSee('Download export') + ->assertSee('Customer-safe output') + ->assertSee('Ready') + ->assertSee('Review pack contents / coverage') + ->assertSee('Findings included') + ->assertSee('Evidence dimensions') + ->assertDontSee('Auditor-ready') + ->assertDontSee('environment is healthy') + ->assertDontSee('compliant'); + + $content = $component->html(); + + spec337AssertFlowStep($content, 'Evidence snapshot', 'Available', false); + spec337AssertFlowStep($content, 'Stored report', 'Available', false); + spec337AssertFlowStep($content, 'Review pack', 'Available', false); + spec337AssertFlowStep($content, 'Customer-safe output', 'Ready', false); + spec337AssertFlowStep($content, 'Export / delivery', 'Available', true); + + expect($review->current_export_review_pack_id)->toBe($pack->getKey()); +}); + +it('renders generating and failed operation proof separately from usable output', function (): void { + [$user, $generatingEnvironment] = createUserWithTenant(role: 'owner', workspaceRole: 'manager'); + $generatingRun = OperationRun::factory()->forTenant($generatingEnvironment)->create([ + 'type' => OperationRunType::EvidenceSnapshotGenerate->value, + 'status' => OperationRunStatus::Running->value, + 'outcome' => OperationRunOutcome::Pending->value, + 'started_at' => now()->subMinute(), + 'initiator_name' => 'Spec337 Operator', + ]); + spec337CreateEvidenceSnapshot($generatingEnvironment, [ + 'operation_run_id' => (int) $generatingRun->getKey(), + 'status' => EvidenceSnapshotStatus::Generating->value, + 'generated_at' => null, + ]); + + $generatingComponent = spec337EvidenceOverviewLivewire($user, $generatingEnvironment) + ->assertSee('Evidence generation in progress') + ->assertSee('View operation progress') + ->assertSee('Operation proof') + ->assertSee('Generating') + ->assertSee('Spec337 Operator') + ->assertDontSee('Customer-safe output ready'); + + spec337AssertFlowStep($generatingComponent->html(), 'Evidence snapshot', 'Generating', true); + + $failedEnvironment = createUserWithTenant(user: $user, role: 'owner', workspaceRole: 'manager')[1]; + $failedRun = OperationRun::factory()->forTenant($failedEnvironment)->create([ + 'type' => OperationRunType::ReviewPackGenerate->value, + 'status' => OperationRunStatus::Completed->value, + 'outcome' => OperationRunOutcome::Failed->value, + 'started_at' => now()->subMinutes(3), + 'completed_at' => now()->subMinute(), + 'initiator_name' => 'Spec337 Reviewer', + ]); + $failedSnapshot = spec337CreateEvidenceSnapshot($failedEnvironment); + spec337CreateStoredReport($failedEnvironment); + ReviewPack::factory()->failed()->create([ + 'managed_environment_id' => (int) $failedEnvironment->getKey(), + 'workspace_id' => (int) $failedEnvironment->workspace_id, + 'evidence_snapshot_id' => (int) $failedSnapshot->getKey(), + 'operation_run_id' => (int) $failedRun->getKey(), + ]); + + $failedComponent = spec337EvidenceOverviewLivewire($user, $failedEnvironment) + ->assertSee('Review pack generation failed') + ->assertSee('Review operation') + ->assertSee('Operation proof') + ->assertSee('Failed') + ->assertSee('Spec337 Reviewer') + ->assertDontSee('Review pack export available'); + + spec337AssertFlowStep($failedComponent->html(), 'Review pack', 'Failed', true); +}); + +it('preserves workspace isolation and avoids legacy tenant aliases in rendered links', function (): void { + [$user, $environment] = createUserWithTenant(role: 'owner', workspaceRole: 'manager'); + $foreignEnvironment = ManagedEnvironment::factory()->active()->create([ + 'name' => 'Spec337 Foreign Workspace Environment', + ]); + spec337CreateEvidenceSnapshot($foreignEnvironment); + + $this->actingAs($user) + ->withSession([WorkspaceContext::SESSION_KEY => (int) $environment->workspace_id]) + ->get(route('admin.evidence.overview')) + ->assertOk() + ->assertDontSee('Spec337 Foreign Workspace Environment'); + + $this->actingAs($user) + ->withSession([WorkspaceContext::SESSION_KEY => (int) $environment->workspace_id]) + ->get(route('admin.evidence.overview', ['environment_id' => (int) $foreignEnvironment->getKey()])) + ->assertNotFound(); + + $content = spec337EvidenceOverviewLivewire($user, $environment)->html(); + + expect($content) + ->not->toContain('/admin/t/') + ->not->toContain('tenant_id='); +} +); + +function spec337EvidenceOverviewLivewire(User $user, ManagedEnvironment $environment): mixed +{ + session()->put(WorkspaceContext::SESSION_KEY, (int) $environment->workspace_id); + setAdminPanelContext($environment); + + return Livewire::withQueryParams([ + 'environment_id' => (int) $environment->getKey(), + ]) + ->actingAs($user) + ->test(EvidenceOverview::class); +} + +/** + * @param array $attributes + */ +function spec337CreateEvidenceSnapshot(ManagedEnvironment $environment, array $attributes = []): EvidenceSnapshot +{ + $snapshot = EvidenceSnapshot::query()->create(array_replace([ + 'managed_environment_id' => (int) $environment->getKey(), + 'workspace_id' => (int) $environment->workspace_id, + 'status' => EvidenceSnapshotStatus::Active->value, + 'fingerprint' => sha1('spec337-snapshot-'.$environment->getKey().'-'.microtime(true)), + 'completeness_state' => EvidenceCompletenessState::Complete->value, + 'summary' => [ + 'dimension_count' => 5, + 'missing_dimensions' => 0, + 'stale_dimensions' => 0, + 'raw_payload' => 'raw payload should stay hidden', + 'provider_response' => 'provider response should stay hidden', + 'stack_trace' => 'stack trace should stay hidden', + ], + 'generated_at' => now(), + 'expires_at' => now()->addDays(30), + ], $attributes)); + + $snapshot->items()->create([ + 'managed_environment_id' => (int) $environment->getKey(), + 'workspace_id' => (int) $environment->workspace_id, + 'dimension_key' => 'permission_posture', + 'state' => (string) ($snapshot->completeness_state ?: EvidenceCompletenessState::Complete->value), + 'required' => true, + 'source_kind' => 'stored_report', + 'source_record_type' => StoredReport::class, + 'source_record_id' => null, + 'source_fingerprint' => null, + 'measured_at' => now(), + 'freshness_at' => now(), + 'summary_payload' => [ + 'label' => 'Permission posture', + 'state' => 'complete', + ], + 'sort_order' => 1, + ]); + + return $snapshot->load([ + 'tenant', + 'operationRun', + 'reviewPacks.operationRun', + 'reviewPacks.environmentReview.currentExportReviewPack', + 'items', + ]); +} + +function spec337CreateStoredReport(ManagedEnvironment $environment): StoredReport +{ + return StoredReport::factory()->permissionPosture([ + 'raw_payload' => 'raw payload should stay hidden', + 'provider_response' => 'provider response should stay hidden', + 'stack_trace' => 'stack trace should stay hidden', + ])->create([ + 'managed_environment_id' => (int) $environment->getKey(), + 'workspace_id' => (int) $environment->workspace_id, + 'fingerprint' => sha1('spec337-report-'.$environment->getKey().'-'.microtime(true)), + ]); +} + +/** + * @return array{0: EnvironmentReview, 1: ReviewPack} + */ +function spec337CreatePublishedReviewWithReadyPack( + ManagedEnvironment $environment, + User $user, + EvidenceSnapshot $snapshot, +): array { + $review = EnvironmentReview::query()->create([ + 'workspace_id' => (int) $environment->workspace_id, + 'managed_environment_id' => (int) $environment->getKey(), + 'evidence_snapshot_id' => (int) $snapshot->getKey(), + 'initiated_by_user_id' => (int) $user->getKey(), + 'published_by_user_id' => (int) $user->getKey(), + 'fingerprint' => sha1('spec337-review-'.$environment->getKey().'-'.microtime(true)), + 'status' => EnvironmentReviewStatus::Published->value, + 'completeness_state' => EnvironmentReviewCompletenessState::Complete->value, + 'summary' => [ + 'governance_package' => [ + 'decision_summary' => [ + 'status' => 'ready', + 'evidence_state' => EnvironmentReviewCompletenessState::Complete->value, + 'decision_data_state' => 'complete', + ], + ], + ], + 'generated_at' => now()->subMinutes(5), + 'published_at' => now()->subMinutes(3), + ]); + + $run = OperationRun::factory()->forTenant($environment)->create([ + 'type' => OperationRunType::ReviewPackGenerate->value, + 'status' => OperationRunStatus::Completed->value, + 'outcome' => OperationRunOutcome::Succeeded->value, + 'started_at' => now()->subMinutes(4), + 'completed_at' => now()->subMinutes(2), + 'initiator_name' => $user->name, + ]); + + $pack = ReviewPack::factory()->ready()->create([ + 'managed_environment_id' => (int) $environment->getKey(), + 'workspace_id' => (int) $environment->workspace_id, + 'evidence_snapshot_id' => (int) $snapshot->getKey(), + 'environment_review_id' => (int) $review->getKey(), + 'operation_run_id' => (int) $run->getKey(), + 'initiated_by_user_id' => (int) $user->getKey(), + 'status' => ReviewPackStatus::Ready->value, + 'summary' => [ + 'finding_count' => 3, + 'report_count' => 2, + 'operation_count' => 1, + 'section_count' => 4, + 'evidence_resolution' => [ + 'required_dimensions' => [ + 'findings_summary', + 'permission_posture', + 'entra_admin_roles', + ], + ], + ], + ]); + + $review->forceFill([ + 'current_export_review_pack_id' => (int) $pack->getKey(), + ])->save(); + + return [$review->refresh(), $pack->refresh()]; +} + +function spec337AssertFlowStep(string $content, string $label, string $state, bool $currentBlocker): void +{ + $blocker = $currentBlocker ? 'true' : 'false'; + + expect($content)->toMatch( + '/data-step-label="'.preg_quote($label, '/').'"[\s\S]*?data-step-state="'.preg_quote($state, '/').'"[\s\S]*?data-step-current-blocker="'.$blocker.'"[\s\S]*?>\s*'.preg_quote($state, '/').'\s* Evidence snapshot -> Stored report -> Review pack -> Customer-safe output -> Export / delivery` + +## Technical Context + +- Language/Version: PHP 8.4.15, Laravel 12.x. +- Primary Dependencies: Filament v5 + Livewire v4, Pest v4, Tailwind v4. +- Storage: PostgreSQL; no schema change expected. +- Testing: Pest Feature/Livewire render tests + one browser smoke file. +- Validation Lanes: confidence + browser. +- Target Platform: Sail locally; Dokploy/container deployment posture unchanged. +- Project Type: Laravel monolith under `apps/platform`. +- Performance Goals: DB-only render; no Microsoft Graph/provider calls during page render. +- Constraints: no new backend engines, no new persisted readiness truth, no new queue/scheduler behavior, no fake customer-safe/auditor-ready/export-ready claims, no raw payload default view, no cross-workspace leaks. +- Scale/Scope: bounded to existing Evidence Overview, Review Pack, Stored Report/Evidence Snapshot proof links, Environment Review export proof, and Customer Review Workspace evidence path only where needed. + +## UI / Surface Guardrail Plan + +- **Guardrail scope**: changed existing operator-facing and customer-safe evidence surfaces. +- **Affected routes/pages/actions/states/navigation/panel/provider surfaces**: + - `/admin/evidence/overview` + - `/admin/reviews/workspace` + - `/admin/workspaces/{workspace}/environments/{environment}/review-packs` + - `/admin/review-packs/{reviewPack}/download` + - linked Evidence Snapshot, Stored Report, Environment Review, and OperationRun proof surfaces only as needed +- **No-impact class, if applicable**: N/A. +- **Native vs custom classification summary**: mixed but existing: Filament resources/pages plus existing Blade workbench sections and shared primitives. +- **Shared-family relevance**: Product Process Flow, evidence disclosure, OperationRun proof, artifact truth, customer-safe review package state. +- **State layers in scope**: page, detail, URL-query/environment filter, artifact links, signed download availability. +- **Audience modes in scope**: operator-MSP, manager, support reviewer, customer-readable review workspace. +- **Decision/diagnostic/raw hierarchy plan**: decision-first, proof second, diagnostics/raw third and collapsed. +- **Raw/support gating plan**: collapsed by default; capability-aware using existing conventions. +- **One-primary-action / duplicate-truth control**: compute one state-specific primary action; lower sections add proof, not alternate verdicts. +- **Handling modes by drift class or surface**: review-mandatory for customer-safe and auditor-facing claims; report-only for diagnostics. +- **Repository-signal treatment**: customer-safe/export signals must be repo-backed; unavailable/deferred is preferred over invented readiness. +- **Special surface test profiles**: monitoring-state-page, shared-detail-family, customer-safe consumption path. +- **Required tests or manual smoke**: feature state contract + browser smoke + screenshots. +- **Exception path and spread control**: no exception expected; follow-up spec if implementation requires new persisted lifecycle truth. +- **Active feature PR close-out entry**: Smoke Coverage. +- **UI/Productization coverage decision**: feature-local screenshots/tests required; audit docs only if route/archetype/navigation changes during implementation. +- **Coverage artifacts to update**: none expected in prep. +- **No-impact rationale**: N/A. +- **Navigation / Filament provider-panel handling**: no panel/provider registration change. Laravel 11+/12 provider registration remains `apps/platform/bootstrap/providers.php`. +- **Screenshot or page-report need**: screenshots required under `specs/337-evidence-review-pack-product-process-flow-alignment/artifacts/screenshots/`. + +## Shared Pattern & System Fit + +- **Cross-cutting feature marker**: yes. +- **Systems touched**: + - Spec 332 Product Process Flow system. + - Spec 329 Evidence Overview proof-first disclosure. + - Spec 326 Customer Review Workspace evidence path. + - Review Pack Resource list/detail/download. + - Evidence Snapshot Resource proof links. + - Stored Report Resource proof links. + - Environment Review current export/review pack proof. + - OperationRun proof links. +- **Shared abstractions reused**: + - Product Process Flow render conventions from Spec 332. + - `OperationRunLinks`. + - `OperationUxPresenter` where operation start/progress messaging is already delegated. + - `UiEnforcement` and policy/capability gates for actions. + - `BadgeCatalog` / `BadgeRenderer` where status-like badges are introduced or changed. +- **New abstraction introduced? why?**: none required in prep. A small `EvidenceReviewPackPresenter` or page-local view model is allowed only if it prevents scattered Blade/Page logic and remains derived-only. +- **Why the existing abstraction was sufficient or insufficient**: Product Process Flow is sufficient for readiness steps; a local presenter may be needed because readiness combines snapshots, reports, packs, review state, and operation proof without a single persisted lifecycle record. +- **Bounded deviation / spread control**: do not create a generic workflow engine or state taxonomy. Presentation-only states stay in the feature contract and tests. + +## OperationRun UX Impact + +- **Touches OperationRun start/completion/link UX?**: yes, for existing proof links and generation/export state display. +- **Central contract reused**: existing OperationRun lifecycle, `OperationRunLinks`, `OperationUxPresenter`, and services/jobs that already create or update runs. +- **Delegated UX behaviors**: queued toast, operation deep link, signed artifact link, tenant/workspace-safe URL resolution, and active-run blocked messaging remain in existing services/resources. +- **Surface-owned behavior kept local**: explanation copy, readiness flow placement, proof panel ordering. +- **Queued DB-notification policy**: no change. +- **Terminal notification path**: unchanged. +- **Exception path**: none. + +## Provider Boundary & Portability Fit + +- **Shared provider/platform boundary touched?**: no. +- **Provider-owned seams**: N/A. +- **Platform-core seams**: evidence/report/review artifact presentation only; no provider contract changes. +- **Neutral platform terms / contracts preserved**: workspace, environment, review, evidence snapshot, stored report, review pack, customer-safe output, export/download, operation proof. +- **Retained provider-specific semantics and why**: existing report type labels such as Entra admin roles remain only where stored reports already expose them. +- **Bounded extraction or follow-up path**: none. + +## Constitution Check + +*GATE: Must pass before runtime work and be rechecked after implementation design.* + +- Inventory-first: pass. This spec separates source data, snapshots, stored reports, review packs, customer-safe output, and export artifacts. +- Read/write separation: pass. Existing generate/export actions stay explicit, capability-gated, and OperationRun/audit-backed; no destructive action added. +- Graph contract path: pass. Page render must not call Graph/providers. +- RBAC-UX: pass. Existing policies/capabilities must continue to deny cross-workspace/environment leakage. +- Workspace isolation: pass. Canonical evidence overview keeps environment filter/context and resource routes remain workspace/environment-scoped. +- Run observability: pass. Existing long-running evidence/review-pack work remains OperationRun-backed; this spec adds proof presentation only. +- Ops-UX lifecycle: pass. No OperationRun lifecycle changes. +- Data minimization: pass. Raw payloads and diagnostics stay collapsed and capability-aware. +- Test governance: pass. Feature + browser lanes are explicit and bounded. +- Proportionality: pass. No persisted truth; only a small derived presenter if needed. +- No premature abstraction: pass. Reuse Spec 332 Product Process Flow and existing artifact/proof helpers. +- Persisted truth: pass. No new tables/entities/artifacts. +- Behavioral state: pass. Presentation states are derived and do not add lifecycle semantics. +- Shared pattern first: pass. Product Process Flow is the core reuse. +- Provider boundary: pass. No provider seam change. +- Badge semantics: pass. New/changed status-like badges must use shared badge semantics. +- Filament-native UI: pass. Use existing Filament/Blade surface conventions and shared primitives. +- Decision-first operating model: pass. Decision card and flow come before tables/raw artifacts. +- Audience-aware disclosure: pass. Customer-readable default hides raw JSON, payloads, diagnostics, fingerprints, and internal reason ownership. +- UI/Productization coverage: pass. Existing reachable surfaces changed; feature-local screenshots/tests are required. + +## Test Governance Check + +- **Test purpose / classification by changed surface**: Feature tests for state contract, RBAC/context, false-claim prevention; Browser smoke for rendered flow, proof panel, collapsed diagnostics, screenshots. +- **Affected validation lanes**: confidence + browser. +- **Why this lane mix is the narrowest sufficient proof**: state composition and RBAC can be proven in Feature tests; layout/readability/collapsed diagnostics require one browser smoke. +- **Narrowest proving command(s)**: + +```bash +cd apps/platform +./vendor/bin/sail artisan test tests/Feature/Filament/Spec337EvidenceReviewPackProductFlowTest.php --compact +./vendor/bin/sail php vendor/bin/pest tests/Browser/Spec337EvidenceReviewPackProductFlowSmokeTest.php --compact +``` + +- **Fixture / helper / factory / seed / context cost risks**: reuse existing workspace/environment/user/EvidenceSnapshot/StoredReport/ReviewPack/EnvironmentReview/OperationRun fixtures. Do not add heavy default seeds. +- **Expensive defaults or shared helper growth introduced?**: no. +- **Heavy-family additions, promotions, or visibility changes**: one explicit browser smoke file only. +- **Surface-class relief / special coverage rule**: no standard-native relief; this is a strategic/customer-safe workbench alignment. +- **Closing validation and reviewer handoff**: verify no raw JSON initial render, no false customer-safe/export/auditor-ready claims, no cross-workspace evidence leaks, and one primary action per state. +- **Budget / baseline / trend follow-up**: none expected. +- **Review-stop questions**: if a fixture needs new backend state, stop and mark the state unavailable/deferred instead of adding persistence. +- **Escalation path**: document-in-feature for unreachable states; follow-up-spec for backend readiness engine needs. +- **Active feature PR close-out entry**: Smoke Coverage. +- **Why no dedicated follow-up spec is needed**: this is a bounded Product Process Flow consumer; broader auditor artifact delivery remains a separate future spec. + +## Current Repo Truth Summary (Implementation-Relevant) + +- `EvidenceSnapshot` exists with status, completeness, generated/expiry timestamps, items, workspace/environment, operation, initiator, review packs, and environment reviews. +- `StoredReport` exists for permission posture and Entra admin role reports; it stores JSONB payloads and fingerprints but has no direct OperationRun relationship in inspected code. +- `ReviewPack` exists with queued/generating/ready/failed/expired status, file metadata, evidence snapshot, environment review, OperationRun, and initiator. +- `EnvironmentReview` exists with evidence snapshot, OperationRun, current export review pack, sections, status, completeness, and summary. +- `CustomerReviewWorkspace` already derives review/package/customer-safe consumption state and signed download availability from existing review and pack truth. +- `EvidenceOverview` already has a proof-first workbench and collapsed diagnostics, but does not yet render the six-step Evidence readiness flow. +- Signed review-pack downloads are repo-backed via `ReviewPackDownloadController`, signed route, policies/capabilities, ready/non-expired/file metadata checks, and audit logging. +- Global search is disabled on relevant resources inspected (`EvidenceSnapshotResource`, `ReviewPackResource`, `StoredReportResource`, `EnvironmentReviewResource`). + +## Implementation Approach + +### Phase 0 - Repo Truth Gate (No Runtime Edits) + +1. Confirm `repo-truth-map.md` and `evidence-review-pack-state-contract.md` still match runtime code. +2. Re-inspect Evidence Overview, Customer Review Workspace, ReviewPackResource, StoredReportResource, EvidenceSnapshotResource, EnvironmentReviewResource, OperationRun links, and download controller. +3. Mark unsupported states unavailable/deferred instead of implementing new backend truth. + +### Phase 1 - Presenter / Flow Model + +1. If needed, create a small `EvidenceReviewPackPresenter` or page-local view model that computes: + - decision card fields + - six flow steps + - proof items + - coverage summary + - customer-safe state + - export/download state + - diagnostics state +2. Keep the presenter derived-only. No static process memoization, no new source of truth, no new enum/status family. + +### Phase 2 - Evidence Overview UI Alignment + +1. Add the decision card question and fields. +2. Add the Evidence readiness flow using the Product Process Flow pattern. +3. Productize the proof panel while preserving existing collapsed diagnostics. +4. Keep raw artifact tables secondary. +5. Avoid duplicate verdict/readiness blocks. + +### Phase 3 - Review Pack / Customer-Safe / Export States + +1. Productize review pack state from existing `ReviewPack` status and file metadata. +2. Productize export/download state only where ready, non-expired, file-backed, signed download is authorized. +3. Productize customer-safe state only where Customer Review Workspace / Environment Review package readiness supports it. +4. Show external delivery as unavailable unless implementation discovers repo-backed delivery. + +### Phase 4 - RBAC / Context / Diagnostics + +1. Preserve workspace/environment/review context in all action links. +2. Respect existing capability-first gates. +3. Keep non-member/cross-workspace artifacts hidden or not found. +4. Keep diagnostics collapsed and raw JSON hidden by default. + +### Phase 5 - Tests + +1. Add `apps/platform/tests/Feature/Filament/Spec337EvidenceReviewPackProductFlowTest.php`. +2. Cover missing evidence, report missing, review pack required, review pack available, OperationRun proof, RBAC/context, no raw JSON, and no false claims. +3. Update existing tests only where assertions are strengthened for the new contract. + +### Phase 6 - Browser Smoke + Screenshots + +1. Add `apps/platform/tests/Browser/Spec337EvidenceReviewPackProductFlowSmokeTest.php`. +2. Capture screenshots under `specs/337-evidence-review-pack-product-process-flow-alignment/artifacts/screenshots/`. +3. Document unreachable states rather than faking screenshots. + +### Phase 7 - Hygiene + Validation + +1. Run the feature and browser commands. +2. Run overlapping filters. +3. Run Pint and `git diff --check`. +4. Report deployment impact: no migrations, packages, env vars, queues, scheduler, storage, or asset changes expected. + +## Project Structure + +### Documentation (this feature) + +```text +specs/337-evidence-review-pack-product-process-flow-alignment/ +├── spec.md +├── plan.md +├── tasks.md +├── repo-truth-map.md +├── evidence-review-pack-state-contract.md +├── checklists/ +│ └── requirements.md +└── artifacts/ + └── screenshots/ +``` + +### Expected Source Code Touchpoints (implementation phase only) + +```text +apps/platform/app/Filament/Pages/Monitoring/EvidenceOverview.php +apps/platform/resources/views/filament/pages/monitoring/evidence-overview.blade.php +apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php +apps/platform/resources/views/filament/pages/reviews/customer-review-workspace.blade.php +apps/platform/app/Filament/Resources/ReviewPackResource.php +apps/platform/app/Filament/Resources/ReviewPackResource/Pages/ViewReviewPack.php +apps/platform/app/Filament/Resources/EnvironmentReviewResource.php +apps/platform/app/Filament/Resources/EvidenceSnapshotResource.php +apps/platform/app/Filament/Resources/StoredReportResource.php +apps/platform/app/Support/... (only if a small derived presenter is needed) +apps/platform/tests/Feature/Filament/Spec337EvidenceReviewPackProductFlowTest.php +apps/platform/tests/Browser/Spec337EvidenceReviewPackProductFlowSmokeTest.php +``` + +Do not create these runtime/test files during preparation-only work. They are listed for the implementation phase. diff --git a/specs/337-evidence-review-pack-product-process-flow-alignment/repo-truth-map.md b/specs/337-evidence-review-pack-product-process-flow-alignment/repo-truth-map.md new file mode 100644 index 00000000..bc15c66d --- /dev/null +++ b/specs/337-evidence-review-pack-product-process-flow-alignment/repo-truth-map.md @@ -0,0 +1,173 @@ +# Spec 337 - Repo Truth Map + +Status: prepared +Created: 2026-05-30 +Branch: `337-evidence-review-pack-product-process-flow-alignment` + +## Numbering Reconciliation + +The user-provided candidate called this "Spec 336". The repository already has `specs/336-baseline-compare-product-process-flow-alignment/` and a matching branch for Baseline Compare Product Process Flow Alignment. To avoid overwriting a completed/in-progress spec, this Evidence / Review Pack preparation uses Spec 337. + +## Truth Classifications + +- `repo-verified`: confirmed in current runtime code, schema, routes, tests, or existing spec truth maps. +- `derived from existing model`: not stored as a standalone field, but safely computed from existing repo-backed data. +- `foundation-real`: foundation exists, but this spec must still productize or connect it. +- `not available`: no repo-backed source was found during preparation. +- `deferred`: intentionally out of scope for Spec 337. + +## Evidence Snapshot Model / Data Source + +| Data Point | Classification | Repo Source | Notes | +|---|---|---|---| +| Evidence snapshot record | repo-verified | `evidence_snapshots`, `App\Models\EvidenceSnapshot` | Scoped by `workspace_id` and `managed_environment_id`. | +| Evidence snapshot status | repo-verified | `App\Enums\EvidenceSnapshotStatus` | `queued`, `generating`, `active`, `superseded`, `expired`, `failed`. | +| Evidence completeness | repo-verified | `App\Enums\EvidenceCompletenessState`, `evidence_snapshots.completeness_state` | `complete`, `partial`, `missing`, `stale`. | +| Evidence freshness | repo-verified | `evidence_snapshots.generated_at`, `expires_at`; `evidence_snapshot_items.freshness_at` | Use expiry/completeness/item freshness where available. | +| Evidence operation proof | repo-verified | `evidence_snapshots.operation_run_id`, `EvidenceSnapshot::operationRun()` | Generation creates an `OperationRun` through `EvidenceSnapshotService` / `GenerateEvidenceSnapshotJob`. | +| Evidence initiator | repo-verified | `evidence_snapshots.initiated_by_user_id`, `initiator()` | Useful for proof panel. | +| Evidence snapshot items | repo-verified | `evidence_snapshot_items`, `EvidenceSnapshot::items()` | Dimension/state/source summaries. | +| Active/current evidence | repo-verified | model scopes `active()`, `current()` and partial unique index | Current active snapshot is repo-backed. | +| Raw evidence payload | repo-verified | `evidence_snapshot_items.summary_payload`, snapshot `summary` | Must remain collapsed by default. | + +## Stored Report Model / Data Source + +| Data Point | Classification | Repo Source | Notes | +|---|---|---|---| +| Stored report record | repo-verified | `stored_reports`, `App\Models\StoredReport` | Scoped by `workspace_id`, `managed_environment_id`, `report_type`. | +| Stored report payload | repo-verified | `stored_reports.payload` JSONB | Must not be default-visible on customer-safe surfaces. | +| Stored report type | repo-verified | `StoredReport::TYPE_PERMISSION_POSTURE`, `TYPE_ENTRA_ADMIN_ROLES` | Only these inspected report types are supported. | +| Stored report fingerprint | repo-verified | `fingerprint`, `previous_fingerprint` | Artifact truth, not customer-safe readiness by itself. | +| Stored report freshness | derived from existing model | `created_at`, `updated_at`, `valid_from`, `valid_until` | Use only where the implementation already treats report validity as meaningful. | +| Stored report generation OperationRun | not available | no direct relation found on `StoredReport` | Do not show generating/failed report states unless a repo-backed run source is discovered in implementation. | +| Stored report global search | repo-verified | `StoredReportResource::$isGloballySearchable = false` | Filament global search hard rule is satisfied by disabling search. | + +## Review Pack Model / Data Source + +| Data Point | Classification | Repo Source | Notes | +|---|---|---|---| +| Review pack record | repo-verified | `review_packs`, `App\Models\ReviewPack` | Scoped by workspace and environment. | +| Review pack status | repo-verified | `App\Enums\ReviewPackStatus` | `queued`, `generating`, `ready`, `failed`, `expired`. | +| Review pack operation proof | repo-verified | `review_packs.operation_run_id`, `ReviewPack::operationRun()` | Generation uses `ReviewPackService` / `GenerateReviewPackJob`. | +| Review pack initiator | repo-verified | `initiated_by_user_id`, `initiator()` | Useful for proof panel. | +| Review pack evidence snapshot relation | repo-verified | `review_packs.evidence_snapshot_id`, `ReviewPack::evidenceSnapshot()` | Review pack can be anchored to snapshot. | +| Review pack environment review relation | repo-verified | `review_packs.environment_review_id`, `ReviewPack::environmentReview()` | Review-derived export packs are repo-backed. | +| Review pack file artifact | repo-verified | `file_disk`, `file_path`, `file_size`, `sha256` | Required for download/export readiness. | +| Review pack expiry | repo-verified | `expires_at`, `ReviewPack::expired()` | Expired pack is not export-ready. | +| Review pack summary/options | repo-verified | JSON casts on `summary`, `options` | Coverage values must come from these or related review/evidence records only. | +| Review pack global search | repo-verified | `ReviewPackResource::$isGloballySearchable = false` | Filament global search hard rule is satisfied by disabling search. | + +## Tenant / Environment Review Relationship + +| Data Point | Classification | Repo Source | Notes | +|---|---|---|---| +| Environment review record | repo-verified | `environment_reviews`, `App\Models\EnvironmentReview` | Scoped by workspace and environment. | +| Review status | repo-verified | `App\Enums\EnvironmentReviewStatus` | `draft`, `ready`, `published`, `archived`, `superseded`, `failed`. | +| Review completeness | repo-verified | `App\Enums\EnvironmentReviewCompletenessState` | `complete`, `partial`, `missing`, `stale`. | +| Review evidence snapshot | repo-verified | `environment_reviews.evidence_snapshot_id`, `evidenceSnapshot()` | Review is anchored to evidence snapshot. | +| Review current export pack | repo-verified | `current_export_review_pack_id`, `currentExportReviewPack()` | This is the primary export/customer package relation. | +| Review operation proof | repo-verified | `operation_run_id`, `operationRun()` | Review generation proof exists. | +| Review sections | repo-verified | `EnvironmentReview::sections()` | Coverage/content summary may be derived only from these summaries. | +| Customer-safe package summary | derived from existing model | `EnvironmentReview.summary`, `currentExportReviewPack`, Customer Review Workspace readiness methods | No separate persisted `customer_safe` flag found. | +| EnvironmentReview global search | repo-verified | `EnvironmentReviewResource::$isGloballySearchable = false` | Filament global search hard rule is satisfied by disabling search. | + +## Customer Review Workspace Relationship + +| Data Point | Classification | Repo Source | Notes | +|---|---|---|---| +| Customer Review Workspace route | repo-verified | `/admin/reviews/workspace`, `CustomerReviewWorkspace` | Existing customer-safe consumption surface. | +| Latest review package payload | repo-verified | `CustomerReviewWorkspace::latestReviewConsumptionPayload()` | Loads review, current export pack, evidence snapshot, OperationRuns. | +| Evidence path panel | repo-verified | `CustomerReviewWorkspace::evidencePathForReview()` and Blade view | Already separates evidence path/proof rows. | +| Review pack panel | repo-verified | `CustomerReviewWorkspace::reviewPackPanelForReview()` | Shows review pack state and export proof. | +| Customer-safe readiness | derived from existing model | `reviewReadinessForTenant()`, `governancePackageAvailability()`, `workspaceReviewNeedsAttention()` | Ready/shareable state is derived from published review, evidence/package availability, accepted risk follow-up, and download URL. | +| Download URL for ready pack | repo-verified | `reviewPackDownloadUrl()` | Requires ready package state, user capability, non-expired pack, file path/disk. | +| Diagnostics collapsed | repo-verified | `diagnosticsDisclosureForReview()` and view details block | Keep collapsed by default. | +| Separate public delivery/email/share | not available | no delivery mechanism found | External delivery must render unavailable/deferred. | + +## OperationRun Relationship For Generation / Export + +| Artifact / Flow | Classification | Repo Source | Notes | +|---|---|---|---| +| Evidence snapshot generation run | repo-verified | `EvidenceSnapshotService::generate()`, `GenerateEvidenceSnapshotJob` | Creates/updates linked `OperationRun`. | +| Review pack generation run | repo-verified | `ReviewPackService::generate()`, `GenerateReviewPackJob` | Creates/updates linked `OperationRun`. | +| Review-derived export generation run | repo-verified | `ReviewPackService::generateFromReview()` | Links `ReviewPack` to `EnvironmentReview` and `OperationRun`. | +| Environment review generation run | repo-verified | `EnvironmentReview::operationRun()` and resource/service usage | Review proof source exists. | +| Stored report generation run | not available | no direct `StoredReport::operationRun()` relation found | Do not invent report generating/failed OperationRun proof unless discovered later. | +| Operation status/outcome | repo-verified | `OperationRunStatus`, `OperationRunOutcome` | Use status/outcome/timeline/initiator/type/result in proof panel. | +| Cross-workspace OperationRun visibility | repo-verified | policies/helpers and route scoping | Must remain enforced in tests. | + +## Export / Download Artifact Relationship + +| Data Point | Classification | Repo Source | Notes | +|---|---|---|---| +| Signed download route | repo-verified | `/admin/review-packs/{reviewPack}/download`, `ReviewPackDownloadController`, route name `admin.review-packs.download` | Signed URL used by service/resource/workspace. | +| Download authorization | repo-verified | controller checks user, tenant access, `Capabilities::REVIEW_PACK_VIEW` | Preserve. | +| Ready/exportable pack | repo-verified | ready status, not expired, file exists via disk/path | Required for `Export available`. | +| Download audit | repo-verified | `ReviewPackDownloaded` audit in controller | Proof/audit exists. | +| Missing file behavior | repo-verified | controller aborts 404 when not ready/expired/missing file | Do not surface as available. | +| External delivery | not available | no email/share/portal delivery source found | Render `External delivery is not configured` if needed. | + +## Evidence Freshness Source + +| Freshness Signal | Classification | Repo Source | Notes | +|---|---|---|---| +| Snapshot generated timestamp | repo-verified | `EvidenceSnapshot.generated_at` | Displayable proof. | +| Snapshot expiry | repo-verified | `EvidenceSnapshot.expires_at` | Use for stale/expired/unavailable. | +| Snapshot completeness | repo-verified | `EvidenceSnapshot.completeness_state` | Complete/partial/missing/stale. | +| Item freshness | repo-verified | `EvidenceSnapshotItem.freshness_at`, `measured_at` | Use only when summarized or safe to show. | +| Stored report validity | derived from existing model | `valid_from`, `valid_until` | No automatic readiness claim unless existing UI/service treats validity as active. | + +## Customer-Safe State Source + +| State Source | Classification | Repo Source | Notes | +|---|---|---|---| +| Explicit persisted customer-safe flag | not available | no standalone field found | Do not add or pretend one exists. | +| Customer Review Workspace readiness | derived from existing model | `reviewReadinessForTenant()`, `governancePackageAvailability()` | Safest source for "ready to share" style presentation. | +| Review current export pack | repo-verified | `EnvironmentReview.current_export_review_pack_id` | Indicates generated package linked to review. | +| Review accepted-risk follow-up | derived from existing model | Customer Review Workspace methods and review summary | Can require review before sharing. | +| Evidence Overview customer-safe state | foundation-real | Evidence Overview can link artifacts but does not by itself confirm customer-safe output | Render unavailable/needs review unless Customer Review Workspace package readiness is linked. | + +## RBAC / Capabilities + +| Capability / Check | Classification | Repo Source | Notes | +|---|---|---|---| +| Evidence view/manage | repo-verified | `EvidenceSnapshotPolicy`, `EvidenceSnapshotResource`, capabilities | Generate/refresh/expire evidence actions are capability-gated. | +| Review pack view/manage | repo-verified | `ReviewPackPolicy`, `ReviewPackResource`, download controller | Generate/download/expire/regenerate are gated. | +| Environment review view/manage/export | repo-verified | `EnvironmentReviewPolicy`, `EnvironmentReviewResource` | Export is policy/capability-backed. | +| Stored report view | repo-verified | `StoredReportResource` and report capability rules | Read-only report surface. | +| OperationRun proof access | repo-verified | `OperationRunLinks`, resource/link visibility helpers | Proof links must stay authorized. | +| Diagnostics access | foundation-real | existing collapsed diagnostics sections | Must follow existing capability/disclosure conventions. | + +## Routes / Surfaces + +| Surface | Classification | Repo Source | Notes | +|---|---|---|---| +| Evidence Overview | repo-verified | `apps/platform/routes/web.php`, `EvidenceOverview` | `/admin/evidence/overview`; named `admin.evidence.overview`. | +| Customer Review Workspace | repo-verified | route list, `CustomerReviewWorkspace`, panel provider registration | `/admin/reviews/workspace`. | +| Review Pack list/detail | repo-verified | route list, `ReviewPackResource` | Environment-owned route under workspace/environment. | +| Review Pack download | repo-verified | route list, `ReviewPackDownloadController` | Signed route. | +| Evidence Snapshot resource | repo-verified | `EvidenceSnapshotResource` | Global search disabled; list/view pages exist. | +| Stored Report resource | repo-verified | `StoredReportResource` | Global search disabled; read-only detail. | +| Environment Review resource | repo-verified | `EnvironmentReviewResource` | Global search disabled; export action exists. | + +## Existing Tests + +| Test Area | Classification | Repo Source | Notes | +|---|---|---|---| +| Evidence Overview | repo-verified | `apps/platform/tests/Feature/Evidence/EvidenceOverviewPageTest.php`, `EvidenceOverviewWorkspaceHubContractTest.php`, `Spec329EvidenceAuditDisclosureProductizationTest.php` | Existing evidence disclosure behavior. | +| Evidence Snapshot | repo-verified | `apps/platform/tests/Feature/Evidence/*`, `apps/platform/tests/Unit/Evidence/*` | Snapshot generation/resolver/completeness coverage. | +| Stored Reports | repo-verified | `apps/platform/tests/Feature/StoredReports/*`, `apps/platform/tests/Feature/Artifacts/*` | Stored report source/detail/entitlement tests. | +| Review Packs | repo-verified | `apps/platform/tests/Feature/ReviewPack/*`, `ReviewPackAccessBoundaryTest.php` | Generation, download, RBAC, widget, redaction, entitlement. | +| Customer Review Workspace | repo-verified | `apps/platform/tests/Feature/Reviews/*`, `apps/platform/tests/Browser/Reviews/CustomerReviewWorkspaceSmokeTest.php`, `Spec326CustomerReviewWorkspaceProductizationSmokeTest.php` | Existing customer workspace proof/package behavior. | +| Product Process Flow | repo-verified | `apps/platform/tests/Feature/Filament/Spec332ProductProcessFlowSystemTest.php`, browser Spec 332 tests | Shared pattern foundation. | +| Baseline Product Flow consumer | repo-verified | `apps/platform/tests/Feature/Filament/Spec336BaselineCompareProductProcessFlowAlignmentTest.php`, browser Spec 336 test | Completed/adjacent consumer pattern. | +| Spec 337 tests | deferred | planned in `tasks.md` | Not created during prep-only work. | + +## Productization Implications + +- Evidence Overview is the primary place to add the six-step flow. +- Customer Review Workspace is the safest source for customer-safe shareability; Evidence Overview must not infer it from raw evidence alone. +- Review Pack `ready` plus file metadata enables download/export availability, but does not automatically mean auditor-ready. +- StoredReport can be `Available` or `Missing`; generating/failed report states are not repo-backed unless implementation discovers a valid OperationRun relation. +- External delivery is not repo-backed and must be shown as unavailable/deferred if displayed. +- Raw payloads and diagnostics already have collapsed patterns; Spec 337 must preserve and test that behavior. diff --git a/specs/337-evidence-review-pack-product-process-flow-alignment/spec.md b/specs/337-evidence-review-pack-product-process-flow-alignment/spec.md new file mode 100644 index 00000000..b9f4c41d --- /dev/null +++ b/specs/337-evidence-review-pack-product-process-flow-alignment/spec.md @@ -0,0 +1,693 @@ +# Feature Specification: Spec 337 - Evidence Path / Review Pack Product Process Flow Alignment + +**Feature Branch**: `337-evidence-review-pack-product-process-flow-alignment` +**Created**: 2026-05-30 +**Status**: Draft +**Type**: Runtime UX alignment / Product Process Flow consumer / Evidence and Review Pack productization +**Runtime posture**: Reuse existing Spec 332 Product Process Flow system. No backend evidence, report, review-pack, export, queue, or storage rewrite. No new persisted truth. +**Input**: User-provided Spec 336 draft, repo-truth reconciled to Spec 337 because `336-baseline-compare-product-process-flow-alignment` already exists in this repo. + +## Spec Candidate Check *(mandatory - SPEC-GATE-001)* + +- **Problem**: TenantPilot has repo-backed evidence snapshots, stored reports, review packs, customer review workspace signals, OperationRun proof, and signed downloads, but the path from technical evidence to customer- or auditor-consumable output is not expressed as one decision-first readiness flow. +- **Today's failure**: Operators must infer readiness from raw artifact lists, internal report names, timestamps, and generated files instead of seeing what exists, what is missing, what is stale, what is customer-safe, what can be exported, and the next action. +- **User-visible improvement**: Evidence and Review Pack surfaces answer in the first screen: Status, Reason, Impact, Primary next action, Evidence readiness flow, Available proof, Customer-safe output state, Export/download state, and collapsed diagnostics. +- **Smallest enterprise-capable version**: Reuse Spec 332's Product Process Flow pattern on existing Evidence Overview, Review Pack, Stored Report, and Customer Review Workspace evidence sections without changing generation engines or persistence. +- **Explicit non-goals**: New evidence backend, report engine, review-pack engine, storage backend, export format, PDF/ZIP generation logic, OperationRun model changes, migrations, packages, queue/scheduler changes, Customer Review Workspace redesign, Baseline Compare changes, Restore changes, billing/entitlement changes, generic document management. +- **Permanent complexity imported**: A small page/surface presenter only if needed, a feature-local state contract, focused Feature tests, one browser smoke file, and screenshot artifacts. No new tables, enums, status family, or platform workflow framework. +- **Why now**: Restore and Baseline Compare now consume the Product Process Flow pattern; Evidence/Review Packs are the next high-value consumer because they are the customer-safe/auditor-facing continuation of proof and review workflows. +- **Why not local**: Copy-only fixes would leave readiness, proof, customer-safe output, and export states scattered across evidence and review surfaces. A shared flow contract prevents another local readiness dialect. +- **Approval class**: Core Enterprise +- **Red flags triggered**: Customer-safe trust language, audit/evidence disclosure, reachable UI surface change, cross-workspace leakage risk. Defense: use repo-backed states only, collapse raw diagnostics, preserve existing RBAC/policies, and test false-claim prevention. +- **Score**: Nutzen: 2 | Dringlichkeit: 2 | Scope: 1 | Komplexitaet: 1 | Produktnaehe: 2 | Wiederverwendung: 2 | **Gesamt: 10/12** +- **Decision**: approve + +## Spec Scope Fields *(mandatory)* + +- **Scope**: canonical-view plus tenant/environment-owned artifacts. +- **Primary Routes**: + - Evidence Overview: `/admin/evidence/overview` (`admin.evidence.overview`, `App\Filament\Pages\Monitoring\EvidenceOverview`) + - Customer Review Workspace evidence path: `/admin/reviews/workspace` (`App\Filament\Pages\Reviews\CustomerReviewWorkspace`), only if needed for alignment + - Review Pack list/detail: `/admin/workspaces/{workspace}/environments/{environment}/review-packs` + - Review Pack signed download: `/admin/review-packs/{reviewPack}/download` (`admin.review-packs.download`) + - Evidence Snapshot and Stored Report detail surfaces, only as linked proof surfaces + - Environment Review detail/export state, only if needed for review-pack readiness truth +- **Data Ownership**: + - Workspace-owned and environment-owned: `EvidenceSnapshot`, `EvidenceSnapshotItem`, `StoredReport`, `ReviewPack`, `EnvironmentReview`, `EnvironmentReviewSection`, `OperationRun`. + - Signed download/export artifact truth is stored on `ReviewPack.file_disk`, `file_path`, `file_size`, `sha256`, `generated_at`, `expires_at`, and status. + - Customer-safe readiness is derived from existing Customer Review Workspace review/package readiness; no separate `customer_safe` persisted flag is introduced. +- **RBAC**: + - Workspace membership and managed environment access remain mandatory. + - Existing capabilities gate actions and links: `EVIDENCE_VIEW`, `EVIDENCE_MANAGE`, `REVIEW_PACK_VIEW`, `REVIEW_PACK_MANAGE`, `ENVIRONMENT_REVIEW_VIEW`, `ENVIRONMENT_REVIEW_MANAGE`, report-specific capabilities, and OperationRun view capability. + - Non-member or cross-workspace access remains not-found; member-but-missing-capability follows existing hidden/disabled/403 conventions. + +For canonical-view specs, the spec MUST define: + +- **Default filter behavior when tenant-context is active**: preserve current environment filter behavior. Evidence Overview must preselect or preserve the active managed environment when provided, otherwise show authorized workspace-scoped evidence without leaking artifacts from inaccessible environments. +- **Explicit entitlement checks preventing cross-tenant leakage**: all artifact links and generated actions must continue to use existing policies/capability checks and route-model scoping. No `/admin/t` routes or legacy tenant query aliases may be introduced. + +## UI Surface Impact *(mandatory - UI-COV-001)* + +Does this spec add, remove, rename, or materially change any reachable UI surface? + +- [ ] No UI surface impact +- [x] Existing page changed +- [ ] New page/route added +- [ ] Navigation changed +- [ ] Filament panel/provider surface changed +- [ ] New modal/drawer/wizard/action added +- [x] New table/form/state added +- [x] Customer-facing surface changed +- [ ] Dangerous action changed +- [x] Status/evidence/review presentation changed +- [x] Workspace/environment context presentation changed + +If any box except "No UI surface impact" is checked, complete the UI/Productization Coverage section below. "No UI surface impact" MUST NOT be checked together with another impact box; if a guarded file path changes for non-surface reasons, keep only "No UI surface impact" checked and explain the rationale below. + +## UI/Productization Coverage *(mandatory when UI Surface Impact is not "No UI surface impact"; otherwise write `N/A - no reachable UI surface impact` plus rationale)* + +- **Route/page/surface**: Evidence Overview (`App\Filament\Pages\Monitoring\EvidenceOverview`), Customer Review Workspace evidence path (`App\Filament\Pages\Reviews\CustomerReviewWorkspace`, only if needed), Review Pack Resource list/detail, and linked proof surfaces. +- **Current or new page archetype**: Existing evidence/report/review workbench and customer review workspace surfaces; archetypes unchanged. +- **Design depth**: Strategic Surface for Evidence Overview and Customer Review Workspace; Domain Pattern Surface for Review Pack/Stored Report/Evidence Snapshot detail proof. +- **Repo-truth level**: repo-verified (see `repo-truth-map.md`). +- **Existing pattern reused**: Spec 332 Product Process Flow system, Spec 329 evidence disclosure/proof-first workbench, Spec 326 Customer Review Workspace decision/readiness sections, existing OperationRun proof links. +- **New pattern required**: none. A small feature presenter is allowed only if it centralizes repo-backed state mapping and does not become a generic workflow engine. +- **Screenshot required**: yes, under `specs/337-evidence-review-pack-product-process-flow-alignment/artifacts/screenshots/`. +- **Page audit required**: no by default; route/archetype unchanged. Feature-local screenshots and tests are required proof. If implementation changes navigation/archetype, update the UI audit artifacts in the implementation PR. +- **Customer-safe review required**: yes. This spec touches customer/auditor consumption language and must not claim customer-safe, auditor-ready, export-ready, or complete unless repo-backed. +- **Dangerous-action review required**: no destructive action change. Existing generate/export/download/expire semantics remain governed by current actions, confirmations where already required, policies, and audits. +- **Coverage files updated or explicitly not needed**: + - [ ] `docs/ui-ux-enterprise-audit/route-inventory.md` + - [ ] `docs/ui-ux-enterprise-audit/design-coverage-matrix.md` + - [ ] `docs/ui-ux-enterprise-audit/page-reports/...` + - [ ] `docs/ui-ux-enterprise-audit/strategic-surfaces.md` + - [ ] `docs/ui-ux-enterprise-audit/grouped-follow-up-candidates.md` + - [ ] `docs/ui-ux-enterprise-audit/unresolved-pages.md` + - [ ] `N/A - no reachable UI surface impact` +- **No-impact rationale when applicable**: N/A. UI impact exists; coverage artifacts may remain unchanged only if implementation preserves route family, navigation, and archetype. + +## Cross-Cutting / Shared Pattern Reuse *(mandatory when the feature touches notifications, status messaging, action links, header actions, dashboard signals/cards, alerts, navigation entry points, evidence/report viewers, or any other existing shared operator interaction family; otherwise write `N/A - no shared interaction family touched`)* + +- **Cross-cutting feature?**: yes +- **Interaction class(es)**: decision cards, status messaging, Product Process Flow steps, action links, OperationRun proof links, evidence/report/review artifact viewers, collapsed diagnostics. +- **Systems touched**: Spec 332 Product Process Flow system; Evidence Overview proof workbench; Customer Review Workspace evidence path; Review Pack Resource; Stored Report Resource; Evidence Snapshot Resource; OperationRun link helpers. +- **Existing pattern(s) to extend**: Spec 332 process flow, Spec 329 proof-first evidence disclosure, Spec 326 customer review readiness, `OperationRunLinks`, `OperationUxPresenter`, `UiEnforcement`, `BadgeCatalog` / `BadgeRenderer`. +- **Shared contract / presenter / builder / renderer to reuse**: Product Process Flow render conventions from Spec 332. Use existing artifact and badge presenters where available. +- **Why the existing shared path is sufficient or insufficient**: sufficient for step-based readiness and proof separation; insufficient only if current components cannot express the six evidence-chain steps, in which case a bounded renderer extension is allowed. +- **Allowed deviation and why**: no new framework. Feature-local mapping may exist because evidence readiness combines several existing artifacts without a persisted lifecycle table. +- **Consistency impact**: Restore, Baseline Compare, Evidence, and Review Packs use the same Status/Reason/Impact/Next action order, honest unavailable/deferred states, and collapsed diagnostics. +- **Review focus**: no fake customer-safe/auditor-ready/export-ready claims, no duplicate readiness blocks, no raw JSON by default, no cross-workspace links. + +## OperationRun UX Impact *(mandatory when the feature creates, queues, deduplicates, resumes, blocks, completes, or deep-links to an `OperationRun`; otherwise write `N/A - no OperationRun start or link semantics touched`)* + +- **Touches OperationRun start/completion/link UX?**: yes, by presenting existing evidence/report/review-pack generation proof and operation deep links. +- **Shared OperationRun UX contract/layer reused**: existing `OperationRunLinks`, `OperationUxPresenter`, `OperationRunService`, and current service/job operation lifecycle. +- **Delegated start/completion UX behaviors**: existing queued toast, "View operation" links, operation status/outcome, and signed artifact links remain delegated to current services and helpers. +- **Local surface-owned behavior that remains**: state-specific explanation copy and artifact proof placement. +- **Queued DB-notification policy**: no change; no new queued DB notification. +- **Terminal notification path**: unchanged. +- **Exception required?**: none. + +## Provider Boundary / Platform Core Check *(mandatory when the feature changes shared provider/platform seams, identity scope, governed-subject taxonomy, compare strategy selection, provider connection descriptors, or operator vocabulary that may leak provider-specific semantics into platform-core truth; otherwise write `N/A - no shared provider/platform boundary touched`)* + +- **Shared provider/platform boundary touched?**: no +- **Boundary classification**: N/A +- **Seams affected**: N/A +- **Neutral platform terms preserved or introduced**: workspace, environment, review, evidence snapshot, stored report, review pack, customer-safe output, export/download. +- **Provider-specific semantics retained and why**: existing report labels such as Entra admin roles may remain only where repo-backed stored reports already expose them. +- **Why this does not deepen provider coupling accidentally**: the flow is artifact/process based and does not add provider contracts or provider-specific truth. +- **Follow-up path**: none. + +## UI / Surface Guardrail Impact *(mandatory when operator-facing surfaces are changed; otherwise write `N/A`)* + +| Surface / Change | Operator-facing surface change? | Native vs Custom | Shared-Family Relevance | State Layers Touched | Exception Needed? | Low-Impact / `N/A` Note | +|---|---|---|---|---|---|---| +| Evidence Overview: decision card, readiness flow, proof panel, collapsed diagnostics | yes | Native Filament + shared primitives + existing Blade workbench | Product Process Flow, evidence disclosure, OperationRun proof | page, table context, URL query | no | Strategic surface, bounded alignment | +| Customer Review Workspace evidence path: truthful customer-safe/export state alignment, if needed | yes | Native Filament + existing workspace Blade workbench | customer-safe readiness, review pack package state | page, URL query | no | Customer-facing consumption path; only adjust if needed | +| Review Pack Resource list/detail: readiness/export proof copy or placement, if needed | yes | Native Filament resource | review pack artifact proof, signed download | list/detail/header actions | no | No action semantics change | + +## Summary + +Align the Evidence Path and Review Pack experience with the reusable Product Process Flow system introduced in Spec 332. + +TenantPilot already has foundations for: + +- evidence snapshots +- stored reports +- review packs +- tenant/environment reviews +- customer review workspace +- audit log and OperationRun proof +- report export/download + +Spec 337 productizes the visible chain: + +1. Source data selected +2. Evidence snapshot +3. Stored report +4. Review pack +5. Customer-safe output +6. Export / delivery + +The first screen must answer: is this evidence package ready for customer or auditor consumption? + +The answer must include: + +- Status +- Reason +- Impact +- Primary next action +- Evidence readiness flow +- Available proof +- Customer-safe output state +- Export/download state +- Diagnostics collapsed + +## Repo-Truth First Requirement + +Before runtime changes, the spec requires: + +- `specs/337-evidence-review-pack-product-process-flow-alignment/repo-truth-map.md` +- `specs/337-evidence-review-pack-product-process-flow-alignment/evidence-review-pack-state-contract.md` + +These files classify every data point as `repo-verified`, `derived from existing model`, `foundation-real`, `not available`, or `deferred`. No unavailable evidence, report, export, or customer-safe state may be invented. + +## Scope + +### In Scope + +- Evidence Overview readiness/product-flow alignment. +- Review Pack generation/status/readiness presentation. +- Stored Report and Evidence Snapshot proof/readiness links. +- Customer Review Workspace evidence path section only if needed to preserve the same flow language. +- Review Pack export/download readiness. +- OperationRun proof for evidence/report/review-pack generation and export. +- Diagnostics collapsed by default. +- Spec 337 feature tests, browser smoke, and screenshots. +- Spec 337 state contract and repo-truth artifacts. + +### Out of Scope + +- New evidence snapshot backend. +- New report generation engine. +- New review pack engine. +- New storage backend. +- New export format. +- New PDF/ZIP generation logic. +- New OperationRun model changes. +- New migrations unless a later implementation proves they are strictly required and receives explicit review. +- New packages. +- New queue/scheduler changes. +- Customer Review Workspace redesign. +- Baseline Compare changes. +- Restore changes. +- Billing/entitlement changes. +- Generic document management. + +## Product Principles + +- Evidence-first. +- Customer-safe consumption. +- Decision-first. +- Auditability. +- OperationRun proof-aware. +- Workspace/environment isolation. +- Diagnostics collapsed. +- Raw artifact metadata secondary. +- No false customer-safe claim. +- No false auditor-ready claim. +- No false export-ready claim. + +Avoid raw stored-report tables as the main UX, raw evidence IDs as primary labels, export file dumps, technical payload default views, generic readiness claims, customer-facing raw diagnostics, duplicated readiness blocks, and nested card-heavy layouts. + +## Required Product Flow + +Evidence / Review Pack uses the Product Process Flow component. + +Default flow steps: + +1. Source data selected +2. Evidence snapshot +3. Stored report +4. Review pack +5. Customer-safe output +6. Export / delivery + +If the repo only supports part of the chain for a surface, unsupported steps render as unavailable or deferred with honest copy. + +### Step Semantics + +- **Source data selected**: `Available`, `Missing`, `Stale`, `Unavailable` +- **Evidence snapshot**: `Available`, `Missing`, `Generating`, `Failed`, `Stale`, `Unavailable` +- **Stored report**: `Available`, `Missing`, `Generating`, `Failed`, `Unavailable` +- **Review pack**: `Available`, `Required`, `Generating`, `Failed`, `Unavailable` +- **Customer-safe output**: `Ready`, `Needs review`, `Not ready`, `Unavailable` +- **Export / delivery**: `Available`, `Required`, `Generated`, `Failed`, `Unavailable` + +Customer-safe output MUST NOT show `Ready` unless repo truth supports it for the selected surface. Evidence Overview alone does not create a customer-safe state. + +## Evidence / Review Pack Product States + +At minimum, support the following state family where fixtures are repo-backed: + +- Evidence snapshot required. +- Evidence generation in progress. +- Evidence generation failed. +- Stored report required. +- Review pack required. +- Review pack generation in progress. +- Customer-safe review required. +- Customer-safe output ready. +- Review pack export/download available. +- Export unavailable or external delivery not configured. + +The exact copy and flow gate states are defined in `evidence-review-pack-state-contract.md`. + +## Page Layout + +Use a calm main/aside layout where practical. + +Default first screen: + +- Main: decision card, Evidence readiness flow, readiness summary, review-pack contents/coverage, customer-safe output state. +- Aside: evidence proof, evidence snapshot, stored report, review pack, OperationRun proof, export artifact, diagnostics collapsed. + +If an aside convention does not exist on the page, proof state may sit below the decision card but above raw artifact lists. + +## Decision Card + +Required question: + +> Is this evidence package ready for customer or auditor consumption? + +Required fields: + +- Status +- Reason +- Impact +- Primary next action + +One state-specific primary action is visible. Secondary proof/export links remain secondary. + +## Evidence Readiness Flow + +Expected title: `Evidence readiness flow` + +Expected subtitle: `Customer-safe evidence requires source data, evidence snapshot, stored report, review pack, and export readiness.` + +Rules: + +- Use horizontal variant by default if space allows. +- Use vertical variant if the page layout requires stacked flow. +- No duplicate lower readiness blocks. +- No raw artifact placeholders. +- No fake progress. +- No clipped status badges. +- Diagnostics collapsed by default. + +## Evidence Proof Panel + +Create or productize an Evidence Proof panel with these rows: + +- Source data +- Evidence snapshot +- Stored report +- Review pack +- Operation proof +- Export artifact +- Customer-safe state +- Diagnostics + +Allowed presentation states: `Available`, `Unavailable`, `Generating`, `Failed`, `Stale`, `Needs review`, `Ready`, `Not ready`, `Collapsed`. + +Do not claim `Customer-safe`, `Auditor-ready`, `Export-ready`, or `Complete` unless repo truth supports it. + +## Review Pack Contents / Coverage + +If repo-backed, show a compact coverage summary such as: + +- Controls covered +- Findings included +- Accepted risks included +- Evidence snapshots included +- Reports included +- Generated files +- Missing evidence +- Warnings + +Only show metrics from repo truth. If unavailable, show: `Review pack coverage details are not available for this artifact.` + +## Customer-Safe Output + +Separate internal evidence from customer-safe output. + +Allowed states: + +- Customer-safe output ready +- Customer-safe review required +- Customer-safe output unavailable +- Foundation exists, but customer-safe state is not linked + +Customer-safe ready is only allowed when the Customer Review Workspace / Environment Review / current export pack path has a repo-backed ready package and no unresolved readiness blocker for that surface. + +## Export / Delivery + +Show export/download state when repo-backed. + +Allowed states: + +- Export available +- Export required +- Export generating +- Export failed +- Export unavailable + +Actions appear only if authorized: + +- Download export +- Regenerate export +- View operation +- Open stored report + +External delivery is not configured unless a repo-backed mechanism is discovered in implementation. Do not fake email, PSA, sharing, or portal delivery. + +## OperationRun Proof + +Evidence/report/review-pack generation must show OperationRun proof when available: + +- Operation status +- Started at +- Completed at +- Requested by +- Run type +- Result +- Link to operation detail + +If unavailable, show: `Operation proof unavailable. No generation operation is linked to this artifact.` + +## Diagnostics + +Diagnostics are collapsed by default. + +Allowed collapsed sections: + +- Raw report metadata +- Raw evidence payload +- Generation diagnostics +- Export diagnostics +- Provider diagnostics + +Never default-show raw JSON, stack traces, provider secrets, raw OperationRun JSON, or internal exceptions. + +## Navigation / Context + +Preserve workspace/environment/review context. + +Allowed actions, only when route- and capability-backed: + +- Generate evidence snapshot +- Open evidence snapshot +- Open stored report +- Generate review pack +- Open review pack +- Download export +- View operation proof +- Open customer review workspace +- Return to review + +No hardcoded workspace IDs, `/admin/t` routes, legacy tenant aliases, or context-losing proof/export links. + +## RBAC / Capabilities + +Actions only appear when authorized: + +- Generate evidence +- Generate report +- Generate review pack +- Export/download +- Open operation proof +- Open diagnostics +- Open customer review workspace + +Unauthorized users must either not see the action or see an unavailable state if repo conventions support that. No new destructive actions are introduced. + +## Required Tests + +Create: + +`apps/platform/tests/Feature/Filament/Spec337EvidenceReviewPackProductFlowTest.php` + +Required assertions: + +- Missing evidence renders the decision question, `Evidence snapshot required`, the evidence flow, missing snapshot, unavailable review pack, not-ready customer-safe output, collapsed diagnostics, and no raw JSON. +- Evidence snapshot available / report missing shows snapshot available, stored report required, and no fake review-pack ready claim when fixtures support it. +- Review pack required shows stored report available, review pack required, and the generate review pack action only when authorized. +- Review pack available shows review pack available, customer-safe state truthful, export state truthful, and no false auditor-ready claim. +- Operation proof shows linked generation OperationRun, blocks cross-workspace OperationRun leakage, and shows failed OperationRun as failed proof. +- RBAC/context tests prove unauthorized users cannot generate/export, cross-workspace evidence is not visible, and no legacy tenant alias is used. + +## Browser Smoke + +Create: + +`apps/platform/tests/Browser/Spec337EvidenceReviewPackProductFlowSmokeTest.php` + +Browser states: + +- missing evidence snapshot +- evidence generating if fixture-supported +- stored report available / review pack missing +- review pack available if fixture-supported +- export unavailable +- diagnostics collapsed +- dark mode if practical + +Required screenshots: + +- `01-evidence-snapshot-required.png` +- `02-evidence-generating.png` +- `03-stored-report-required.png` +- `04-review-pack-required.png` +- `05-review-pack-available.png` +- `06-customer-safe-output-state.png` +- `07-export-unavailable.png` +- `08-diagnostics-collapsed.png` +- `09-dark-mode.png` + +If a state is not reachable, document why; do not fake screenshots. + +## Acceptance Criteria + +### Product + +- [ ] Evidence / Review Pack uses Product Process Flow. +- [ ] Decision card is visible. +- [ ] Evidence readiness state is clear. +- [ ] Review pack state is clear. +- [ ] Customer-safe output state is truthful. +- [ ] Export/download state is truthful. +- [ ] OperationRun proof is visible where available. +- [ ] Diagnostics collapsed. +- [ ] Raw payload hidden by default. + +### Truth / Safety + +- [ ] No fake evidence claim. +- [ ] No fake customer-safe claim. +- [ ] No fake auditor-ready claim. +- [ ] No fake export-ready claim. +- [ ] No fake coverage counts. +- [ ] OperationRun proof is separated from evidence output. + +### Context / RBAC + +- [ ] Workspace/environment/review isolation preserved. +- [ ] Capabilities respected. +- [ ] No `/admin/t`. +- [ ] No tenant query aliases. +- [ ] No cross-workspace evidence leakage. + +### Validation + +- [ ] Feature tests pass. +- [ ] Browser smoke passes. +- [ ] Screenshots captured or unreached states documented. +- [ ] Pint passes. +- [ ] `git diff --check` passes. + +## Decision-First Surface Role *(mandatory when operator-facing surfaces are changed)* + +| Surface | Decision Role | Human-in-the-loop Moment | Immediately Visible for First Decision | On-Demand Detail / Evidence | Why This Is Primary or Why Not | Workflow Alignment | Attention-load Reduction | +|---|---|---|---|---|---|---|---| +| Evidence Overview | Primary Decision Surface | Operator decides whether evidence/review pack work is ready, missing, stale, generating, failed, or exportable | Status, Reason, Impact, Primary next action, Evidence readiness flow, proof summary, customer-safe/export state | raw report metadata, raw evidence payload, operation diagnostics, artifact lists | Primary because it is the evidence proof workbench | Source data -> evidence -> report -> pack -> customer-safe -> export | Removes cross-page inference from files, IDs, and runs | +| Customer Review Workspace evidence path | Primary customer-safe consumption surface | Operator/customer reviewer decides whether a published review package can be shared or downloaded | Review readiness, evidence path, review pack state, customer-safe state, export availability | diagnostics, operation proof, internal artifact metadata | Primary for customer-safe consumption; only changed if needed | Review -> customer-safe package -> download | Prevents internal evidence from being mistaken for shareable output | +| Review Pack Resource detail | Secondary Evidence / Diagnostics | Operator inspects a generated pack and export proof | pack status, download availability, evidence snapshot link, operation proof | file metadata, summary/options diagnostics | Secondary because it is artifact detail, not the whole readiness chain | Artifact proof follows workbench decision | Keeps artifact detail from becoming the main readiness UX | + +## Audience-Aware Disclosure *(mandatory when operator-facing surfaces are changed)* + +| Surface | Audience Modes In Scope | Decision-First Default-Visible Content | Operator Diagnostics | Support / Raw Evidence | One Dominant Next Action | Hidden / Gated By Default | Duplicate-Truth Prevention | +|---|---|---|---|---|---|---|---| +| Evidence Overview | operator-MSP, manager, support reviewer | decision card, readiness flow, proof panel, customer-safe/export state | generation diagnostics, report metadata, operation failure summary | raw evidence payload, raw report payload, stack traces, raw OperationRun JSON | state-specific action: generate evidence, generate report, generate review pack, view operation, review customer output, or download export | raw/support sections collapsed and capability-aware | one top verdict; lower sections add proof only | +| Customer Review Workspace evidence path | customer-readable, operator-MSP, manager | review readiness, customer-safe state, package/download state, evidence path | internal proof and diagnostics | raw JSON, fingerprints, provider diagnostics, reason ownership | review customer output or download export when repo-backed | diagnostics collapsed; raw/internal detail hidden from customer-safe default path | do not duplicate top readiness with alternate wording | +| Review Pack Resource detail | operator-MSP, support reviewer | pack status, export/download proof, linked snapshot/review/run | options, summary, file metadata, operation proof | raw package metadata and generation diagnostics | download export or view operation | raw diagnostics collapsed/capability-aware | artifact status does not restate customer-safe readiness unless repo-backed | + +## UI/UX Surface Classification *(mandatory when operator-facing surfaces are changed)* + +| Surface | Action Surface Class | Surface Type | Likely Next Operator Action | Primary Inspect/Open Model | Row Click | Secondary Actions Placement | Destructive Actions Placement | Canonical Collection Route | Canonical Detail Route | Scope Signals | Canonical Noun | Critical Truth Visible by Default | Exception Type / Justification | +|---|---|---|---|---|---|---|---|---|---|---|---|---|---| +| Evidence Overview | Workbench / Monitoring | Evidence and review-pack readiness workbench | Take the next generation/review/export action | explicit action links and artifact deep links | N/A | proof panel / aside | none introduced | `/admin/evidence/overview` | N/A | workspace + environment filter | Evidence / Review Pack | Status, Reason, Impact, flow, proof, customer-safe/export state | N/A | +| Customer Review Workspace evidence path | Workbench / Review | Customer-safe review consumption surface | review or download package | workspace page sections + artifact links | N/A | evidence path/proof sections | none introduced | `/admin/reviews/workspace` | N/A | workspace + environment filter | Customer Review | readiness, package availability, evidence path | N/A | +| Review Pack Resource | List / Detail | Artifact list/detail | inspect pack or download export | resource row/detail view | existing resource behavior | row/header actions | existing expire action remains grouped/confirmed | `/admin/workspaces/{workspace}/environments/{environment}/review-packs` | `/admin/workspaces/{workspace}/environments/{environment}/review-packs/{record}` | workspace + environment route | Review Pack | status, generated/export proof, linked evidence/review/run | N/A | + +## Operator Surface Contract *(mandatory when operator-facing surfaces are changed)* + +| Surface | Primary Persona | Decision / Operator Action Supported | Surface Type | Primary Operator Question | Default-visible Information | Diagnostics-only Information | Status Dimensions Used | Mutation Scope | Primary Actions | Dangerous Actions | +|---|---|---|---|---|---|---|---|---|---|---| +| Evidence Overview | Workspace manager / governance operator | Decide whether evidence/review pack readiness requires generation, review, proof inspection, or export | Monitoring / decision-first workbench | Is this evidence package ready for customer or auditor consumption? | decision card, readiness flow, proof panel, customer-safe/export state | raw payload, generation diagnostics, failure details | data availability, evidence lifecycle, report availability, pack lifecycle, customer-safe readiness, export artifact availability, operation status/outcome | TenantPilot artifact generation/export only; no Microsoft tenant mutation introduced | generate/open evidence, open stored report, generate/open review pack, view operation, download export | none introduced | +| Customer Review Workspace evidence path | Customer reviewer / governance operator | Decide whether published review evidence can be shared/downloaded | Review / customer-safe consumption | Is this review package ready to share? | review readiness, evidence path, package/download state | internal diagnostics/proof | review lifecycle, evidence availability, accepted-risk follow-up, package availability | TenantPilot review/export artifacts only | review customer output, download export, open proof | none introduced | +| Review Pack Resource | Governance operator / support reviewer | Inspect generated pack and export proof | Artifact detail | Is this specific pack generated and downloadable? | pack status, file/download proof, linked snapshot/review/operation | options, summary, diagnostics | pack status, expiration, file availability, operation outcome | TenantPilot review-pack artifacts only | download, regenerate if authorized, view operation | existing expire action; no change | + +## Proportionality Review *(mandatory when structural complexity is introduced)* + +- **New source of truth?**: no +- **New persisted entity/table/artifact?**: no +- **New abstraction?**: yes, only a small presenter/view-model if runtime implementation needs one to avoid scattered Blade/Page state mapping. +- **New enum/state/reason family?**: no. Presentation states are derived from existing model statuses and the feature-local state contract. +- **New cross-domain UI framework/taxonomy?**: no. Reuse Spec 332 Product Process Flow. +- **Current operator problem**: evidence, report, review-pack, customer-safe, and export readiness are repo-real but not assembled into one decision-first flow. +- **Existing structure is insufficient because**: current surfaces expose proof and artifacts, but the operator still reconstructs readiness across pages, tables, file state, and operations. +- **Narrowest correct implementation**: derive a single flow model from existing artifacts and render it on the existing surfaces; keep backend generation, storage, OperationRun lifecycle, and route structure unchanged. +- **Ownership cost**: one focused presenter if needed, one feature state contract, feature/browser tests, and screenshots. +- **Alternative intentionally rejected**: a new persisted readiness engine, new artifact lifecycle table, new review-pack workflow service, or a generic document-management layer. +- **Release truth**: current-release UX alignment over existing enterprise evidence foundations. + +### Compatibility posture + +This feature assumes a pre-production environment. + +Backward compatibility, legacy aliases, migration shims, historical fixtures, and compatibility-specific tests are out of scope unless explicitly required by this spec. + +Canonical replacement is preferred over preservation. + +## Testing / Lane / Runtime Impact *(mandatory for runtime behavior changes)* + +- **Test purpose / classification**: Feature + Browser. +- **Validation lane(s)**: confidence + browser. +- **Why this classification and these lanes are sufficient**: the risk is state composition, RBAC/context leakage, truthfulness, and visible product flow rendering, not isolated pure functions or backend engine behavior. +- **New or expanded test families**: `Spec337EvidenceReviewPackProductFlowTest` and `Spec337EvidenceReviewPackProductFlowSmokeTest`. +- **Fixture / helper cost impact**: reuse existing Evidence, ReviewPack, StoredReport, EnvironmentReview, OperationRun, workspace, and managed environment factories/helpers; no new global seeds or heavy defaults. +- **Heavy-family visibility / justification**: one explicit browser smoke file because this changes a strategic/customer-facing evidence path and screenshots are required. +- **Special surface test profile**: monitoring-state-page + shared-detail-family + customer-safe consumption path. +- **Standard-native relief or required special coverage**: dedicated coverage required because this is not a simple native resource list change. +- **Reviewer handoff**: verify lane fit, no hidden fixture growth, no raw diagnostics by default, no false customer-safe/export/auditor-ready copy, and capability-scoped actions. +- **Budget / baseline / trend impact**: none expected beyond one explicit browser smoke. +- **Escalation needed**: document-in-feature if a state is unreachable; follow-up-spec if implementation requires a new persisted readiness engine. +- **Active feature PR close-out entry**: Smoke Coverage. +- **Planned validation commands**: + +```bash +cd apps/platform +./vendor/bin/sail artisan test tests/Feature/Filament/Spec337EvidenceReviewPackProductFlowTest.php --compact +./vendor/bin/sail php vendor/bin/pest tests/Browser/Spec337EvidenceReviewPackProductFlowSmokeTest.php --compact +./vendor/bin/sail artisan test --filter='Evidence|ReviewPack|StoredReport|CustomerReview|ProductProcessFlow' --compact +./vendor/bin/sail pint --dirty +git diff --check +``` + +## User Scenarios & Testing *(mandatory)* + +### User Story 1 - Understand readiness from the first screen (Priority: P1) + +As a governance operator, I need Evidence / Review Pack surfaces to tell me whether a package is ready for customer or auditor consumption without inspecting raw artifacts. + +**Why this priority**: This is the core trust and workflow gap. + +**Independent Test**: Render missing-evidence and ready-package fixtures and assert the decision card, readiness flow, primary next action, proof panel, and diagnostics behavior. + +**Acceptance Scenarios**: + +1. **Given** no evidence snapshot exists for the selected scope, **When** the Evidence / Review Pack surface renders, **Then** it shows `Evidence snapshot required`, marks the evidence step missing, marks review pack unavailable, marks customer-safe output not ready, and keeps diagnostics collapsed. +2. **Given** a ready package with a download-backed review pack exists, **When** the surface renders for an authorized user, **Then** it shows the pack/export state truthfully and does not claim auditor-ready unless the repo-backed customer-safe conditions are satisfied. + +--- + +### User Story 2 - Follow the correct next action (Priority: P2) + +As an operator, I need one primary action per state so I know whether to generate evidence, generate a report, generate a review pack, review customer output, view operation proof, or download export. + +**Why this priority**: Evidence workflows are multi-step; competing CTAs create unsafe exports or repeated generation attempts. + +**Independent Test**: Assert state-specific actions appear only for authorized users and that unauthorized users do not see generate/export actions. + +**Acceptance Scenarios**: + +1. **Given** an evidence snapshot exists but no report exists, **When** the page renders, **Then** the primary action is `Generate stored report` only if repo-supported and authorized. +2. **Given** a stored report exists but no review pack exists, **When** the page renders, **Then** the primary action is `Generate review pack` only for an authorized user. + +--- + +### User Story 3 - Inspect proof without exposing raw internals (Priority: P3) + +As a reviewer, I need OperationRun proof and artifact links while raw diagnostics remain secondary and collapsed. + +**Why this priority**: Auditability requires proof, but customer-safe surfaces must not default to raw provider or payload detail. + +**Independent Test**: Render linked OperationRun fixtures, failed runs, and cross-workspace runs and assert proof visibility, failed-proof copy, cross-workspace exclusion, and raw JSON hidden by default. + +**Acceptance Scenarios**: + +1. **Given** a linked OperationRun exists, **When** the proof panel renders, **Then** it shows status/outcome/timeline/requester/run type and an operation detail link when authorized. +2. **Given** a raw payload exists, **When** the page loads, **Then** raw JSON is not visible until the diagnostics disclosure is opened and only where capability conventions allow. + +## Functional Requirements *(mandatory)* + +- **FR-001**: Evidence / Review Pack surfaces MUST render the question `Is this evidence package ready for customer or auditor consumption?`. +- **FR-002**: The first screen MUST show Status, Reason, Impact, and Primary next action. +- **FR-003**: The Evidence readiness flow MUST use the Product Process Flow pattern and the six default steps. +- **FR-004**: The flow MUST derive step states from repo-backed artifacts only. +- **FR-005**: Customer-safe output MUST NOT be shown as ready unless the selected surface has repo-backed customer-safe/package/download readiness. +- **FR-006**: Export/download state MUST derive from ready, non-expired review packs with stored file metadata and authorized signed download access. +- **FR-007**: OperationRun proof MUST be shown where linked and authorized, and cross-workspace proof MUST NOT leak. +- **FR-008**: Diagnostics MUST be collapsed by default and raw payload/JSON MUST NOT be visible on initial render. +- **FR-009**: Generate/export/download/open actions MUST be capability-aware and preserve workspace/environment/review context. +- **FR-010**: The implementation MUST NOT introduce migrations, packages, env vars, queue/scheduler changes, new backend engines, or new persisted lifecycle truth. + +## Non-Functional Requirements *(mandatory)* + +- **NFR-001**: Page rendering remains database-only and must not call Microsoft Graph or external providers during render. +- **NFR-002**: Existing Filament v5 / Livewire v4 conventions remain intact. +- **NFR-003**: Panel provider registration remains in `apps/platform/bootstrap/providers.php`; no panel provider change is planned. +- **NFR-004**: Globally searchable resources touched by this spec must either have Edit/View pages or keep global search disabled. EvidenceSnapshotResource, ReviewPackResource, StoredReportResource, and EnvironmentReviewResource currently disable global search. +- **NFR-005**: Destructive actions remain confirmed and authorized. This spec introduces no new destructive actions. +- **NFR-006**: No global heavy assets are introduced; no deployment `filament:assets` change is expected. + +## Key Dependencies + +- Spec 332 - Product Process Flow System v1 +- Spec 333 - Restore Create UX Final Productization +- Spec 335 - Restore Run Detail / Post-Execution Proof Productization +- Spec 336 - Baseline Compare Product Process Flow Alignment +- Spec 326 - Customer Review Workspace v1 Productization +- Spec 329 - Evidence / Audit Log Disclosure Productization +- Spec 325 - Screenshot-Anchored Strategic Target Images + +## Follow-Up Specs *(out of scope)* + +- Provider Readiness Productization +- Restore Evidence Export / Auditor Artifact Productization +- Cross-Tenant Compare and Promotion v1 +- Customer Review Workspace Decision/Attestation Polish + +## Implementation Close-Out Report + +When implementation completes, report: + +- Changed behavior. +- Evidence / Review Pack states covered. +- Product Process Flow reuse details. +- Files changed. +- Tests and results. +- Browser screenshots. +- Known gaps and unreachable states. +- Merge readiness. +- Confirmation that no migrations, packages, env vars, queues, scheduler, storage, deployment asset changes, destructive action behavior changes, or false customer-safe/evidence/export claims were introduced. diff --git a/specs/337-evidence-review-pack-product-process-flow-alignment/tasks.md b/specs/337-evidence-review-pack-product-process-flow-alignment/tasks.md new file mode 100644 index 00000000..0878d1cc --- /dev/null +++ b/specs/337-evidence-review-pack-product-process-flow-alignment/tasks.md @@ -0,0 +1,259 @@ +# Tasks: Spec 337 - Evidence Path / Review Pack Product Process Flow Alignment + +- Input: `specs/337-evidence-review-pack-product-process-flow-alignment/spec.md`, `specs/337-evidence-review-pack-product-process-flow-alignment/plan.md` +- Prerequisites: `repo-truth-map.md`, `evidence-review-pack-state-contract.md` +- Preparation status: runtime implementation completed; checkboxes below reflect implementation and validation evidence. + +**Tests**: Required. This changes strategic evidence/review surfaces and customer-safe package readiness presentation. + +## Test Governance Checklist + +- [x] Lane assignment remains explicit and narrowest sufficient (Feature + Browser). +- [x] Browser coverage stays single-file and scenario-scoped. +- [x] No new default-heavy helpers/factories/seeds are introduced; reuse existing fixture helpers. +- [x] Validation commands remain minimal and directly prove the changed contract. +- [x] Any unreachable state resolves as `document-in-feature` instead of fake screenshots or fake data. + +## Phase 1: Preparation And Repo Truth + +**Purpose**: Confirm repo truth and lock the state contract before runtime edits. + +- [x] T001 Re-read `spec.md`, `plan.md`, this `tasks.md`, `repo-truth-map.md`, and `evidence-review-pack-state-contract.md`. +- [x] T002 Confirm working tree intent and record baseline commit (`git status`, `git log -1`). +- [x] T003 Re-verify related specs and guardrails: + - `specs/332-product-process-flow-system-v1/` + - `specs/326-customer-review-workspace-v1-productization/` + - `specs/329-evidence-audit-log-disclosure-productization/` + - `specs/336-baseline-compare-product-process-flow-alignment/` + - `.specify/memory/constitution.md` + - `docs/ai-coding-rules.md` + - `docs/filament-guidelines.md` + - `docs/security-guidelines.md` + - `docs/testing-guidelines.md` +- [x] T004 Re-verify repo truth sources and step semantics: + - `apps/platform/app/Models/EvidenceSnapshot.php` + - `apps/platform/app/Models/StoredReport.php` + - `apps/platform/app/Models/ReviewPack.php` + - `apps/platform/app/Models/EnvironmentReview.php` + - `apps/platform/app/Models/OperationRun.php` + - `apps/platform/app/Filament/Pages/Monitoring/EvidenceOverview.php` + - `apps/platform/resources/views/filament/pages/monitoring/evidence-overview.blade.php` + - `apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php` + - `apps/platform/resources/views/filament/pages/reviews/customer-review-workspace.blade.php` + - `apps/platform/app/Filament/Resources/ReviewPackResource.php` + - `apps/platform/app/Filament/Resources/StoredReportResource.php` + - `apps/platform/app/Filament/Resources/EvidenceSnapshotResource.php` + - `apps/platform/app/Filament/Resources/EnvironmentReviewResource.php` + - `apps/platform/app/Services/Evidence/EvidenceSnapshotService.php` + - `apps/platform/app/Services/ReviewPackService.php` + - `apps/platform/app/Http/Controllers/ReviewPackDownloadController.php` +- [x] T005 Update `repo-truth-map.md` and `evidence-review-pack-state-contract.md` if implementation-time code differs from the prepared truth. No update required; implementation stayed within the prepared derived-state contract. +- [x] T006 Confirm Product Process Flow rendering conventions from Spec 332 and decide reuse strategy before editing UI. + +## Phase 2: Presenter / Flow Model + +**Purpose**: Centralize "what exists, what is missing, what is customer-safe, and what can be exported" without adding persisted truth. + +- [x] T007 Decide whether a small `EvidenceReviewPackPresenter` is needed or whether existing page payload builders can produce the flow model cleanly. +- [x] T008 Implement the narrowest derived-only mapping for: + - decision card (`Status`, `Reason`, `Impact`, `Primary next action`) + - six readiness flow steps + - proof items + - coverage/contents summary + - customer-safe state + - export/download state + - diagnostics default state +- [x] T009 Ensure mapping uses existing models/statuses only and introduces no new enum/status/reason family. +- [x] T010 Ensure primary next action is exactly one per state and capability-aware. +- [x] T011 Ensure unsupported states render as unavailable/deferred with honest copy. + +## Phase 3: Evidence Overview UI Alignment + +**Purpose**: Make Evidence Overview the decision-first evidence readiness workbench. + +- [x] T012 Add the decision question: `Is this evidence package ready for customer or auditor consumption?` +- [x] T013 Render `Status`, `Reason`, `Impact`, and `Primary next action` before raw artifact lists. +- [x] T014 Render `Evidence readiness flow` with Product Process Flow steps: + - Source data selected + - Evidence snapshot + - Stored report + - Review pack + - Customer-safe output + - Export / delivery +- [x] T015 Productize the Evidence Proof panel with rows for source data, snapshot, stored report, review pack, operation proof, export artifact, customer-safe state, and diagnostics. +- [x] T016 Keep raw artifact inventory secondary and diagnostics collapsed by default. +- [x] T017 Remove or avoid duplicated readiness/verdict blocks below the decision card. +- [x] T018 Ensure badges/status labels remain readable in light and dark mode. + +## Phase 4: Review Pack / Customer Review Workspace / Export States + +**Purpose**: Productize only repo-backed customer-safe and export states. + +- [x] T019 Align Review Pack Resource list/detail copy or proof placement only where needed for state truth. No runtime change required; existing resource state/download semantics already matched the repo-truth contract. +- [x] T020 Align Customer Review Workspace evidence path only if current copy conflicts with the Spec 337 state contract. No runtime change required; existing customer-safe workspace tests remain the source of customer-safe readiness truth. +- [x] T021 Derive review-pack available/generating/failed/expired states from `ReviewPack.status`, `expires_at`, and file metadata. +- [x] T022 Derive export/download available only from ready, non-expired packs with `file_disk`, `file_path`, and authorized signed download. +- [x] T023 Render external delivery as unavailable unless a repo-backed delivery mechanism exists. +- [x] T024 Derive customer-safe output ready only from Customer Review Workspace / Environment Review readiness that is already repo-backed. +- [x] T025 Show coverage/contents metrics only if they exist in review/evidence/report summary data. + +## Phase 5: OperationRun Proof / RBAC / Context / Diagnostics + +**Purpose**: Preserve auditability and tenancy safety while hiding raw internals by default. + +- [x] T026 Show OperationRun proof when linked and authorized: + - status + - started/completed timestamps + - requested by / initiator + - run type + - result/outcome + - operation detail link +- [x] T027 Show failed linked OperationRuns as failed proof, not as usable evidence output. +- [x] T028 Prevent cross-workspace/environment OperationRun and artifact links. +- [x] T029 Preserve workspace/environment/review query context in all secondary links. +- [x] T030 Keep diagnostics collapsed by default and hide raw JSON, raw payloads, stack traces, and internal exceptions on first render. +- [x] T031 Respect existing capabilities for generate evidence, generate report, generate review pack, export/download, open operation proof, open diagnostics, and open Customer Review Workspace. +- [x] T032 Do not add destructive actions; preserve confirmation and authorization on existing destructive/high-impact actions. + +## Phase 6: Feature Tests (Pest) + +- [x] T033 Add `apps/platform/tests/Feature/Filament/Spec337EvidenceReviewPackProductFlowTest.php`. +- [x] T034 Test missing evidence: + - decision question renders + - `Evidence snapshot required` + - flow visible + - evidence snapshot marked missing + - review pack unavailable + - customer-safe output not ready + - diagnostics collapsed + - no raw JSON visible +- [x] T035 Test evidence snapshot available / report missing when fixture-supported: + - evidence snapshot available + - stored report required + - no fake review pack ready claim +- [x] T036 Test review pack required when fixture-supported: + - stored report available + - review pack required + - generate review pack primary action only if authorized +- [x] T037 Test review pack available when fixture-supported: + - review pack available + - customer-safe state truthful + - export state truthful + - no false auditor-ready claim +- [x] T038 Test OperationRun proof: + - generation OperationRun visible when linked + - no cross-workspace OperationRun leak + - failed OperationRun shown as failed proof +- [x] T039 Test RBAC/context: + - unauthorized user cannot generate/export + - cross-workspace evidence not visible + - no legacy tenant alias +- [x] T040 Update existing Evidence/ReviewPack/CustomerReview tests only where assertions are strengthened. + +## Phase 7: Browser Smoke + Screenshots + +- [x] T041 Add `apps/platform/tests/Browser/Spec337EvidenceReviewPackProductFlowSmokeTest.php`. +- [x] T042 Cover browser states: + - missing evidence snapshot + - evidence generating if fixture-supported + - stored report available / review pack missing + - review pack available if fixture-supported + - export unavailable + - diagnostics collapsed + - dark mode if practical +- [x] T043 Assert in browser: + - Evidence readiness flow visible + - decision card visible + - proof panel visible + - customer-safe state visible + - raw payload hidden + - primary next action visible + - badges readable +- [x] T044 Capture screenshots into `specs/337-evidence-review-pack-product-process-flow-alignment/artifacts/screenshots/`: + - `01-evidence-snapshot-required.png` + - `02-evidence-generating.png` + - `03-stored-report-required.png` + - `04-review-pack-required.png` + - `05-review-pack-available.png` + - `06-customer-safe-output-state.png` + - `07-export-unavailable.png` + - `08-diagnostics-collapsed.png` + - `09-dark-mode.png` +- [x] T045 If a state is unreachable, document the repo-truth reason in implementation close-out. All required screenshot states were reachable with repo-backed fixtures. + +## Phase 8: Validation + +- [x] T046 Run narrow Feature tests: + +```bash +cd apps/platform +./vendor/bin/sail artisan test tests/Feature/Filament/Spec337EvidenceReviewPackProductFlowTest.php --compact +``` + +- [x] T047 Run browser smoke: + +```bash +cd apps/platform +./vendor/bin/sail php vendor/bin/pest tests/Browser/Spec337EvidenceReviewPackProductFlowSmokeTest.php --compact +``` + +- [x] T048 Run overlapping guard filters. Command ran; unrelated dashboard/restore/customer-review failures reproduced individually and are documented in close-out: + +```bash +cd apps/platform +./vendor/bin/sail artisan test --filter='Evidence|ReviewPack|StoredReport|CustomerReview|ProductProcessFlow' --compact +``` + +- [x] T049 Run formatting and whitespace checks: + +```bash +cd apps/platform +./vendor/bin/sail pint --dirty +git diff --check +``` + +- [x] T050 Report full-suite status honestly if not run. + +## Final Report Template + +When implementation completes, report: + +```text +Spec 337 completed. + +Changed behavior: +... + +Evidence / Review Pack states: +- Evidence missing: +- Evidence generating: +- Stored report required: +- Review pack required: +- Review pack available: +- Customer-safe state: +- Export state: + +Product Process Flow: +... + +Files changed: +... + +Tests: +- command: +- result: + +Browser screenshots: +... + +Known gaps: +... + +Merge readiness: +... + +No migrations were created. +No packages, env vars, queues, scheduler, storage, or deployment asset changes were made. +No destructive action behavior was changed. +No false customer-safe/evidence/export claims were introduced. +```