diff --git a/apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php b/apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php index ea0db69c..e2a782fe 100644 --- a/apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php +++ b/apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php @@ -32,9 +32,12 @@ use App\Support\Navigation\WorkspaceHubEnvironmentFilter; use App\Support\Navigation\WorkspaceHubNavigation; use App\Support\OperationRunLinks; -use App\Support\ReviewPackStatus; +use App\Support\ResolutionGuidance\Adapters\ReviewPackOutputResolutionAdapter; +use App\Support\ResolutionGuidance\ResolutionAction; +use App\Support\ResolutionGuidance\ResolutionCase; use App\Support\ReviewPacks\ReviewPackOutputReadiness; use App\Support\ReviewPacks\ReviewPackOutputResolutionGuidance; +use App\Support\ReviewPackStatus; use App\Support\Ui\ActionSurface\ActionSurfaceDeclaration; use App\Support\Ui\ActionSurface\Enums\ActionSurfaceInspectAffordance; use App\Support\Ui\ActionSurface\Enums\ActionSurfaceProfile; @@ -380,6 +383,7 @@ public function latestReviewConsumptionPayload(): ?array reviewUrl: $reviewUrl, evidenceUrl: $evidenceUrl, ); + $resolutionCase = $this->reviewOutputResolutionCaseForReview($review, $outputGuidance); $decision = $this->decisionSummaryForReview($review); $acceptedRisks = $this->acceptedRisksForReview($review); $hasAcceptedRiskFollowUp = $this->acceptedRiskFollowUpRequiredForReview($review); @@ -391,6 +395,7 @@ public function latestReviewConsumptionPayload(): ?array packageAvailability: $packageAvailability, outputReadiness: $outputReadiness, outputGuidance: $outputGuidance, + resolutionCase: $resolutionCase, downloadUrl: $downloadUrl, reviewUrl: $reviewUrl, evidenceUrl: $evidenceUrl, @@ -644,7 +649,8 @@ private function reviewScopePayload(ManagedEnvironment $tenant): array * primary_action_icon:string, * secondary_action_label:?string, * secondary_action_url:?string, - * secondary_actions:list, + * secondary_actions:list, + * resolution_case:array, * output_guidance:array * } */ @@ -654,6 +660,7 @@ private function reviewReadinessForTenant( array $packageAvailability, array $outputReadiness, array $outputGuidance, + array $resolutionCase, ?string $downloadUrl, ?string $reviewUrl, ?string $evidenceUrl, @@ -677,25 +684,19 @@ private function reviewReadinessForTenant( evidenceUrl: $evidenceUrl, ); $followUpOverride = in_array($reasonCode, ['findings_follow_up_required', 'accepted_risk_follow_up_required'], true); - $secondaryActions = $followUpOverride - ? collect([ - $actions['secondary_url'] !== null && $actions['secondary_label'] !== null - ? [ - 'label' => $actions['secondary_label'], - 'url' => $actions['secondary_url'], - 'kind' => 'environment_link', - 'icon' => 'heroicon-o-arrow-top-right-on-square', - ] - : null, - ])->filter()->values()->all() - : (is_array($outputGuidance['secondary_actions'] ?? null) ? $outputGuidance['secondary_actions'] : []); - $primaryAction = $followUpOverride - ? [ - 'label' => $actions['primary_label'], - 'url' => $actions['primary_url'], - 'icon' => $actions['primary_icon'], - ] - : (is_array($outputGuidance['primary_action'] ?? null) ? $outputGuidance['primary_action'] : null); + $presentedResolutionCase = $followUpOverride + ? $this->workspaceFollowUpResolutionCase( + baseCase: $resolutionCase, + effectiveState: $effectiveState, + reasonCode: $reasonCode, + outputReadiness: $outputReadiness, + findingPanel: $findingPanel, + packageAvailability: $packageAvailability, + actions: $actions, + ) + : $resolutionCase; + $primaryAction = is_array($presentedResolutionCase['primary_action'] ?? null) ? $presentedResolutionCase['primary_action'] : null; + $secondaryActions = is_array($presentedResolutionCase['secondary_actions'] ?? null) ? $presentedResolutionCase['secondary_actions'] : []; return [ 'question' => __('localization.review.review_pack_output_status'), @@ -711,26 +712,15 @@ private function reviewReadinessForTenant( 'boundary_color' => $followUpOverride ? $this->workspaceBoundaryColor((string) ($outputReadiness['customer_safe_state'] ?? 'requires_review')) : (string) ($outputGuidance['boundary_color'] ?? $this->workspaceBoundaryColor((string) ($outputReadiness['customer_safe_state'] ?? 'requires_review'))), - 'reason' => $followUpOverride - ? $this->workspaceReadinessReason( - reasonCode: $reasonCode, - outputReadiness: $outputReadiness, - findingPanel: $findingPanel, - packageAvailability: $packageAvailability, - ) - : (string) ($outputGuidance['primary_reason'] ?? $packageAvailability['description']), - 'impact' => $followUpOverride - ? $this->workspaceReadinessImpact( - state: $effectiveState, - reasonCode: $reasonCode, - ) - : (string) ($outputGuidance['impact'] ?? $this->workspaceReadinessImpact(state: $effectiveState, reasonCode: $reasonCode)), + 'reason' => (string) ($presentedResolutionCase['reason'] ?? $outputGuidance['primary_reason'] ?? $packageAvailability['description']), + 'impact' => (string) ($presentedResolutionCase['impact'] ?? $outputGuidance['impact'] ?? $this->workspaceReadinessImpact(state: $effectiveState, reasonCode: $reasonCode)), 'primary_action_label' => (string) ($primaryAction['label'] ?? $actions['primary_label']), 'primary_action_url' => $primaryAction['url'] ?? $actions['primary_url'], 'primary_action_icon' => (string) ($primaryAction['icon'] ?? $actions['primary_icon']), 'secondary_action_label' => $secondaryActions[0]['label'] ?? null, 'secondary_action_url' => $secondaryActions[0]['url'] ?? null, 'secondary_actions' => $secondaryActions, + 'resolution_case' => $presentedResolutionCase, 'output_guidance' => $outputGuidance, ]; } @@ -2324,6 +2314,19 @@ private function reviewOutputGuidanceForReview( ); } + /** + * @param array $outputGuidance + * @return array + */ + private function reviewOutputResolutionCaseForReview(EnvironmentReview $review, array $outputGuidance): array + { + return ReviewPackOutputResolutionAdapter::fromGuidance( + review: $review, + guidance: $outputGuidance, + sourceSurface: self::SOURCE_SURFACE, + ); + } + private function reviewPackHasReadyExport(?ReviewPack $pack): bool { if (! $pack instanceof ReviewPack) { @@ -2439,6 +2442,71 @@ private function workspaceReadinessImpact(string $state, string $reasonCode): st }; } + /** + * @param array $baseCase + * @param array $outputReadiness + * @param array{summary:string} $findingPanel + * @param array{state:string,label:string,description:string} $packageAvailability + * @param array{primary_label:string,primary_url:?string,primary_icon:string,secondary_label:?string,secondary_url:?string} $actions + * @return array + */ + private function workspaceFollowUpResolutionCase( + array $baseCase, + string $effectiveState, + string $reasonCode, + array $outputReadiness, + array $findingPanel, + array $packageAvailability, + array $actions, + ): array { + $primaryAction = ResolutionAction::fromArray([ + 'key' => 'customer_review_workspace.'.$reasonCode.'.primary_action', + 'label' => $actions['primary_label'], + 'url' => $actions['primary_url'], + 'icon' => $actions['primary_icon'], + 'kind' => str_starts_with($actions['primary_icon'], 'heroicon-o-arrow-down-tray') + ? 'download' + : 'environment_link', + ], 'customer_review_workspace.'.$reasonCode.'.primary_action', $actions['primary_label']); + + $secondaryActions = $actions['secondary_url'] !== null && $actions['secondary_label'] !== null + ? [ + ResolutionAction::fromArray([ + 'key' => 'customer_review_workspace.'.$reasonCode.'.secondary_action', + 'label' => $actions['secondary_label'], + 'url' => $actions['secondary_url'], + 'icon' => 'heroicon-o-arrow-top-right-on-square', + 'kind' => str_contains(strtolower($actions['secondary_label']), 'download') + ? 'download' + : 'environment_link', + ], 'customer_review_workspace.'.$reasonCode.'.secondary_action', $actions['secondary_label']), + ] + : []; + + return ResolutionCase::make( + key: 'customer_review_workspace.'.$reasonCode, + scope: is_array($baseCase['scope'] ?? null) ? $baseCase['scope'] : [], + severity: 'warning', + status: 'action_required', + title: $this->workspaceReadinessLabel($effectiveState), + reason: $this->workspaceReadinessReason( + reasonCode: $reasonCode, + outputReadiness: $outputReadiness, + findingPanel: $findingPanel, + packageAvailability: $packageAvailability, + ), + impact: $this->workspaceReadinessImpact( + state: $effectiveState, + reasonCode: $reasonCode, + ), + primaryAction: $primaryAction, + secondaryActions: $secondaryActions, + sourceRefs: is_array($baseCase['source_refs'] ?? null) ? $baseCase['source_refs'] : [], + evidenceRefs: is_array($baseCase['evidence_refs'] ?? null) ? $baseCase['evidence_refs'] : [], + technicalDetails: is_array($baseCase['technical_details'] ?? null) ? $baseCase['technical_details'] : [], + ); + } + /** * @return array{ * primary_label:string, diff --git a/apps/platform/app/Filament/Resources/EnvironmentReviewResource.php b/apps/platform/app/Filament/Resources/EnvironmentReviewResource.php index ad5017a1..5099e5b7 100644 --- a/apps/platform/app/Filament/Resources/EnvironmentReviewResource.php +++ b/apps/platform/app/Filament/Resources/EnvironmentReviewResource.php @@ -32,8 +32,9 @@ use App\Support\OpsUx\OperationUxPresenter; use App\Support\Rbac\UiEnforcement; use App\Support\ReasonTranslation\ReasonPresenter; -use App\Support\ReviewPackStatus; +use App\Support\ResolutionGuidance\Adapters\ReviewPackOutputResolutionAdapter; use App\Support\ReviewPacks\ReviewPackOutputResolutionGuidance; +use App\Support\ReviewPackStatus; use App\Support\Ui\ActionSurface\ActionSurfaceDeclaration; use App\Support\Ui\ActionSurface\Enums\ActionSurfaceInspectAffordance; use App\Support\Ui\ActionSurface\Enums\ActionSurfaceProfile; @@ -1083,6 +1084,13 @@ public static function outputGuidanceState(EnvironmentReview $record): array 'evidence' => $evidenceUrl, 'operation' => $operationUrl, ]); + $guidance['resolution_case'] = ReviewPackOutputResolutionAdapter::fromGuidance( + review: $record, + guidance: $guidance, + sourceSurface: static::isCustomerWorkspaceMode() + ? 'environment_review_detail.customer_workspace' + : 'environment_review_detail', + ); if (! static::isCustomerWorkspaceMode()) { return $guidance; diff --git a/apps/platform/app/Support/ResolutionGuidance/Adapters/ReviewPackOutputResolutionAdapter.php b/apps/platform/app/Support/ResolutionGuidance/Adapters/ReviewPackOutputResolutionAdapter.php new file mode 100644 index 00000000..19f07a01 --- /dev/null +++ b/apps/platform/app/Support/ResolutionGuidance/Adapters/ReviewPackOutputResolutionAdapter.php @@ -0,0 +1,203 @@ + $guidance + * @return array{ + * key:string, + * scope:array, + * severity:string, + * status:string, + * title:string, + * reason:string, + * impact:string, + * primary_action:array{ + * key:string, + * label:string, + * type:string, + * url:?string, + * icon:string, + * kind:string, + * capability:?string, + * requires_confirmation:bool, + * audit_event:?string, + * operation_run_type:?string, + * disabled_reason:?string + * }, + * secondary_actions:list, + * source_refs:list, + * evidence_refs:list, + * technical_details:array + * } + */ + public static function fromGuidance(EnvironmentReview $review, array $guidance, string $sourceSurface): array + { + $scope = array_filter([ + 'type' => 'review_pack', + 'workspace_id' => (int) $review->workspace_id, + 'managed_environment_id' => (int) $review->managed_environment_id, + 'environment_review_id' => (int) $review->getKey(), + 'review_pack_id' => $review->currentExportReviewPack instanceof ReviewPack + ? (int) $review->currentExportReviewPack->getKey() + : '', + 'source_surface' => $sourceSurface, + ], static fn (mixed $value): bool => $value !== null && $value !== ''); + + $primaryAction = ResolutionAction::fromArray( + is_array($guidance['primary_action'] ?? null) ? $guidance['primary_action'] : null, + self::caseKey((string) ($guidance['state'] ?? ReviewPackOutputResolutionGuidance::STATE_UNKNOWN)).'.primary_action', + __('localization.review.review_output_limitations'), + ); + + return ResolutionCase::make( + key: self::caseKey((string) ($guidance['state'] ?? ReviewPackOutputResolutionGuidance::STATE_UNKNOWN)), + scope: $scope, + severity: self::severity((string) ($guidance['state'] ?? ReviewPackOutputResolutionGuidance::STATE_UNKNOWN)), + status: self::status((string) ($guidance['state'] ?? ReviewPackOutputResolutionGuidance::STATE_UNKNOWN)), + title: (string) ($guidance['label'] ?? __('localization.review.requires_review')), + reason: (string) ($guidance['primary_reason'] ?? __('localization.review.review_pack_with_limitations_description')), + impact: (string) ($guidance['impact'] ?? __('localization.review.published_with_limitations_impact')), + primaryAction: $primaryAction, + secondaryActions: self::secondaryActions($guidance), + sourceRefs: self::sourceRefs($review), + evidenceRefs: self::evidenceRefs($review), + technicalDetails: self::technicalDetails($guidance), + ); + } + + /** + * @param array $guidance + * @return list + */ + private static function secondaryActions(array $guidance): array + { + $secondaryActions = is_array($guidance['secondary_actions'] ?? null) ? $guidance['secondary_actions'] : []; + + return array_values(array_map( + static fn (array $action, int $index): array => ResolutionAction::fromArray( + $action, + self::caseKey((string) ($guidance['state'] ?? ReviewPackOutputResolutionGuidance::STATE_UNKNOWN)).'.secondary_action_'.$index, + __('localization.review.review_output_limitations'), + ), + array_values(array_filter($secondaryActions, static fn (mixed $action): bool => is_array($action))), + array_keys(array_values(array_filter($secondaryActions, static fn (mixed $action): bool => is_array($action)))), + )); + } + + /** + * @return list + */ + private static function sourceRefs(EnvironmentReview $review): array + { + $refs = [ + [ + 'type' => 'environment_review', + 'id' => (int) $review->getKey(), + ], + ]; + + if ($review->currentExportReviewPack instanceof ReviewPack) { + $refs[] = [ + 'type' => 'review_pack', + 'id' => (int) $review->currentExportReviewPack->getKey(), + ]; + } + + if ($review->operationRun !== null) { + $refs[] = [ + 'type' => 'operation_run', + 'id' => (int) $review->operationRun->getKey(), + ]; + } + + return $refs; + } + + /** + * @return list + */ + private static function evidenceRefs(EnvironmentReview $review): array + { + if (! $review->evidenceSnapshot instanceof EvidenceSnapshot) { + return []; + } + + return [[ + 'type' => 'evidence_snapshot', + 'id' => (int) $review->evidenceSnapshot->getKey(), + ]]; + } + + /** + * @param array $guidance + * @return array + */ + private static function technicalDetails(array $guidance): array + { + return array_filter( + is_array($guidance['technical_details'] ?? null) ? $guidance['technical_details'] : [], + static fn (mixed $value): bool => is_string($value) && $value !== '', + ); + } + + private static function caseKey(string $state): string + { + return 'review_output.'.$state; + } + + private static function severity(string $state): string + { + return match ($state) { + ReviewPackOutputResolutionGuidance::STATE_CUSTOMER_SAFE_READY => 'success', + ReviewPackOutputResolutionGuidance::STATE_PUBLICATION_BLOCKED => 'critical', + default => 'warning', + }; + } + + private static function status(string $state): string + { + return match ($state) { + ReviewPackOutputResolutionGuidance::STATE_CUSTOMER_SAFE_READY => 'ready', + ReviewPackOutputResolutionGuidance::STATE_PUBLICATION_BLOCKED, + ReviewPackOutputResolutionGuidance::STATE_EXPORT_NOT_READY => 'blocked', + ReviewPackOutputResolutionGuidance::STATE_UNKNOWN => 'unknown', + default => 'action_required', + }; + } +} diff --git a/apps/platform/app/Support/ResolutionGuidance/ResolutionAction.php b/apps/platform/app/Support/ResolutionGuidance/ResolutionAction.php new file mode 100644 index 00000000..d7c6fca1 --- /dev/null +++ b/apps/platform/app/Support/ResolutionGuidance/ResolutionAction.php @@ -0,0 +1,206 @@ + $key, + 'label' => $label, + 'type' => $type, + 'url' => $url, + 'icon' => $icon, + 'kind' => $kind, + 'capability' => $capability, + 'requires_confirmation' => $requiresConfirmation, + 'audit_event' => $auditEvent, + 'operation_run_type' => $operationRunType, + 'disabled_reason' => $disabledReason, + ]; + } + + /** + * @return array{ + * key:string, + * label:string, + * type:string, + * url:null, + * icon:string, + * kind:string, + * capability:null, + * requires_confirmation:false, + * audit_event:null, + * operation_run_type:null, + * disabled_reason:?string + * } + */ + public static function none(string $key, string $label, ?string $disabledReason = null): array + { + return [ + 'key' => $key, + 'label' => $label, + 'type' => self::TYPE_NONE, + 'url' => null, + 'icon' => self::iconForType(self::TYPE_NONE), + 'kind' => 'none', + 'capability' => null, + 'requires_confirmation' => false, + 'audit_event' => null, + 'operation_run_type' => null, + 'disabled_reason' => $disabledReason, + ]; + } + + private static function typeFromKind(?string $kind, ?string $url): string + { + return match ($kind) { + 'download' => self::TYPE_DOWNLOAD, + 'disclosure' => self::TYPE_DISCLOSURE, + 'none' => self::TYPE_NONE, + default => $url !== null ? self::TYPE_NAVIGATION : self::TYPE_NONE, + }; + } + + private static function fallbackType(?string $kind, ?string $url): string + { + return match (true) { + $kind === 'download' => self::TYPE_DOWNLOAD, + $kind === 'disclosure' => self::TYPE_DISCLOSURE, + $url !== null => self::TYPE_NAVIGATION, + default => self::TYPE_NONE, + }; + } + + private static function kindFromType(string $type, ?string $kind): string + { + if (is_string($kind) && $kind !== '') { + return $kind; + } + + return match ($type) { + self::TYPE_DOWNLOAD => 'download', + self::TYPE_DISCLOSURE => 'disclosure', + self::TYPE_NONE => 'none', + default => 'environment_link', + }; + } + + private static function iconForType(string $type): string + { + return match ($type) { + self::TYPE_DOWNLOAD => 'heroicon-o-arrow-down-tray', + self::TYPE_DISCLOSURE => 'heroicon-o-information-circle', + self::TYPE_NONE => 'heroicon-o-minus-circle', + default => 'heroicon-o-arrow-top-right-on-square', + }; + } + + private static function isUnsafeExecutable( + string $type, + ?string $capability, + ?string $auditEvent, + bool $requiresConfirmation, + ?string $operationRunType, + ): bool { + if (! in_array($type, [self::TYPE_DOMAIN_ACTION, self::TYPE_OPERATION_ACTION], true)) { + return false; + } + + if ($capability === null || $auditEvent === null) { + return true; + } + + if (! $requiresConfirmation) { + return true; + } + + return $type === self::TYPE_OPERATION_ACTION && $operationRunType === null; + } +} diff --git a/apps/platform/app/Support/ResolutionGuidance/ResolutionCase.php b/apps/platform/app/Support/ResolutionGuidance/ResolutionCase.php new file mode 100644 index 00000000..025cef64 --- /dev/null +++ b/apps/platform/app/Support/ResolutionGuidance/ResolutionCase.php @@ -0,0 +1,108 @@ + $scope + * @param array{ + * key:string, + * label:string, + * type:string, + * url:?string, + * icon:string, + * kind:string, + * capability:?string, + * requires_confirmation:bool, + * audit_event:?string, + * operation_run_type:?string, + * disabled_reason:?string + * } $primaryAction + * @param list $secondaryActions + * @param list $sourceRefs + * @param list $evidenceRefs + * @param array $technicalDetails + * @return array{ + * key:string, + * scope:array, + * severity:string, + * status:string, + * title:string, + * reason:string, + * impact:string, + * primary_action:array{ + * key:string, + * label:string, + * type:string, + * url:?string, + * icon:string, + * kind:string, + * capability:?string, + * requires_confirmation:bool, + * audit_event:?string, + * operation_run_type:?string, + * disabled_reason:?string + * }, + * secondary_actions:list, + * source_refs:list, + * evidence_refs:list, + * technical_details:array + * } + */ + public static function make( + string $key, + array $scope, + string $severity, + string $status, + string $title, + string $reason, + string $impact, + array $primaryAction, + array $secondaryActions = [], + array $sourceRefs = [], + array $evidenceRefs = [], + array $technicalDetails = [], + ): array { + return [ + 'key' => $key, + 'scope' => array_filter($scope, static fn (mixed $value): bool => $value !== ''), + 'severity' => $severity, + 'status' => $status, + 'title' => $title, + 'reason' => $reason, + 'impact' => $impact, + 'primary_action' => $primaryAction, + 'secondary_actions' => array_values($secondaryActions), + 'source_refs' => array_values($sourceRefs), + 'evidence_refs' => array_values($evidenceRefs), + 'technical_details' => $technicalDetails, + ]; + } +} diff --git a/apps/platform/app/Support/ReviewPacks/ReviewPackOutputResolutionGuidance.php b/apps/platform/app/Support/ReviewPacks/ReviewPackOutputResolutionGuidance.php index f5f6b750..4afb5324 100644 --- a/apps/platform/app/Support/ReviewPacks/ReviewPackOutputResolutionGuidance.php +++ b/apps/platform/app/Support/ReviewPacks/ReviewPackOutputResolutionGuidance.php @@ -84,9 +84,9 @@ public static function readinessForReview(EnvironmentReview $review): array * limitation_count:int, * limitation_summary:?string, * action_help:?string, - * primary_action:array{label:string,url:?string,kind:string,icon:string}|null, - * secondary_actions:list, - * limitations:list}>, + * primary_action:array{key:string,label:string,url:?string,kind:string,icon:string}|null, + * secondary_actions:list, + * limitations:list}>, * technical_details:array * } */ @@ -174,7 +174,7 @@ private static function sectionStateCounts(Collection $sections): array } /** - * @param list,priority:int}> $limitations + * @param list,priority:int}> $limitations */ private static function state(array $readiness, array $limitations): string { @@ -193,7 +193,7 @@ private static function state(array $readiness, array $limitations): string /** * @param array $readiness * @param array{download?:?string,review?:?string,evidence?:?string,operation?:?string} $urls - * @return list,priority:int}> + * @return list,priority:int}> */ private static function limitations(array $readiness, array $urls): array { @@ -297,7 +297,7 @@ private static function limitations(array $readiness, array $urls): array /** * @param array{download?:?string,review?:?string,evidence?:?string,operation?:?string} $urls - * @return array{label:string,url:?string,kind:string,icon:string}|null + * @return array{key:string,label:string,url:?string,kind:string,icon:string}|null */ private static function primaryAction(string $state, ?string $primaryLimitationKey, array $urls): ?array { @@ -323,8 +323,8 @@ private static function primaryAction(string $state, ?string $primaryLimitationK /** * @param array{download?:?string,review?:?string,evidence?:?string,operation?:?string} $urls - * @param array{label:string,url:?string,kind:string,icon:string}|null $primaryAction - * @return list + * @param array{key:string,label:string,url:?string,kind:string,icon:string}|null $primaryAction + * @return list */ private static function secondaryActions(string $state, ?array $primaryAction, array $urls): array { @@ -380,11 +380,12 @@ private static function primaryActionUrl(string $actionKey, array $urls): ?strin } /** - * @return array{label:string,url:?string,kind:string,icon:string}|null + * @return array{key:string,label:string,url:?string,kind:string,icon:string}|null */ private static function action(string $actionKey, ?string $url): ?array { return [ + 'key' => $actionKey, 'label' => match ($actionKey) { 'download_customer_safe_review_pack' => __('localization.review.download_customer_safe_review_pack'), 'download_internal_review_pack' => __('localization.review.download_internal_review_pack'), diff --git a/apps/platform/resources/views/filament/infolists/entries/review-pack-output-guidance.blade.php b/apps/platform/resources/views/filament/infolists/entries/review-pack-output-guidance.blade.php index 574281a2..7d0d61ce 100644 --- a/apps/platform/resources/views/filament/infolists/entries/review-pack-output-guidance.blade.php +++ b/apps/platform/resources/views/filament/infolists/entries/review-pack-output-guidance.blade.php @@ -3,12 +3,15 @@ $state = is_array($state) ? $state : []; $limitations = is_array($state['limitations'] ?? null) ? $state['limitations'] : []; $technicalDetails = is_array($state['technical_details'] ?? null) ? $state['technical_details'] : []; - $secondaryActions = is_array($state['secondary_actions'] ?? null) ? $state['secondary_actions'] : []; + $resolutionCase = is_array($state['resolution_case'] ?? null) ? $state['resolution_case'] : []; + $secondaryActions = is_array($resolutionCase['secondary_actions'] ?? null) + ? $resolutionCase['secondary_actions'] + : (is_array($state['secondary_actions'] ?? null) ? $state['secondary_actions'] : []); $detailMode = (bool) ($state['detail_mode'] ?? false); $contextNote = is_string($state['context_note'] ?? null) ? $state['context_note'] : null; $nextStepLabel = is_string($state['next_step_label'] ?? null) ? $state['next_step_label'] - : data_get($state, 'primary_action.label', __('localization.review.review_output_limitations')); + : data_get($resolutionCase, 'primary_action.label', data_get($state, 'primary_action.label', __('localization.review.review_output_limitations'))); @endphp
@@ -52,11 +55,14 @@
+

+ {{ $resolutionCase['title'] ?? ($state['label'] ?? __('localization.review.requires_review')) }} +

- {{ $state['primary_reason'] ?? __('localization.review.review_pack_with_limitations_description') }} + {{ $resolutionCase['reason'] ?? ($state['primary_reason'] ?? __('localization.review.review_pack_with_limitations_description')) }}

- {{ $state['impact'] ?? __('localization.review.published_with_limitations_impact') }} + {{ $resolutionCase['impact'] ?? ($state['impact'] ?? __('localization.review.published_with_limitations_impact')) }}

@if (filled($contextNote))

@@ -67,14 +73,14 @@ @unless ($detailMode)

- @if (filled(data_get($state, 'primary_action.url'))) + @if (filled(data_get($resolutionCase, 'primary_action.url', data_get($state, 'primary_action.url')))) - {{ $state['primary_action']['label'] }} + {{ data_get($resolutionCase, 'primary_action.label', data_get($state, 'primary_action.label')) }} @endif diff --git a/apps/platform/resources/views/filament/pages/reviews/customer-review-workspace.blade.php b/apps/platform/resources/views/filament/pages/reviews/customer-review-workspace.blade.php index e7ac271f..2f2d7435 100644 --- a/apps/platform/resources/views/filament/pages/reviews/customer-review-workspace.blade.php +++ b/apps/platform/resources/views/filament/pages/reviews/customer-review-workspace.blade.php @@ -47,6 +47,7 @@ $followUps = $reviewPayload['follow_ups']; $diagnostics = $reviewPayload['diagnostics']; $disclosureRules = $reviewPayload['disclosure_rules']; + $resolutionCase = is_array($readiness['resolution_case'] ?? null) ? $readiness['resolution_case'] : []; $reviewPackValueToneClasses = [ 'gray' => 'border-gray-200 bg-gray-50 text-gray-700 dark:border-white/10 dark:bg-white/5 dark:text-gray-300', 'info' => 'border-info-200 bg-info-50 text-info-700 dark:border-info-700/60 dark:bg-info-500/10 dark:text-info-300', @@ -74,11 +75,14 @@ class="rounded-xl border border-gray-200 bg-white p-4 shadow-sm dark:border-whit
-

+
{{ $readiness['question'] }} +
+

+ {{ $resolutionCase['title'] ?? $readiness['label'] }}

- {{ $readiness['reason'] }} + {{ $resolutionCase['reason'] ?? $readiness['reason'] }}

@@ -88,7 +92,7 @@ class="rounded-xl border border-gray-200 bg-white p-4 shadow-sm dark:border-whit {{ __('localization.review.impact') }}

- {{ $readiness['impact'] }} + {{ $resolutionCase['impact'] ?? $readiness['impact'] }}

@@ -106,19 +110,19 @@ class="rounded-xl border border-gray-200 bg-white p-4 shadow-sm dark:border-whit
- @if ($readiness['primary_action_url']) + @if (filled(data_get($resolutionCase, 'primary_action.url', $readiness['primary_action_url']))) - {{ $readiness['primary_action_label'] }} + {{ data_get($resolutionCase, 'primary_action.label', $readiness['primary_action_label']) }} @endif - @foreach ($readiness['secondary_actions'] as $secondaryAction) + @foreach ((is_array($resolutionCase['secondary_actions'] ?? null) ? $resolutionCase['secondary_actions'] : $readiness['secondary_actions']) as $secondaryAction) browser()->timeout(60_000); + +beforeEach(function (): void { + Storage::fake('exports'); +}); + +it('Spec350 smokes the shared resolution path from workspace guidance into review detail', function (): void { + [$user, $environment] = createUserWithTenant(role: 'owner', workspaceRole: 'manager'); + $environment->forceFill(['name' => 'Spec350 Browser Blocked'])->save(); + + [$review] = spec350BrowserCreatePublishedReviewWithPack( + $environment, + $user, + seedPartialEnvironmentReviewEvidence($environment, findingCount: 0, driftCount: 0), + [ + 'publish_blockers' => ['Operator approval note is still missing.'], + ], + [ + 'include_pii' => false, + 'include_operations' => true, + ], + 'review-packs/spec350-browser-blocked.zip', + markReady: false, + ); + + spec350AuthenticateBrowser($this, $user, $environment); + + $detailUrl = EnvironmentReviewResource::environmentScopedUrl('view', ['record' => $review], $environment) + .'?'.http_build_query([ + CustomerReviewWorkspace::DETAIL_CONTEXT_QUERY_KEY => 1, + 'source_surface' => CustomerReviewWorkspace::SOURCE_SURFACE, + 'tenant_filter_id' => (int) $environment->getKey(), + ]); + + $page = visit(CustomerReviewWorkspace::environmentFilterUrl($environment)) + ->resize(1236, 900) + ->waitForText('What is the current review pack output state?') + ->assertSee('Output not customer-ready') + ->assertSee('Inspect review blockers') + ->assertSee('Evidence basis incomplete') + ->assertSee('Technical details') + ->assertScript('document.querySelector("[data-testid=\"customer-review-primary-action\"]")?.getAttribute("href")', $detailUrl) + ->assertNoJavaScriptErrors() + ->assertNoConsoleLogs(); + $page->screenshot(true, spec350BrowserScreenshotName('01-workspace-blocked')); + spec350CopyBrowserScreenshot('01-workspace-blocked'); + + $page = visit($detailUrl) + ->waitForText('Output not customer-ready') + ->assertSee('Review limitations below') + ->assertSee('You are already on the review detail for this output.') + ->assertDontSee('Open evidence basis') + ->assertDontSee('Open operation proof') + ->assertNoJavaScriptErrors() + ->assertNoConsoleLogs(); + $page->screenshot(true, spec350BrowserScreenshotName('02-detail-context')); + spec350CopyBrowserScreenshot('02-detail-context'); +}); + +function spec350BrowserScreenshotName(string $name): string +{ + return 'spec350-operator-resolution-guidance-'.$name; +} + +function spec350CopyBrowserScreenshot(string $name): void +{ + $filename = spec350BrowserScreenshotName($name).'.png'; + $source = base_path('tests/Browser/Screenshots/'.$filename); + $targetDirectory = repo_path('specs/350-operator-resolution-guidance-framework-v1/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 spec350AuthenticateBrowser(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); +} + +/** + * @param array $summaryOverrides + * @param array $packOptions + * @return array{0: EnvironmentReview, 1: ReviewPack} + */ +function spec350BrowserCreatePublishedReviewWithPack( + ManagedEnvironment $environment, + User $user, + EvidenceSnapshot $snapshot, + array $summaryOverrides = [], + array $packOptions = [], + string $filePath = 'review-packs/spec350-browser-review-pack.zip', + bool $markReady = true, +): array { + $review = composeEnvironmentReviewForTest($environment, $user, $snapshot); + $summary = array_replace_recursive( + is_array($review->summary) ? $review->summary : [], + [ + 'control_interpretation' => [ + 'version_key' => ComplianceEvidenceMappingV1::VERSION_KEY, + 'controls' => [ + [ + 'control_key' => 'customer-output', + 'title' => 'Customer output', + 'readiness_bucket' => $markReady ? 'evidence_on_record' : 'review_recommended', + 'readiness_label' => $markReady ? 'Evidence on record' : 'Review recommended', + 'primary_reason' => $markReady ? 'Evidence path is complete.' : 'Evidence basis needs review.', + 'recommended_next_action' => $markReady ? 'Open the current customer review pack.' : 'Review the evidence basis before sharing.', + ], + ], + ], + ], + $summaryOverrides, + ); + + Storage::disk('exports')->put($filePath, 'PK-spec350-browser-test'); + + $review->forceFill([ + 'status' => EnvironmentReviewStatus::Published->value, + 'completeness_state' => $markReady + ? EnvironmentReviewCompletenessState::Complete->value + : (string) $review->completeness_state, + 'summary' => $summary, + 'generated_at' => now()->subMinutes(5), + 'published_at' => now()->subMinutes(3), + 'published_by_user_id' => (int) $user->getKey(), + ])->save(); + + if ($markReady) { + $review = markEnvironmentReviewCustomerSafeReady($review); + } + + $pack = ReviewPack::factory()->ready()->create([ + 'managed_environment_id' => (int) $environment->getKey(), + 'workspace_id' => (int) $environment->workspace_id, + 'environment_review_id' => (int) $review->getKey(), + 'evidence_snapshot_id' => (int) $snapshot->getKey(), + 'initiated_by_user_id' => (int) $user->getKey(), + 'options' => array_replace([ + 'include_pii' => false, + 'include_operations' => true, + ], $packOptions), + 'file_path' => $filePath, + 'file_disk' => 'exports', + 'generated_at' => now()->subMinutes(4), + ]); + + $review->forceFill(['current_export_review_pack_id' => (int) $pack->getKey()])->save(); + + return [$review->fresh(['tenant', 'evidenceSnapshot', 'currentExportReviewPack.operationRun', 'operationRun']), $pack]; +} diff --git a/apps/platform/tests/Feature/EnvironmentReview/Spec350EnvironmentReviewResolutionGuidanceTest.php b/apps/platform/tests/Feature/EnvironmentReview/Spec350EnvironmentReviewResolutionGuidanceTest.php new file mode 100644 index 00000000..2e384175 --- /dev/null +++ b/apps/platform/tests/Feature/EnvironmentReview/Spec350EnvironmentReviewResolutionGuidanceTest.php @@ -0,0 +1,119 @@ +create(['name' => 'Spec350 Detail Blocked']); + [$owner, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner'); + $snapshot = seedPartialEnvironmentReviewEvidence($tenant, findingCount: 0, driftCount: 0); + $review = composeEnvironmentReviewForTest($tenant, $owner, $snapshot); + $summary = array_replace_recursive(is_array($review->summary) ? $review->summary : [], [ + 'publish_blockers' => ['Operator approval note is still missing.'], + ]); + + $review->forceFill([ + 'status' => EnvironmentReviewStatus::Published->value, + 'published_at' => now(), + 'published_by_user_id' => (int) $owner->getKey(), + 'summary' => $summary, + ])->save(); + + Storage::disk('exports')->put('review-packs/spec350-detail-blocked.zip', 'PK-spec350-detail-blocked'); + + $pack = ReviewPack::factory()->ready()->create([ + 'managed_environment_id' => (int) $tenant->getKey(), + 'workspace_id' => (int) $tenant->workspace_id, + 'environment_review_id' => (int) $review->getKey(), + 'evidence_snapshot_id' => (int) $snapshot->getKey(), + 'initiated_by_user_id' => (int) $owner->getKey(), + 'options' => [ + 'include_pii' => false, + 'include_operations' => true, + ], + 'file_path' => 'review-packs/spec350-detail-blocked.zip', + 'file_disk' => 'exports', + 'generated_at' => now()->subMinutes(3), + ]); + + $review->forceFill(['current_export_review_pack_id' => (int) $pack->getKey()])->save(); + + setAdminEnvironmentContext($tenant); + + $state = EnvironmentReviewResource::outputGuidanceState($review->fresh(['tenant', 'evidenceSnapshot', 'currentExportReviewPack.operationRun', 'operationRun'])); + + expect(data_get($state, 'resolution_case.key'))->toBe('review_output.publication_blocked') + ->and(data_get($state, 'resolution_case.primary_action.key'))->toBe('resolve_review_blockers') + ->and(data_get($state, 'resolution_case.source_refs'))->toContainEqual(['type' => 'environment_review', 'id' => (int) $review->getKey()]) + ->and(data_get($state, 'resolution_case.source_refs'))->toContainEqual(['type' => 'review_pack', 'id' => (int) $pack->getKey()]) + ->and(data_get($state, 'resolution_case.evidence_refs'))->toHaveCount(1); + + $this->actingAs($owner) + ->get(EnvironmentReviewResource::environmentScopedUrl('view', ['record' => $review], $tenant)) + ->assertOk() + ->assertSee('Output not customer-ready') + ->assertSee('Inspect review blockers'); +}); + +it('keeps the customer-workspace detail mode action suppression while retaining the shared case payload', function (): void { + $tenant = ManagedEnvironment::factory()->create(['name' => 'Spec350 Detail Context']); + [$owner, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner'); + $snapshot = seedPartialEnvironmentReviewEvidence($tenant, findingCount: 0, driftCount: 0); + $review = composeEnvironmentReviewForTest($tenant, $owner, $snapshot); + $summary = array_replace_recursive(is_array($review->summary) ? $review->summary : [], [ + 'publish_blockers' => ['Operator approval note is still missing.'], + ]); + + $review->forceFill([ + 'status' => EnvironmentReviewStatus::Published->value, + 'published_at' => now(), + 'published_by_user_id' => (int) $owner->getKey(), + 'summary' => $summary, + ])->save(); + + Storage::disk('exports')->put('review-packs/spec350-detail-context.zip', 'PK-spec350-detail-context'); + + $pack = ReviewPack::factory()->ready()->create([ + 'managed_environment_id' => (int) $tenant->getKey(), + 'workspace_id' => (int) $tenant->workspace_id, + 'environment_review_id' => (int) $review->getKey(), + 'evidence_snapshot_id' => (int) $snapshot->getKey(), + 'initiated_by_user_id' => (int) $owner->getKey(), + 'options' => [ + 'include_pii' => false, + 'include_operations' => true, + ], + 'file_path' => 'review-packs/spec350-detail-context.zip', + 'file_disk' => 'exports', + 'generated_at' => now()->subMinutes(3), + ]); + + $review->forceFill(['current_export_review_pack_id' => (int) $pack->getKey()])->save(); + + setAdminEnvironmentContext($tenant); + + $this->actingAs($owner) + ->get(EnvironmentReviewResource::environmentScopedUrl('view', [ + 'record' => $review, + CustomerReviewWorkspace::DETAIL_CONTEXT_QUERY_KEY => 1, + ], $tenant)) + ->assertOk() + ->assertSee('Output not customer-ready') + ->assertSee('Review limitations below') + ->assertSee('You are already on the review detail for this output.') + ->assertDontSee('Open evidence basis') + ->assertDontSee('Open operation proof'); +}); diff --git a/apps/platform/tests/Feature/Filament/Spec350CustomerReviewWorkspaceGuidanceIntegrationTest.php b/apps/platform/tests/Feature/Filament/Spec350CustomerReviewWorkspaceGuidanceIntegrationTest.php new file mode 100644 index 00000000..2ff127ed --- /dev/null +++ b/apps/platform/tests/Feature/Filament/Spec350CustomerReviewWorkspaceGuidanceIntegrationTest.php @@ -0,0 +1,130 @@ +create(['name' => 'Spec350 Workspace Blocked']); + [$user, $environment] = createUserWithTenant(tenant: $environment, role: 'owner', workspaceRole: 'manager'); + $snapshot = seedPartialEnvironmentReviewEvidence($environment, findingCount: 0, driftCount: 0); + $review = composeEnvironmentReviewForTest($environment, $user, $snapshot); + $summary = array_replace_recursive(is_array($review->summary) ? $review->summary : [], [ + 'publish_blockers' => ['Operator approval note is still missing.'], + ]); + + $review->forceFill([ + 'status' => EnvironmentReviewStatus::Published->value, + 'published_at' => now(), + 'published_by_user_id' => (int) $user->getKey(), + 'summary' => $summary, + ])->save(); + + Storage::disk('exports')->put('review-packs/spec350-workspace-blocked.zip', 'PK-spec350-workspace-blocked'); + + $pack = ReviewPack::factory()->ready()->create([ + 'managed_environment_id' => (int) $environment->getKey(), + 'workspace_id' => (int) $environment->workspace_id, + 'environment_review_id' => (int) $review->getKey(), + 'evidence_snapshot_id' => (int) $snapshot->getKey(), + 'initiated_by_user_id' => (int) $user->getKey(), + 'options' => [ + 'include_pii' => false, + 'include_operations' => true, + ], + 'file_path' => 'review-packs/spec350-workspace-blocked.zip', + 'file_disk' => 'exports', + 'generated_at' => now()->subMinutes(3), + ]); + + $review->forceFill(['current_export_review_pack_id' => (int) $pack->getKey()])->save(); + + $component = spec350WorkspaceComponent($user, $environment) + ->assertSee('What is the current review pack output state?') + ->assertSee('Output not customer-ready') + ->assertSee('Inspect review blockers') + ->assertSee('Review blockers are still recorded for this output.'); + + $payload = $component->instance()->latestReviewConsumptionPayload(); + + expect(data_get($payload, 'readiness.resolution_case.key'))->toBe('review_output.publication_blocked') + ->and(data_get($payload, 'readiness.resolution_case.scope.source_surface'))->toBe(CustomerReviewWorkspace::SOURCE_SURFACE) + ->and(data_get($payload, 'readiness.resolution_case.primary_action.key'))->toBe('resolve_review_blockers') + ->and(data_get($payload, 'readiness.resolution_case.source_refs'))->toContainEqual(['type' => 'environment_review', 'id' => (int) $review->getKey()]) + ->and(data_get($payload, 'readiness.resolution_case.source_refs'))->toContainEqual(['type' => 'review_pack', 'id' => (int) $pack->getKey()]) + ->and(data_get($payload, 'readiness.resolution_case.evidence_refs'))->toHaveCount(1); +}); + +it('preserves findings follow-up overrides above the shared review-output case', function (): void { + $environment = ManagedEnvironment::factory()->create(['name' => 'Spec350 Workspace Findings']); + [$user, $environment] = createUserWithTenant(tenant: $environment, role: 'owner', workspaceRole: 'manager'); + $snapshot = seedEnvironmentReviewEvidence($environment, findingCount: 0, driftCount: 0); + $review = composeEnvironmentReviewForTest($environment, $user, $snapshot); + $review->forceFill([ + 'status' => EnvironmentReviewStatus::Published->value, + 'published_at' => now(), + 'published_by_user_id' => (int) $user->getKey(), + ])->save(); + $review = markEnvironmentReviewCustomerSafeReady($review); + + Storage::disk('exports')->put('review-packs/spec350-workspace-findings.zip', 'PK-spec350-workspace-findings'); + + $pack = ReviewPack::factory()->ready()->create([ + 'managed_environment_id' => (int) $environment->getKey(), + 'workspace_id' => (int) $environment->workspace_id, + 'environment_review_id' => (int) $review->getKey(), + 'evidence_snapshot_id' => (int) $snapshot->getKey(), + 'initiated_by_user_id' => (int) $user->getKey(), + 'options' => [ + 'include_pii' => false, + 'include_operations' => true, + ], + 'file_path' => 'review-packs/spec350-workspace-findings.zip', + 'file_disk' => 'exports', + 'generated_at' => now()->subMinutes(3), + ]); + + $review->forceFill(['current_export_review_pack_id' => (int) $pack->getKey()])->save(); + + Finding::factory()->create([ + 'managed_environment_id' => (int) $environment->getKey(), + 'workspace_id' => (int) $environment->workspace_id, + 'severity' => Finding::SEVERITY_HIGH, + 'status' => Finding::STATUS_NEW, + ]); + + $component = spec350WorkspaceComponent($user, $environment) + ->assertSee('Published with limitations') + ->assertSee('Keep open findings visible before customer handoff.') + ->assertSee('Open review'); + + $payload = $component->instance()->latestReviewConsumptionPayload(); + + expect(data_get($payload, 'readiness.resolution_case.key'))->toBe('customer_review_workspace.findings_follow_up_required') + ->and(data_get($payload, 'readiness.resolution_case.primary_action.label'))->toBe('Open review') + ->and(data_get($payload, 'readiness.output_guidance.label'))->toBe('Customer-safe review pack ready'); +}); + +function spec350WorkspaceComponent(User $user, ManagedEnvironment $environment): mixed +{ + session()->put(WorkspaceContext::SESSION_KEY, (int) $environment->workspace_id); + setAdminPanelContext(); + + return Livewire::actingAs($user) + ->test(CustomerReviewWorkspace::class); +} diff --git a/apps/platform/tests/Unit/ResolutionGuidance/Spec350ResolutionCaseContractTest.php b/apps/platform/tests/Unit/ResolutionGuidance/Spec350ResolutionCaseContractTest.php new file mode 100644 index 00000000..c3455d58 --- /dev/null +++ b/apps/platform/tests/Unit/ResolutionGuidance/Spec350ResolutionCaseContractTest.php @@ -0,0 +1,94 @@ + 'review_output.open_review', + 'label' => 'Open review', + 'url' => '/admin/reviews/1', + 'kind' => 'environment_link', + 'icon' => 'heroicon-o-arrow-top-right-on-square', + ], 'review_output.open_review'); + $download = ResolutionAction::fromArray([ + 'key' => 'review_output.download_review_pack_with_limitations', + 'label' => 'Download review pack with limitations', + 'url' => '/admin/review-packs/1/download', + 'kind' => 'download', + 'icon' => 'heroicon-o-arrow-down-tray', + ], 'review_output.download_review_pack_with_limitations'); + + expect($navigation['type'])->toBe(ResolutionAction::TYPE_NAVIGATION) + ->and($navigation['kind'])->toBe('environment_link') + ->and($download['type'])->toBe(ResolutionAction::TYPE_DOWNLOAD) + ->and($download['kind'])->toBe('download'); +}); + +it('degrades unsafe executable actions to a safe non-executable fallback', function (): void { + $action = ResolutionAction::fromArray([ + 'key' => 'review_output.retry_generation', + 'label' => 'Retry generation', + 'type' => ResolutionAction::TYPE_OPERATION_ACTION, + 'url' => '/admin/operations/1', + 'icon' => 'heroicon-o-arrow-path', + ], 'review_output.retry_generation'); + + expect($action['type'])->toBe(ResolutionAction::TYPE_NAVIGATION) + ->and($action['url'])->toBe('/admin/operations/1') + ->and($action['capability'])->toBeNull() + ->and($action['audit_event'])->toBeNull() + ->and($action['requires_confirmation'])->toBeFalse() + ->and($action['operation_run_type'])->toBeNull(); +}); + +it('builds a resolution case with explicit scope and proof references', function (): void { + $case = ResolutionCase::make( + key: 'review_output.publication_blocked', + scope: [ + 'type' => 'review_pack', + 'workspace_id' => 1, + 'managed_environment_id' => 41, + 'environment_review_id' => 6, + 'source_surface' => 'customer_review_workspace', + ], + severity: 'critical', + status: 'blocked', + title: 'Output not customer-ready', + reason: 'The published review is based on incomplete evidence.', + impact: 'Do not share the current review pack externally.', + primaryAction: ResolutionAction::fromArray([ + 'key' => 'review_output.resolve_review_blockers', + 'label' => 'Inspect review blockers', + 'url' => '/admin/reviews/6', + 'kind' => 'environment_link', + ], 'review_output.resolve_review_blockers'), + secondaryActions: [ + ResolutionAction::fromArray([ + 'key' => 'review_output.open_evidence_basis', + 'label' => 'Open evidence basis', + 'url' => '/admin/evidence/8', + 'kind' => 'environment_link', + ], 'review_output.open_evidence_basis'), + ], + sourceRefs: [ + ['type' => 'environment_review', 'id' => 6], + ['type' => 'review_pack', 'id' => 8], + ], + evidenceRefs: [ + ['type' => 'evidence_snapshot', 'id' => 8], + ], + technicalDetails: [ + 'Review status' => 'Published', + ], + ); + + expect($case['scope']['source_surface'])->toBe('customer_review_workspace') + ->and($case['primary_action']['key'])->toBe('review_output.resolve_review_blockers') + ->and($case['secondary_actions'])->toHaveCount(1) + ->and($case['source_refs'])->toHaveCount(2) + ->and($case['evidence_refs'])->toHaveCount(1) + ->and($case['technical_details']['Review status'])->toBe('Published'); +}); diff --git a/apps/platform/tests/Unit/ResolutionGuidance/Spec350ReviewPackResolutionAdapterTest.php b/apps/platform/tests/Unit/ResolutionGuidance/Spec350ReviewPackResolutionAdapterTest.php new file mode 100644 index 00000000..10a8fcfa --- /dev/null +++ b/apps/platform/tests/Unit/ResolutionGuidance/Spec350ReviewPackResolutionAdapterTest.php @@ -0,0 +1,109 @@ +value, + evidenceCompletenessState: EnvironmentReviewCompletenessState::Partial->value, + sectionStateCounts: [ + EnvironmentReviewCompletenessState::Complete->value => 3, + EnvironmentReviewCompletenessState::Missing->value => 2, + ], + requiredSectionCount: 5, + requiredSectionStateCounts: [ + EnvironmentReviewCompletenessState::Complete->value => 3, + EnvironmentReviewCompletenessState::Missing->value => 2, + ], + publishBlockers: ['Operator approval note is still missing.'], + hasReadyExport: false, + includePii: false, + protectedValuesHidden: true, + disclosurePresent: true, + ); + + $guidance = ReviewPackOutputResolutionGuidance::fromReadiness($readiness, [ + 'download' => '/admin/review-packs/8/download', + 'review' => '/admin/reviews/6', + 'evidence' => '/admin/evidence/8', + ]); + + $review = new EnvironmentReview; + $review->forceFill([ + 'id' => 6, + 'workspace_id' => 1, + 'managed_environment_id' => 41, + ]); + $review->setRelation('evidenceSnapshot', tap(new EvidenceSnapshot, function (EvidenceSnapshot $snapshot): void { + $snapshot->forceFill(['id' => 8]); + })); + $review->setRelation('currentExportReviewPack', tap(new ReviewPack, function (ReviewPack $pack): void { + $pack->forceFill(['id' => 8]); + })); + + $case = ReviewPackOutputResolutionAdapter::fromGuidance($review, $guidance, 'customer_review_workspace'); + + expect($case['key'])->toBe('review_output.publication_blocked') + ->and($case['severity'])->toBe('critical') + ->and($case['status'])->toBe('blocked') + ->and($case['title'])->toBe('Output not customer-ready') + ->and($case['primary_action']['key'])->toBe('resolve_review_blockers') + ->and($case['primary_action']['type'])->toBe(ResolutionAction::TYPE_NAVIGATION) + ->and($case['source_refs'])->toEqual([ + ['type' => 'environment_review', 'id' => 6], + ['type' => 'review_pack', 'id' => 8], + ]) + ->and($case['evidence_refs'])->toEqual([ + ['type' => 'evidence_snapshot', 'id' => 8], + ]); +}); + +it('maps ready customer-safe exports to a download-first shared resolution case', function (): void { + $readiness = ReviewPackOutputReadiness::derive( + reviewStatus: 'published', + reviewCompletenessState: EnvironmentReviewCompletenessState::Complete->value, + evidenceCompletenessState: EnvironmentReviewCompletenessState::Complete->value, + sectionStateCounts: [ + EnvironmentReviewCompletenessState::Complete->value => 5, + ], + requiredSectionCount: 5, + requiredSectionStateCounts: [ + EnvironmentReviewCompletenessState::Complete->value => 5, + ], + publishBlockers: [], + hasReadyExport: true, + includePii: false, + protectedValuesHidden: true, + disclosurePresent: true, + ); + + $guidance = ReviewPackOutputResolutionGuidance::fromReadiness($readiness, [ + 'download' => '/admin/review-packs/8/download', + 'review' => '/admin/reviews/6', + ]); + + $review = new EnvironmentReview; + $review->forceFill([ + 'id' => 6, + 'workspace_id' => 1, + 'managed_environment_id' => 41, + ]); + + $case = ReviewPackOutputResolutionAdapter::fromGuidance($review, $guidance, 'customer_review_workspace'); + + expect($case['key'])->toBe('review_output.customer_safe_ready') + ->and($case['severity'])->toBe('success') + ->and($case['status'])->toBe('ready') + ->and($case['primary_action']['key'])->toBe('download_customer_safe_review_pack') + ->and($case['primary_action']['type'])->toBe(ResolutionAction::TYPE_DOWNLOAD); +}); diff --git a/docs/ui-ux-enterprise-audit/page-reports/ui-006-customer-review-workspace.md b/docs/ui-ux-enterprise-audit/page-reports/ui-006-customer-review-workspace.md index 3dadc7bf..393b1cf1 100644 --- a/docs/ui-ux-enterprise-audit/page-reports/ui-006-customer-review-workspace.md +++ b/docs/ui-ux-enterprise-audit/page-reports/ui-006-customer-review-workspace.md @@ -107,3 +107,12 @@ ### Repo-truth note - The user-draft audit-doc target `ui-009-review-pack-output-contract.md` conflicts with repo truth. - `ui-009` is already reserved for Provider Connections, so Spec 349 keeps the durable audit update on `ui-006-customer-review-workspace.md`. + +## Spec 350 Follow-up + +Spec 350 keeps the review-output slice bounded but gives the workspace a shared resolution-case handoff: + +- the top decision block now reads as issue / reason / impact / one dominant next action instead of a page-local interpretation only +- review-output truth still comes from `ReviewPackOutputResolutionGuidance` +- findings-follow-up and accepted-risk follow-up remain local workspace overrides and are not flattened into the shared adapter +- the primary action handoff now matches the review-detail contract without changing workspace/environment isolation or customer-safe disclosure rules diff --git a/docs/ui-ux-enterprise-audit/page-reports/ui-040-environment-review-detail.md b/docs/ui-ux-enterprise-audit/page-reports/ui-040-environment-review-detail.md new file mode 100644 index 00000000..7e250671 --- /dev/null +++ b/docs/ui-ux-enterprise-audit/page-reports/ui-040-environment-review-detail.md @@ -0,0 +1,52 @@ +# UI-040 Environment Review Detail + +| Field | Value | +| --- | --- | +| Route | `/admin/workspaces/{workspace}/environments/{environment}/environment-reviews/{record}` | +| Source | `EnvironmentReviewResource::view` | +| Area / scope | Reviews / environment detail | +| Archetype | Evidence / Audit | +| Design depth | Strategic Surface | +| Repo truth | repo-verified | +| Screenshot | `Spec 350 browser proof: specs/350-operator-resolution-guidance-framework-v1/artifacts/screenshots/02-detail-context.png` | +| Browser status | Reached through direct environment route and customer-workspace handoff. | + +## First Five Seconds + +The page should answer three questions without forcing the operator to reconstruct the review from raw sections: + +1. what is the review status +2. what is the output readiness +3. what is the safe next step + +## Productization Review + +- Decision-first: improved by the shared resolution-case summary. +- Evidence-first: limitations and technical details remain visible below the summary. +- Context: environment-bound review detail with optional customer-workspace handoff context. +- Customer/auditor safety: high because this page explains whether the current released output is share-safe. +- Diagnostics: sections and raw detail stay secondary to the first-screen output guidance. + +## Information Inventory + +Default content should show lifecycle status, output guidance, publication/sharing boundary, evidence snapshot linkage, current export linkage, and section completeness. + +## Dangerous Actions + +Lifecycle actions such as refresh, publish, export, create-next-review, and archive remain source-owned. In customer-workspace detail mode, the repeated primary action rail should stay suppressed so the operator does not get duplicate or conflicting calls to action. + +## Spec 349 Follow-up + +Spec 349 separated review status, output readiness, and publication/sharing state while keeping the customer-workspace detail mode free of repeated CTA rails. + +## Spec 350 Follow-up + +Spec 350 adds the shared review-output resolution-case handoff: + +- the first-screen summary now uses the same issue / reason / impact / next-action reading direction as the workspace +- source refs and evidence refs remain repo-backed in the underlying contract +- customer-workspace detail mode still suppresses repeated action buttons and keeps the limitations/technical-details path as the primary inspection flow + +## Target Direction + +Keep this surface audit- and evidence-oriented. If future work broadens it beyond the review-output path, that should happen through a dedicated detail-surface spec rather than hidden incremental drift. diff --git a/docs/ui-ux-enterprise-audit/route-inventory.md b/docs/ui-ux-enterprise-audit/route-inventory.md index 1c97c849..84fb35c1 100644 --- a/docs/ui-ux-enterprise-audit/route-inventory.md +++ b/docs/ui-ux-enterprise-audit/route-inventory.md @@ -45,7 +45,7 @@ # Route Inventory | UI-037 | `/admin/reviews` | page | Review Register | Reviews | workspace hub | reachable | workspace member | Reviews | Evidence / Audit | Strategic Surface | repo-verified | [desktop](screenshots/desktop/ui-011-reviews.png) | [report](page-reports/ui-011-reviews.md) | Review planning and proof surface. | | UI-038 | `/admin/reviews/workspace` | page | Customer Review Workspace | Customer review | workspace hub | reachable | workspace member | Customer Workspace | Reviews | Strategic Surface | repo-verified | [desktop](screenshots/desktop/ui-006-customer-review-workspace.png) | [report](page-reports/ui-006-customer-review-workspace.md) | Highest customer-safe productization surface. | | UI-039 | `/admin/workspaces/{workspace}/environments/{environment}/environment-reviews` | resource | Environment Reviews | Reviews | environment-bound | route exists | environment entitlement | Reviews | Evidence / Audit | Domain Pattern Surface | repo-verified | - | - | Environment-scoped review list. | -| UI-040 | `/admin/workspaces/{workspace}/environments/{environment}/environment-reviews/{record}` | resource | Environment Review Detail | Reviews | environment record | route exists | environment + record entitlement | Reviews | Evidence / Audit | Strategic Surface | repo-verified | - | - | Customer/auditor-facing evidence risk. | +| UI-040 | `/admin/workspaces/{workspace}/environments/{environment}/environment-reviews/{record}` | resource | Environment Review Detail | Reviews | environment record | route exists | environment + record entitlement | Reviews | Evidence / Audit | Strategic Surface | repo-verified | - | [report](page-reports/ui-040-environment-review-detail.md) | Customer/auditor-facing evidence risk. | | UI-041 | `/admin/workspaces/{workspace}/environments/{environment}/review-packs` | resource | Review Packs | Reviews | environment-bound | route exists | environment entitlement | Reviews | Evidence / Audit | Domain Pattern Surface | repo-verified | - | - | Export artifact list. | | UI-042 | `/admin/workspaces/{workspace}/environments/{environment}/review-packs/{record}` | resource | Review Pack Detail | Reviews | environment record | route exists | environment + record entitlement | Reviews | Evidence / Audit | Strategic Surface | repo-verified | - | - | Export/evidence artifact detail. | | UI-043 | `/admin/review-packs/{reviewPack}/download` | controller | Review Pack Download | Reviews | workspace/environment artifact | route exists | download authorization expected | Reviews | Evidence / Audit | Design-System Cleanup Surface | repo-verified | - | - | Action endpoint, not page; include in coverage due customer artifact impact. | diff --git a/docs/ui-ux-enterprise-audit/unresolved-pages.md b/docs/ui-ux-enterprise-audit/unresolved-pages.md index 6a56df4f..f51d8b2e 100644 --- a/docs/ui-ux-enterprise-audit/unresolved-pages.md +++ b/docs/ui-ux-enterprise-audit/unresolved-pages.md @@ -4,9 +4,9 @@ # Unresolved Pages Summary: -- High-priority unresolved/manual-review entries: 32. +- High-priority unresolved/manual-review entries: 31. - Capability/fixture blockers with desktop evidence: UI-051, UI-053, UI-061. -- Strategic routes not browser-captured in this bounded pass: 28. +- Strategic routes not browser-captured in this bounded pass: 27. - Hidden/file-discovered manual-review surface: UI-080. | ID | Page | Blocker / Reason | Needed Evidence | Next Action | @@ -18,7 +18,6 @@ # Unresolved Pages | UI-017 | Operation Detail | Dynamic operation record route requires a run fixture. | OperationRun records covering success, failure, running, retryable states. | Add operation detail report later. | | UI-034 | Finding Detail | Dynamic finding detail requires seeded finding state. | Finding records with owner, severity, exception, and close state. | Add strategic finding detail mockup. | | UI-036 | Exception Detail | Accepted-risk detail requires seeded exception record. | Pending, approved, expired, rejected exception states. | Add accepted-risk detail mockup. | -| UI-040 | Environment Review Detail | Dynamic customer/auditor review record was not captured. | Review records with evidence/progress states. | Add review detail report later. | | UI-042 | Review Pack Detail | Export/evidence artifact detail requires seeded review pack. | Review pack with files, freshness, and download state. | Add review-pack target artifact. | | UI-044 | Evidence Overview | Workspace evidence landing was not captured. | Workspace with evidence sources, gaps, and stale states. | Add evidence overview report. | | UI-046 | Evidence Snapshot Detail | Dynamic raw/support evidence detail requires snapshot record. | Snapshot with normalized summary and raw payload. | Add progressive-disclosure review. | diff --git a/specs/350-operator-resolution-guidance-framework-v1/artifacts/screenshots/01-workspace-blocked.png b/specs/350-operator-resolution-guidance-framework-v1/artifacts/screenshots/01-workspace-blocked.png new file mode 100644 index 00000000..3f5cf63b Binary files /dev/null and b/specs/350-operator-resolution-guidance-framework-v1/artifacts/screenshots/01-workspace-blocked.png differ diff --git a/specs/350-operator-resolution-guidance-framework-v1/artifacts/screenshots/02-detail-context.png b/specs/350-operator-resolution-guidance-framework-v1/artifacts/screenshots/02-detail-context.png new file mode 100644 index 00000000..d75f9a6e Binary files /dev/null and b/specs/350-operator-resolution-guidance-framework-v1/artifacts/screenshots/02-detail-context.png differ diff --git a/specs/350-operator-resolution-guidance-framework-v1/checklists/requirements.md b/specs/350-operator-resolution-guidance-framework-v1/checklists/requirements.md new file mode 100644 index 00000000..061076be --- /dev/null +++ b/specs/350-operator-resolution-guidance-framework-v1/checklists/requirements.md @@ -0,0 +1,43 @@ +# Requirements Checklist: Spec 350 - Operator Resolution Guidance Framework v1 + +**Purpose**: Validate that Spec 350 is bounded, repo-based, constitution-aligned, and ready for a later implementation loop. +**Created**: 2026-06-03 +**Feature**: `specs/350-operator-resolution-guidance-framework-v1/spec.md` + +## Candidate Selection And Guardrail + +- [x] CHK001 The package names the direct user-provided candidate source and its roadmap alignment with customer review, governance inbox, provider readiness, and environment-readiness productization. +- [x] CHK002 Completed or active related specs are treated as context only and are not reopened or normalized. +- [x] CHK003 The speculative Spec-347 follow-up number conflict is documented and handled without rewriting historical artifacts. +- [x] CHK004 The scope is narrowed to a derived contract, one required review-output adapter, and only bounded optional provider/operation adapters rather than a workflow engine or broad platform rebuild. + +## Repo Truth And Architecture + +- [x] CHK005 The spec and plan explicitly anchor the work to existing guidance producers: review output guidance, operation guidance, operator explanation, primary-next-step helpers, provider readiness summaries, and current strategic consumers. +- [x] CHK006 The artifacts state that any new case/action contract remains derived-only and request-scoped; no persistence is introduced. +- [x] CHK007 The plan forbids replacement-by-rewrite of `ReviewPackOutputResolutionGuidance`, `OperationUxPresenter`, and `OperatorExplanationPattern`. +- [x] CHK008 Optional consumers are bounded explicitly so Governance Inbox, provider readiness, and environment dashboard do not become hidden redesign scope. + +## UI/Productization Coverage + +- [x] CHK009 UI Surface Impact is explicit and consistent with the intended review-output-first rollout plus bounded optional consumers. +- [x] CHK010 UI/Productization Coverage reuses the existing page-report identities and target-experience briefs, and it resolves `UI-040` / `UI-077` through the current audit registry instead of inventing a new audit taxonomy. +- [x] CHK011 The spec requires one dominant issue and one dominant next action rather than equal-weight warning groups. +- [x] CHK012 Audience-aware disclosure keeps technical detail, source refs, and raw/support detail secondary. + +## Testing And Validation + +- [x] CHK013 Planned tests cover the shared contract, the required review-output adapter, the required review-output consumers, and one bounded browser smoke, with optional provider/operation tests only if those adapters are adopted. +- [x] CHK014 Validation commands explicitly rerun focused regressions for Specs 347 and 349, while treating Spec 346 / Governance Inbox and other non-review consumers as optional regressions only when those consumers are adopted. +- [x] CHK015 The artifacts name `pint --dirty` and `git diff --check` as final validation steps. + +## Review Outcome + +- [x] CHK016 Review outcome class: `documentation-required-exception` +- [x] CHK017 Workflow outcome: `keep` +- [x] CHK018 Final note location is the active feature PR close-out entry `Guardrail / Smoke Coverage`. + +## Notes + +- This checklist validates preparation readiness only. No application implementation has been performed. +- The documented exception is the new cross-domain contract itself; it is acceptable only because the spec keeps the implementation derived-only, review-output-first, and bounded to real current consumers. diff --git a/specs/350-operator-resolution-guidance-framework-v1/contracts/adapter-contract.md b/specs/350-operator-resolution-guidance-framework-v1/contracts/adapter-contract.md new file mode 100644 index 00000000..fa5795a8 --- /dev/null +++ b/specs/350-operator-resolution-guidance-framework-v1/contracts/adapter-contract.md @@ -0,0 +1,79 @@ +# Adapter Contract + +Status: Draft for Spec 350 +Scope: Runtime adapters that wrap existing guidance-producing truth + +## Purpose + +Each adapter translates one current repo-real guidance family into one or more `ResolutionCase` objects without inventing a new workflow system. + +Adapters are not source-of-truth owners. They are normalizers over existing truth. + +## Required Responsibilities + +Each adapter must: + +1. accept already-scoped repo-backed input +2. determine the dominant issue case +3. derive one primary action +4. attach source refs and evidence refs where applicable +5. attach technical details as secondary disclosure only +6. preserve existing action safety requirements + +Each adapter must not: + +1. persist new state +2. create new domain lifecycle or workflow state +3. perform remote calls during render +4. invent actions that do not exist safely in the repo + +## Suggested Adapter Inputs + +| Adapter | Expected input truth | +|---|---| +| `ReviewPackOutputResolutionAdapter` | `EnvironmentReview`, `ReviewPack`, `EvidenceSnapshot`, existing output-readiness guidance | +| standalone evidence-basis adapter | not justified in v1 because evidence-basis truth is already part of review-output guidance | +| `ProviderReadinessResolutionAdapter` | existing provider surface summary, required-permissions guidance, verification truth | +| `OperationFollowUpResolutionAdapter` | existing `OperationRun`, `OperationUxPresenter`, operator explanation, proof links | + +## Output Rule + +Each adapter returns either: + +- one dominant `ResolutionCase`, or +- a small ordered list of cases where the consumer surface can legitimately display more than one case without creating a warning wall + +Default bias: one dominant case. + +## Consumer Rule + +Consumer surfaces may: + +- render one case card +- render a bounded list +- merge case metadata into an existing first-viewport decision block + +Consumer surfaces may not: + +- rebuild the adapter logic locally +- fork the primary-action logic without documenting why +- elevate technical details above the dominant case + +## V1 Required Adapters + +- `ReviewPackOutputResolutionAdapter` + +## Optional Same-Slice Adapters + +- `ProviderReadinessResolutionAdapter` +- `OperationFollowUpResolutionAdapter` + +These adapters are allowed only when a concrete in-scope consumer can adopt them without a broader surface redesign. + +## Optional Later Adapters + +- `FindingRequiresTriageResolutionAdapter` +- `AcceptedRiskReviewResolutionAdapter` +- standalone evidence-basis adapter after a non-review consumer proves it is necessary + +Those optional adapters are out of current guaranteed scope unless the implementation proves they can be added without widening the feature. diff --git a/specs/350-operator-resolution-guidance-framework-v1/contracts/future-ai-hitl-extension.md b/specs/350-operator-resolution-guidance-framework-v1/contracts/future-ai-hitl-extension.md new file mode 100644 index 00000000..42a4dc91 --- /dev/null +++ b/specs/350-operator-resolution-guidance-framework-v1/contracts/future-ai-hitl-extension.md @@ -0,0 +1,64 @@ +# Future AI / Human-in-the-Loop Extension + +Status: Draft for Spec 350 +Scope: Documentation only + +## Current Rule + +Spec 350 does not implement AI. + +No runtime AI call, no AI suggestion rendering, and no AI-driven execution path are part of v1. + +## Why This Document Exists + +The shared case/action contract is a plausible future attachment point for AI-assisted guidance. Documenting that boundary now avoids later feature-local AI drift. + +## Reserved Extension Shape + +If a later spec enables AI suggestions, the `ResolutionCase` envelope may gain a strictly optional field like: + +```php +[ + 'ai_suggestion' => [ + 'enabled' => false, + 'provider' => null, + 'model' => null, + 'confidence' => null, + 'summary' => null, + 'requires_human_approval' => true, + 'policy_gate' => null, + 'budget_gate' => null, + 'context_refs' => [], + ], +] +``` + +That field must stay absent or disabled in Spec 350 runtime work. + +## Mandatory Future Gates + +Any future AI-enabled follow-up must require: + +1. AI policy gate +2. AI context boundary +3. AI budget/cost gate +4. audit trail +5. human approval gate +6. capability gate +7. existing domain action or `OperationRun` execution path + +## Forbidden Future Shortcuts + +- direct AI execution without human approval +- direct AI writes around existing policy/capability checks +- storing AI-generated resolution truth as canonical truth without a separate spec +- bypassing existing audit requirements + +## Current Implementation Guidance + +Spec 350 runtime work should leave clear seams for later extension but should not: + +- add AI fields to rendered UI +- add AI fields to persisted rows +- add AI-specific copy keys +- add AI-specific jobs or service calls diff --git a/specs/350-operator-resolution-guidance-framework-v1/contracts/resolution-action-contract.md b/specs/350-operator-resolution-guidance-framework-v1/contracts/resolution-action-contract.md new file mode 100644 index 00000000..e79f91f1 --- /dev/null +++ b/specs/350-operator-resolution-guidance-framework-v1/contracts/resolution-action-contract.md @@ -0,0 +1,84 @@ +# Resolution Action Contract + +Status: Draft for Spec 350 +Scope: Dominant next-step contract only + +## Purpose + +`ResolutionAction` is the action envelope inside a `ResolutionCase`. + +It standardizes what the operator should do next and whether that next step remains navigation/disclosure only or can safely reuse an existing source-owned executable path. + +Repo note: this contract is intentionally named `ResolutionAction` to avoid colliding with the existing dashboard-local `RecommendedAction` schema in `specs/266-tenant-dashboard-productization-v1/contracts/tenant-dashboard-productization.openapi.yaml`. + +## Hard Rules + +- Each `ResolutionCase` has exactly one `primary_action`. +- The default v1 bias is `navigation`, `download`, `disclosure`, or `none`. +- Unsupported or unsafe executable actions must degrade to a non-executable action type. +- No fake fix buttons. + +## Required Shape + +```php +[ + 'key' => 'provider_readiness.open_required_permissions', + 'label' => 'Open required permissions', + 'type' => 'navigation', + 'url' => '...', + 'capability' => null, + 'requires_confirmation' => false, + 'audit_event' => null, + 'operation_run_type' => null, + 'disabled_reason' => null, +] +``` + +## Allowed Action Types + +- `navigation` +- `workspace_filtered_link` +- `environment_link` +- `download` +- `disclosure` +- `none` +- `domain_action` only when the source surface already owns the full safety envelope +- `operation_action` only when the source surface already owns the full safety envelope + +## Required Safety Fields For Executable Actions + +If `type` is `domain_action` or `operation_action`, the action must include: + +- `capability` +- `requires_confirmation` +- `audit_event` or equivalent audit reason +- `operation_run_type` when the action is execution-backed + +If any of those are unavailable, the action must become `navigation`, `download`, `disclosure`, or `none`. + +## Existing Runtime Inputs To Reuse + +| Existing producer | Contract expectation | +|---|---| +| scoped URL helpers | populate `url` for navigation/disclosure actions | +| current policy/capability checks | populate `capability` and `disabled_reason` where applicable | +| existing Filament action semantics | preserve confirmation and audit behavior only when the action is already source-owned and executable | +| `OperationRunLinks` | provide proof/detail destinations for operation follow-up | +| current qualified download labels | remain downloads, not disguised execution actions | + +## Secondary Actions + +Secondary actions are optional supporting actions. They must never compete visually or semantically with the primary action. + +Allowed secondary examples: + +- `Open evidence basis` +- `Open operation proof` +- `Review limitations` +- `Open provider readiness` + +Forbidden secondary examples: + +- duplicate copies of the primary action +- fake auto-remediation +- destructive actions without the same safety metadata requirements diff --git a/specs/350-operator-resolution-guidance-framework-v1/contracts/resolution-case-contract.md b/specs/350-operator-resolution-guidance-framework-v1/contracts/resolution-case-contract.md new file mode 100644 index 00000000..d5a522c1 --- /dev/null +++ b/specs/350-operator-resolution-guidance-framework-v1/contracts/resolution-case-contract.md @@ -0,0 +1,149 @@ +# Resolution Case Contract + +Status: Draft for Spec 350 +Scope: Derived operator-guidance envelope only + +## Purpose + +`ResolutionCase` is the shared productized guidance object for current-release operator issues that already exist in repo truth. + +It answers: + +1. What is wrong? +2. Why does it matter? +3. What is the dominant next step? +4. Is that step navigation/disclosure-only or an existing safe executable path? +5. Which source records prove this guidance? + +This contract must wrap existing guidance producers. It must not replace them or persist a second truth model. + +## Design Constraints + +- Derived only; no persistence +- Scope explicit +- Exactly one primary action +- Source-traceable +- Safe-execution aware +- Provider-neutral in the core contract +- Technical details secondary + +## Required Shape + +```php +[ + 'key' => 'review_pack.output_not_customer_ready', + 'scope' => [ + 'type' => 'environment', + 'workspace_id' => 1, + 'managed_environment_id' => 41, + 'source_surface' => 'customer_review_workspace', + ], + 'severity' => 'warning', + 'status' => 'action_required', + + 'title' => 'Output not customer-ready', + 'reason' => 'The published review is based on incomplete evidence.', + 'impact' => 'This review pack should not be shared externally until the evidence basis is refreshed.', + + 'primary_action' => [ + 'key' => 'environment_review.open_current_limitations', + 'label' => 'Inspect review blockers', + 'type' => 'navigation', + 'url' => '...', + 'capability' => null, + 'requires_confirmation' => false, + 'audit_event' => null, + 'operation_run_type' => null, + ], + + 'secondary_actions' => [ + [ + 'key' => 'evidence.open_basis', + 'label' => 'Open evidence basis', + 'type' => 'navigation', + 'url' => '...', + ], + ], + + 'source_refs' => [ + ['type' => 'environment_review', 'id' => 6], + ['type' => 'review_pack', 'id' => 8], + ], + + 'evidence_refs' => [ + ['type' => 'evidence_snapshot', 'id' => 8], + ], + + 'technical_details' => [ + 'review_status' => 'published', + 'output_readiness' => 'publication_blocked', + 'evidence_state' => 'missing', + ], +] +``` + +## Required Fields + +| Field | Requirement | +|---|---| +| `key` | Stable, repo-owned case identifier | +| `scope` | Explicit workspace/environment/review/operation/provider scope | +| `severity` | Presentation-only severity | +| `status` | Presentation-only actionability state | +| `title` | Primary operator-facing issue label | +| `reason` | Plain-language explanation of the dominant cause | +| `impact` | Why this matters now | +| `primary_action` | Exactly one dominant next step | +| `secondary_actions` | Optional supporting navigation/disclosure actions | +| `source_refs` | Required repo-backed source references | +| `evidence_refs` | Optional but required when evidence truth is central to the case | +| `technical_details` | Secondary disclosure payload only | + +## Allowed Scope Types + +- `workspace` +- `environment` +- `review` +- `review_pack` +- `evidence` +- `provider_connection` +- `operation` +- `finding` +- `system` + +## Allowed Severities + +- `critical` +- `warning` +- `info` +- `success` + +## Allowed Statuses + +- `action_required` +- `blocked` +- `needs_review` +- `informational` +- `ready` +- `resolved` +- `unknown` + +## Existing Runtime Inputs To Reuse + +| Existing producer | How it maps into `ResolutionCase` | +|---|---| +| `ReviewPackOutputResolutionGuidance` | `title`, `reason`, `impact`, `primary_action`, `secondary_actions`, `technical_details`, and review-output evidence-basis truth | +| `OperationUxPresenter` | dominant issue/action text for operation follow-up cases | +| `OperatorExplanationPattern` | trust/reliability/next-action semantics that can inform title/reason/impact | +| `EnterpriseDetailSectionFactory::primaryNextStep()` | existing primary-next-step shape for action text and supporting guidance | +| provider readiness summaries and required-permissions guidance | provider-owned issue/reason/action sources | + +## Hard Rules + +- Do not persist resolution cases. +- Do not create source refs that cannot be resolved to repo-backed records. +- Do not expose more than one primary action. +- Do not encode hidden scope. +- Do not force executable action metadata onto producers that only support navigation, qualified download, or disclosure today. +- Do not introduce a standalone evidence-basis adapter in v1 while review-output guidance already owns that truth. +- Do not use the contract as a generic workflow-state machine. diff --git a/specs/350-operator-resolution-guidance-framework-v1/plan.md b/specs/350-operator-resolution-guidance-framework-v1/plan.md new file mode 100644 index 00000000..15e474dd --- /dev/null +++ b/specs/350-operator-resolution-guidance-framework-v1/plan.md @@ -0,0 +1,241 @@ +# Implementation Plan: Spec 350 - Operator Resolution Guidance Framework v1 + +**Branch**: `350-operator-resolution-guidance-framework-v1` | **Date**: 2026-06-03 | **Spec**: `specs/350-operator-resolution-guidance-framework-v1/spec.md` +**Input**: User-provided Spec 350 draft + repo truth from existing output guidance, operator explanation, next-step, provider readiness, and governance inbox paths. + +## Summary + +Introduce one bounded derived `ResolutionCase` / `ResolutionAction` contract over existing guidance-producing runtime paths so operators see the same reading direction first on review output, with optional reuse on provider-readiness or operation-follow-up consumers only when that reuse stays bounded: + +1. issue +2. reason +3. impact +4. one dominant next action +5. supporting proof and source references + +This slice must reuse current guidance producers instead of replacing them: + +- `ReviewPackOutputResolutionGuidance` +- `OperationUxPresenter` +- `OperatorExplanationPattern` +- `EnterpriseDetailSectionFactory::primaryNextStep()` +- current Governance Inbox next-recommended-item logic +- current provider readiness and required-permissions guidance + +This slice must not: + +- create persistence +- create a workflow engine +- create a new provider framework +- broaden dashboard or governance surface redesigns beyond bounded consumption +- introduce AI execution + +## Technical Context + +- **Language/Version**: PHP 8.4.15, Laravel 12.52.x +- **Primary Dependencies**: Filament 5.2.x, Livewire 4.1.x, Pest 4, Tailwind CSS 4 +- **Storage**: PostgreSQL; no schema change expected +- **Testing**: Pest Unit + Feature/Livewire + one bounded Browser smoke +- **Validation Lanes**: fast-feedback + confidence + browser +- **Target Platform**: `apps/platform` Laravel monolith, Sail-first locally +- **Project Type**: server-rendered Filament web application +- **Performance Goals**: keep derivation DB-only and scoped; no new remote calls during render; no new queue family +- **Constraints**: reuse-first, no new persistence, no fake actions, no hidden scope, no provider-semantic bleed into the core contract, no third parallel explanation framework +- **Scale/Scope**: one support-layer contract, one required review-output adapter, up to two optional bounded adapters if a concrete same-slice consumer exists, one reusable rendering path if required, first visible rollout on review-output surfaces, optional bounded follow-through on other surfaces + +## UI / Surface Guardrail Plan + +- **Guardrail scope**: strategic operator and customer-safe surfaces with existing guidance islands +- **Affected routes/pages/actions/states/navigation/panel/provider surfaces**: + - `/admin/reviews/workspace` + - Environment Review detail + - existing Governance Inbox top recommendation only if consumed + - existing provider readiness / required-permissions summary only if consumed + - existing environment dashboard readiness / recommendation cards only if consumed +- **No-impact class, if applicable**: N/A +- **Native vs custom classification summary**: native Filament page/resource/detail surfaces plus existing Blade composition; no new route family or panel/provider work expected +- **Shared-family relevance**: review/output guidance, next recommended action, readiness cards, proof links, provider readiness guidance +- **State layers in scope**: page, detail, URL-query, derived request-scoped support-layer contract +- **Audience modes in scope**: customer-safe reader, operator-MSP, manager, support where already authorized +- **Decision/diagnostic/raw hierarchy plan**: issue, reason, impact, primary action first; technical/source/proof details second; raw/support detail stays source-owned and gated +- **Raw/support gating plan**: preserve current raw/support gating on source surfaces and avoid moving raw detail into the shared contract +- **One-primary-action / duplicate-truth control**: the new contract owns only the dominant case/action summary; downstream sections add proof, not a second headline +- **Handling modes by drift class or surface**: review-mandatory +- **Repository-signal treatment**: review-mandatory because the feature adds a shared contract over existing strategic surfaces +- **Special surface test profiles**: `global-context-shell` + `shared-detail-family` + bounded strategic smoke +- **Required tests or manual smoke**: focused unit/feature tests plus one browser smoke for first-screen review-output guidance +- **Exception path and spread control**: if a proposed consumer needs a broader redesign, stop and keep that consumer out of Spec 350 +- **Active feature PR close-out entry**: Guardrail / Smoke Coverage +- **UI/Productization coverage decision**: update only the existing relevant page reports for actually touched surfaces, and resolve `UI-040` / `UI-077` registry obligations through the existing audit files (`page-reports`, `unresolved-pages`, and related registry files) rather than inventing a new taxonomy + +## Shared Pattern & System Fit + +- **Cross-cutting feature marker**: yes +- **Systems touched**: + - `App\Support\ReviewPacks\ReviewPackOutputResolutionGuidance` + - `App\Support\OpsUx\OperationUxPresenter` + - `App\Support\Ui\OperatorExplanation\OperatorExplanationPattern` + - `App\Support\Ui\EnterpriseDetail\EnterpriseDetailSectionFactory` + - `App\Filament\Pages\Reviews\CustomerReviewWorkspace` + - `App\Filament\Resources\EnvironmentReviewResource` + - `App\Filament\Pages\Governance\GovernanceInbox` + - `App\Support\EnvironmentDashboard\EnvironmentDashboardSummaryBuilder` + - `App\Support\Providers\TargetScope\ProviderConnectionSurfaceSummary` + - `App\Filament\Pages\EnvironmentRequiredPermissions` +- **Shared abstractions reused**: + - current review/output readiness mapping + - current run-detail operator guidance and next-step text + - current operator explanation / enterprise-detail decision zone semantics + - current scoped route helpers and `OperationRunLinks` +- **New abstraction introduced? why?**: yes, one bounded resolution-case/action contract is justified because multiple real guidance families now exist and already drift in shape, but the required v1 proof remains review-output first +- **Why the existing abstraction was sufficient or insufficient**: each current producer solves its local problem well, but no existing contract standardizes source refs, explicit scope, one action, and safe non-executable defaults across those producers +- **Bounded deviation / spread control**: do not replace `OperatorExplanationPattern`, `ReviewPackOutputResolutionGuidance`, or `OperationUxPresenter`; wrap them + +## OperationRun UX Impact + +- **Touches OperationRun start/completion/link UX?**: only deep-link and follow-up guidance normalization +- **Central contract reused**: `OperationRunLinks`, existing proof URLs, and `OperationUxPresenter` +- **Delegated UX behaviors**: unchanged queue/terminal behavior +- **Surface-owned behavior kept local**: case ranking and grouped supporting details +- **Queued DB-notification policy**: unchanged +- **Terminal notification path**: unchanged +- **Exception path**: none + +## Provider Boundary & Portability Fit + +- **Shared provider/platform boundary touched?**: yes +- **Provider-owned seams**: verification, required permissions, consent/credential readiness detail +- **Platform-core seams**: cross-surface case/action contract, scope, source refs, severity/status/action typing +- **Neutral platform terms / contracts preserved**: provider, environment, evidence basis, operation, resolution case, resolution action +- **Retained provider-specific semantics and why**: provider-owned adapters may still surface provider permission or verification labels because those surfaces already are provider-owned +- **Bounded extraction or follow-up path**: any deeper provider readiness redesign becomes a follow-up spec, not hidden scope + +## Current Repo Truth Summary + +- `ReviewPackOutputResolutionGuidance` already returns a rich derived structure: state, label, primary reason, impact, qualified download label, limitations, primary action, secondary actions, and technical details. +- `CustomerReviewWorkspace` already consumes that guidance and is the best first visible consumer for a generalized contract, but it also contains repo-real findings and accepted-risk follow-up overrides that must remain authoritative. +- `EnvironmentReviewResource` already exposes output-guidance state and qualified download wording on detail, and the customer-workspace detail mode intentionally suppresses repeated action rails. +- `OperationUxPresenter::surfaceGuidance()` already produces dominant follow-up text, and `OperationRunResource` plus enterprise-detail helpers already model a `primaryNextStep` shape. +- `GovernanceInbox` already has a repo-real first-viewport `Next recommended action`, but it is lane/page-specific and not a general case envelope. +- `ProviderConnectionSurfaceSummary` already distills provider readiness to a summary, while `EnvironmentRequiredPermissions` already exposes guidance, next-step links, and verification follow-through on a dedicated page. +- `EnvironmentDashboardSummaryBuilder` already builds readiness and recommended-action cards that can act as a later bounded consumer if the new contract remains thin. +- No repo-real cross-surface contract currently combines explicit scope, one primary action, safe non-executable defaults, source refs, and secondary proof across the current guidance families. + +## Implementation Approach + +### Phase 0 - Repo Truth Gate + +1. Re-read `spec.md`, `plan.md`, `tasks.md`, `repo-truth-map.md`, and the contract docs before runtime changes. +2. Re-verify the current guidance producers in: + - `apps/platform/app/Support/ReviewPacks/ReviewPackOutputResolutionGuidance.php` + - `apps/platform/app/Support/OpsUx/OperationUxPresenter.php` + - `apps/platform/app/Support/Ui/OperatorExplanation/OperatorExplanationPattern.php` + - `apps/platform/app/Support/Ui/EnterpriseDetail/EnterpriseDetailSectionFactory.php` + - `apps/platform/app/Filament/Pages/Governance/GovernanceInbox.php` + - `apps/platform/app/Support/Providers/TargetScope/ProviderConnectionSurfaceSummary.php` + - `apps/platform/app/Filament/Pages/EnvironmentRequiredPermissions.php` +3. Keep `repo-truth-map.md` current if runtime inspection changes the narrowest correct implementation. + +### Phase 1 - Tests First + +1. Add focused contract and adapter unit tests before implementation: + - `apps/platform/tests/Unit/ResolutionGuidance/Spec350ResolutionCaseContractTest.php` + - `apps/platform/tests/Unit/ResolutionGuidance/Spec350ReviewPackResolutionAdapterTest.php` + - optional: `apps/platform/tests/Unit/ResolutionGuidance/Spec350ProviderReadinessResolutionAdapterTest.php` + - optional: `apps/platform/tests/Unit/ResolutionGuidance/Spec350OperationFollowUpResolutionAdapterTest.php` +2. Add focused first-consumer tests: + - `apps/platform/tests/Feature/Filament/Spec350CustomerReviewWorkspaceGuidanceIntegrationTest.php` + - `apps/platform/tests/Feature/EnvironmentReview/Spec350EnvironmentReviewResolutionGuidanceTest.php` +3. Add one bounded browser smoke: + - `apps/platform/tests/Browser/Spec350OperatorResolutionGuidanceSmokeTest.php` +4. Extend or reuse Spec 347/349 regressions rather than duplicating them wholesale, and pull in Spec 346 only if a Governance Inbox consumer or inbox-facing shared helper is adopted. + +### Phase 2 - Core Contract + +1. Choose the narrowest implementation shape: + - prefer rigorously validated arrays if they fit the current support-layer style, or + - use small readonly value objects under `app/Support/ResolutionGuidance/` only if they reduce review risk without creating a new framework layer +2. Define: + - `ResolutionCase` + - `ResolutionAction` + - presentation-only severity/status/action-type vocabularies only if plain strings/constants prove insufficient +3. Keep the contract derived-only and request-scoped. +4. Do not persist or cache the new cases beyond the current request unless an existing request-scoped pattern is already in place. + +### Phase 3 - Bounded Adapters + +1. Review-pack output adapter: + - wrap `ReviewPackOutputResolutionGuidance` + - preserve current output/readiness truth + - add explicit scope, source refs, and safe action typing + - keep evidence-basis guidance inside this adapter for v1 instead of adding a standalone evidence adapter +2. Preserve current review-output exceptions: + - keep `CustomerReviewWorkspace` findings/accepted-risk follow-up overrides authoritative + - keep customer-workspace detail mode on `EnvironmentReviewResource` free of repeated primary-action rails + +### Phase 4 - Rendering And Required First Consumers + +1. Add one reusable rendering path only if it reduces real duplication: + - e.g. `resolution-guidance-card.blade.php` and list wrapper +2. Required first consumers: + - `CustomerReviewWorkspace` + - Environment Review detail +3. Preserve current review-output behavior while adopting the shared contract: + - do not regress qualified download wording + - do not regress findings/accepted-risk follow-up overrides + - do not regress customer-workspace detail-mode CTA suppression + +### Phase 5 - Optional Additional Adapters And Consumers + +1. Provider readiness adapter, only if an in-scope consumer can adopt it without a broader redesign: + - wrap current provider surface summary, verification state, and required-permissions guidance + - keep provider-specific detail inside the adapter, not the core contract +2. Operation follow-up adapter, only if an in-scope consumer can adopt it without a broader redesign: + - wrap current operation follow-up text and explanation + - enrich with scope, proof links, and stable action typing +3. Optional bounded consumers: + - Governance Inbox top recommendation + - provider readiness / required-permissions summary + - environment dashboard readiness/recommended-action cards +4. If any optional adapter or consumer requires a broader local taxonomy or layout rewrite, keep it out of Spec 350. + +### Phase 6 - Copy, Audit, And Browser Proof + +1. Update only the required copy keys in the existing localization files. +2. Update the existing relevant UI audit page reports for touched surfaces. +3. Resolve `UI-040` in the current audit registry by updating `unresolved-pages.md` unless a dedicated review-detail report is added in the implementation PR. +4. If `UI-077` is touched through a required-permissions consumer, update the current provider/support registry artifacts instead of assuming `ui-009` alone is sufficient. +5. Capture screenshots under `specs/350-operator-resolution-guidance-framework-v1/artifacts/screenshots/`. +6. Keep technical details collapsed or clearly secondary in the rendered consumers. + +### Phase 7 - Validation And Close-Out + +1. Run focused Spec 350 tests. +2. Run bounded browser smoke. +3. Re-run filtered regressions for Specs 347/349, plus Spec 346 only if a Governance Inbox consumer or inbox-facing shared helper is adopted. +4. Run `pint --dirty` and `git diff --check`. +5. Report any out-of-scope failures separately without widening implementation scope. + +## Validation Plan + +```bash +cd apps/platform +./vendor/bin/sail php vendor/bin/pest tests/Unit/ResolutionGuidance/Spec350ResolutionCaseContractTest.php tests/Unit/ResolutionGuidance/Spec350ReviewPackResolutionAdapterTest.php --compact +./vendor/bin/sail artisan test tests/Feature/Filament/Spec350CustomerReviewWorkspaceGuidanceIntegrationTest.php tests/Feature/EnvironmentReview/Spec350EnvironmentReviewResolutionGuidanceTest.php --compact +./vendor/bin/sail php vendor/bin/pest tests/Browser/Spec350OperatorResolutionGuidanceSmokeTest.php --compact +./vendor/bin/sail artisan test --compact --filter=Spec347 +./vendor/bin/sail artisan test --compact --filter=Spec349 +./vendor/bin/sail artisan test --compact --filter=CustomerReviewWorkspace +./vendor/bin/sail pint --dirty +git diff --check +``` + +Add `./vendor/bin/sail artisan test --compact --filter=Spec346` only if a Governance Inbox consumer or inbox-facing shared helper is actually adopted in-scope. Add optional provider-readiness or operation-follow-up unit tests, plus any optional consumer regressions, only if those adapters or consumers are actually adopted in-scope. + +## Deployment Impact + +- **Env vars**: none expected +- **Migrations**: none +- **Queues / scheduler**: none +- **Storage**: none +- **Assets**: no new Filament asset registration expected; `filament:assets` is not newly required by this slice diff --git a/specs/350-operator-resolution-guidance-framework-v1/repo-truth-map.md b/specs/350-operator-resolution-guidance-framework-v1/repo-truth-map.md new file mode 100644 index 00000000..eeedcd21 --- /dev/null +++ b/specs/350-operator-resolution-guidance-framework-v1/repo-truth-map.md @@ -0,0 +1,68 @@ +# Spec 350 - Repo Truth Map + +Created: 2026-06-03 +Scope: Operator Resolution Guidance Framework v1 + +This map records the repo-backed truth that Spec 350 is allowed to standardize. It exists to stop the implementation from inventing a workflow engine or a second explanation subsystem. + +## Existing Guidance Producers + +| Area | Repo evidence | Current repo-real truth | Current gap | Spec 350 handling | +|---|---|---|---|---| +| Review output guidance | `apps/platform/app/Support/ReviewPacks/ReviewPackOutputResolutionGuidance.php` | Derived review/output guidance already exists with state, reason, impact, one primary action, secondary actions, technical details, and evidence-basis semantics | local to review-output path; no shared source/scope/action envelope | required adapter; wrap, do not replace | +| Customer-safe review consumer | `apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php` | first-screen review output uses existing output guidance plus repo-real findings and accepted-risk follow-up overrides | page-local contract and helper structure | first required visible consumer; preserve overrides | +| Review detail consumer | `apps/platform/app/Filament/Resources/EnvironmentReviewResource.php` | detail surface already exposes output guidance and qualified download wording, and customer-workspace detail mode suppresses repeated action rails | not yet expressed as a shared case contract | second required visible consumer; preserve detail-mode suppression | +| Operation follow-up | `apps/platform/app/Support/OpsUx/OperationUxPresenter.php` | dominant follow-up text already exists for runs | mostly text-only; no explicit case/action/source envelope | optional bounded adapter only if an in-scope consumer needs it | +| Shared explanation | `apps/platform/app/Support/Ui/OperatorExplanation/OperatorExplanationPattern.php` | operator explanation already separates trust/reliability/next action for some domains | not a cross-domain resolution-case envelope | reuse as semantic input | +| Shared next-step shape | `apps/platform/app/Support/Ui/EnterpriseDetail/EnterpriseDetailSectionFactory.php` | `primaryNextStep()` already models main text + secondary guidance | local to enterprise-detail surfaces | reuse as action-shape prior art | +| Governance Inbox | `apps/platform/app/Filament/Pages/Governance/GovernanceInbox.php` | page already shows `Next recommended action` over lane-specific items | queue-specific, not reusable across other domains | optional bounded consumer | +| Provider readiness | `apps/platform/app/Support/Providers/TargetScope/ProviderConnectionSurfaceSummary.php` | readiness summary already exists for provider connections | no shared action-safe case envelope | optional adapter input only | +| Required permissions guidance | `apps/platform/app/Filament/Pages/EnvironmentRequiredPermissions.php` | page already exposes guidance, verification link, and provider-management link | local guidance page, not shared case contract | optional bounded consumer | +| Environment dashboard recommendations | `apps/platform/app/Support/EnvironmentDashboard/EnvironmentDashboardSummaryBuilder.php` | readiness and recommended-action summaries already exist | dashboard-local action model | optional bounded consumer | + +## Existing Shared Contracts To Reuse + +| Contract / helper | What it already solves | Spec 350 rule | +|---|---|---| +| `ReviewPackOutputResolutionGuidance` | issue + reason + impact + one action for review output | do not re-derive raw output truth | +| `OperationUxPresenter::surfaceGuidance()` | dominant follow-up text for run states | do not replace run lifecycle or toasts/notifications | +| `OperatorExplanationPattern` | trust/reliability/next-action semantics | use as semantic input, not duplicate terminology | +| `EnterpriseDetailSectionFactory::primaryNextStep()` | existing primary-next-step structure | align action naming and supporting guidance | +| `OperationRunLinks` and existing scoped URL helpers | safe deep-linking | reuse for all action destinations | + +## Verified Scope Constraints + +- No new persistence is justified by current repo truth. +- No new route family is justified. +- No new workflow engine is justified. +- No hidden shell/sidebar/topbar scope is allowed. +- Provider-specific language must remain bounded to provider-owned adapters and surfaces. +- Any executable action must defer to existing capability, confirmation, audit, and `OperationRun` paths. + +## Known Overlap / Drift Risks + +1. `ReviewPackOutputResolutionGuidance` already looks like a case contract for one domain; Spec 350 must wrap it, not supersede it. +2. `CustomerReviewWorkspace` already overrides review-output guidance for findings and accepted-risk follow-up; Spec 350 must preserve those repo-real exceptions instead of flattening them away. +3. `EnvironmentReviewResource` already suppresses repeated action rails in customer-workspace detail mode; Spec 350 must preserve that behavior. +4. `OperatorExplanationPattern` already exists for another domain family; Spec 350 must not become a competing explanation framework. +5. Governance Inbox and environment dashboard already have local next-action logic; if integrating them would require a bigger taxonomy rewrite, they stay out of scope. +6. A standalone evidence-basis adapter is not justified in v1 because the review-output path already carries evidence-basis truth through `ReviewPackOutputResolutionGuidance`. +7. Spec 347 contains a speculative follow-up note using number `350`; this is commentary only and not a real package reservation. + +## Narrowest Correct Implementation Boundary + +The current repo truth supports: + +- one shared derived case/action envelope +- one required review-output adapter over existing guidance producers +- two required visible consumers on the review-output path +- optional provider-readiness or operation-follow-up adapters only when a concrete same-slice consumer keeps the reuse cost low +- optional additional consumers only when the reuse cost stays low + +The current repo truth does not support: + +- a generic workflow engine +- a new provider-neutral execution framework +- a new persisted resolution queue +- a mandatory standalone evidence-basis adapter in v1 +- a broad dashboard or governance surface redesign hidden inside this spec diff --git a/specs/350-operator-resolution-guidance-framework-v1/spec.md b/specs/350-operator-resolution-guidance-framework-v1/spec.md new file mode 100644 index 00000000..a3aa88fb --- /dev/null +++ b/specs/350-operator-resolution-guidance-framework-v1/spec.md @@ -0,0 +1,352 @@ +# Feature Specification: Spec 350 - Operator Resolution Guidance Framework v1 + +**Feature Branch**: `350-operator-resolution-guidance-framework-v1` +**Created**: 2026-06-03 +**Status**: Draft +**Type**: Platform productization / operator guidance / derived resolution contract / future AI-ready extension point +**Runtime posture**: Reuse-first and bounded. This spec standardizes operator guidance over existing repo-backed truth. It does not introduce a workflow engine, ticket system, portal, AI execution path, or new persisted resolution state. +**Input**: User-provided full Spec 350 draft + repo truth from Specs 161, 312, 338, 346, 347, 349 and current guidance-producing runtime paths. + +## Dependencies And Repo-Truth Adjustments + +This spec is a follow-up over already repo-real guidance foundations: + +- Spec 161 - Operator Explanation Layer +- Spec 312 - Customer Review Workspace v1 Completion +- Spec 338 - Workspace / Environment Resource Scope Contract +- Spec 346 - Governance Inbox Final Operator Workflow +- Spec 347 - Review Pack Output Contract & Readiness Semantics +- Spec 349 - Customer Review Workspace Output Resolution Guidance + +Repo-truth adjustment against the user draft: + +- `App\Support\ReviewPacks\ReviewPackOutputResolutionGuidance` already exists and covers the review-pack/output path; Spec 350 must reuse it, not replace it. +- `App\Support\OpsUx\OperationUxPresenter`, `App\Support\Ui\OperatorExplanation\OperatorExplanationPattern`, and `App\Support\Ui\EnterpriseDetail\EnterpriseDetailSectionFactory::primaryNextStep()` already provide operator-guidance primitives. +- `GovernanceInbox`, `EnvironmentDashboardSummaryBuilder`, `ProviderConnectionSurfaceSummary`, and `EnvironmentRequiredPermissions` already contain repo-real next-step/readiness/guidance behavior. +- The problem is no longer "no guidance exists". The real gap is that cross-surface guidance does not share one traceable, scope-explicit, action-safe case contract. +- Spec 347 contains a speculative follow-up note naming "Spec 350" as a sellable smoke matrix, but no real `specs/350-*` package or branch existed before this prep. That speculative note is not a reserved number. + +## Spec Candidate Check *(mandatory - SPEC-GATE-001)* + +- **Problem**: Multiple strategic surfaces already expose repo-real blocker or readiness truth, but they still translate that truth into page-local guidance differently. Operators can get a reason on one page, a raw warning wall on another, a text-only next step on a third, and no traceable source/action contract across them. +- **Today's failure**: TenantPilot risks becoming diagnostics-heavy again whenever review output, evidence basis, provider readiness, or operation follow-up require the operator to infer the next safe step from inconsistent local copy and local link logic. +- **User-visible improvement**: A bounded shared `ResolutionCase` contract makes the product answer the same first-order questions consistently: what is wrong, why it matters, what the dominant next step is, and which source records prove the guidance. +- **Smallest enterprise-capable version**: Introduce one derived `ResolutionCase` / `ResolutionAction` contract over repo-real guidance producers, prove it first on Customer Review Workspace and Environment Review detail through the existing review-output path, and allow provider-readiness or operation-follow-up adoption only where the reuse cost stays bounded. Evidence-basis guidance remains embedded in the review-output path for v1 instead of becoming a standalone adapter. +- **Explicit non-goals**: No workflow engine, no ticketing, no portal, no AI execution, no new persistence, no broad dashboard rewrite, no Governance Inbox rebuild, no new provider framework, no PDF/HTML renderer, no new review lifecycle state machine, and no legal/signature semantics. +- **Permanent complexity imported**: One bounded support-layer contract, one required review-output adapter, up to two optional bounded adapters if they are actually consumed in-scope, focused unit/feature/browser tests, one reusable rendering path if needed, and new shared terminology around resolution cases and resolution actions. No new table, model, persisted enum, or queue family. +- **Why now**: Specs 346, 347, and 349 created the first clear set of real consumers. The codebase now has enough concrete guidance producers to justify a shared contract, and enough drift risk to make local copy-only fixes insufficient. +- **Why not local**: Local fixes would deepen existing drift between `ReviewPackOutputResolutionGuidance`, Governance Inbox next-action presentation, provider readiness guidance, and operation follow-up semantics. The same cross-surface problem would continue to reappear. +- **Approval class**: Core Enterprise. +- **Red flags triggered**: new meta-infrastructure, many touched surfaces, and foundation-style naming. Defense: the contract is justified by at least four real consumers, stays derived-only, and is explicitly forbidden from becoming a workflow engine or persisted taxonomy. +- **Score**: Nutzen: 2 | Dringlichkeit: 2 | Scope: 1 | Komplexitaet: 1 | Produktnaehe: 2 | Wiederverwendung: 2 | **Gesamt: 10/12** +- **Decision**: approve with reuse-first and bounded-contract constraints. + +## Candidate Source And Completed-Spec Guardrail + +- **Candidate source**: + - direct user-provided Spec 350 draft + - roadmap alignment: customer-safe review consumption, decision-centered governance workflow, provider readiness productization, and environment/dashboard next-step clarity +- **Completed-spec guardrail result**: + - no `specs/350-*` package existed before this prep + - Specs 161, 312, 338, 346, 347, and 349 already carry prepared, validated, checked-off-task, screenshot, browser-smoke, or historical implementation signals and are treated as context only + - this prep must not rewrite or normalize those completed or active historical artifacts + - the speculative Spec-347 follow-up number is treated as non-binding repo commentary, not as a claimed package +- **Close alternatives deferred**: + - sellable smoke matrix for governance/review/export flows + - provider readiness deep productization as a standalone surface spec + - customer portal boundary contract + - private AI runtime consumer or AI suggestion delivery +- **Smallest viable implementation slice**: one traceable resolution-case contract, one bounded resolution-action contract, one required review-output adapter, one reusable rendering path if it clearly reduces duplication, and first visible integration on the review-output path before any broader cross-surface adoption. + +## Spec Scope Fields *(mandatory)* + +- **Scope**: review-output-first across workspace, environment, review, and review-pack surfaces, with provider-connection or operation follow-up surfaces included only when a concrete same-slice consumer can reuse the contract without broadening scope. +- **Primary Routes**: + - `/admin/reviews/workspace` + - existing Environment Review detail route(s) + - `/admin/governance/inbox` for bounded follow-through only if reuse remains local and non-disruptive + - existing Provider Connections and Required Permissions surfaces only if a bounded provider-readiness consumer is adopted + - existing Environment Dashboard readiness/recommended-action surfaces only if a bounded optional consumer is adopted + - existing OperationRun proof/detail destinations only as linked follow-up context +- **Data Ownership**: + - `EnvironmentReview`, `ReviewPack`, `EvidenceSnapshot`, `ProviderConnection`, `OperationRun`, `Finding`, and `FindingException` remain the source-of-truth records + - any `ResolutionCase` or `ResolutionAction` stays derived-only and request-scoped unless a later spec proves independent persistence is necessary +- **RBAC**: + - workspace membership, managed-environment entitlement, and existing capability checks remain authoritative + - executable actions must continue to route through existing policies, services, confirmation rules, audit behavior, and `OperationRun` flows where already required + - cross-workspace or cross-environment access remains deny-as-not-found through the current scoped routes and policies + +For canonical-view specs: + +- **Default filter behavior when tenant-context is active**: workspace-wide surfaces keep visible local `environment_id` filtering or route-owned environment context only. No hidden shell/sidebar/topbar scope may be introduced. +- **Explicit entitlement checks preventing cross-tenant leakage**: each resolution case must carry explicit scope, and any linked source or action destination must continue to resolve through existing workspace/environment-scoped URLs and policies. + +## 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 + +## UI/Productization Coverage *(mandatory when UI Surface Impact is not "No UI surface impact")* + +- **Route/page/surface**: + - `CustomerReviewWorkspace` + - Environment Review detail output-guidance summary (`UI-040`) + - existing Governance Inbox next-recommended item when a bounded consumer is added + - existing provider readiness / required-permissions guidance surfaces (`UI-072` / `UI-077`) when a bounded consumer is added + - existing environment readiness/recommended-action cards when a bounded consumer is added +- **Current or new page archetype**: existing strategic workspace/customer-review surface plus existing strategic operator surfaces; no new route archetype +- **Design depth**: Strategic Surface for review workspace, governance inbox, provider connections, and environment dashboard; Domain Pattern Surface for review detail and required-permissions guidance +- **Repo-truth level**: repo-verified runtime surfaces with existing guidance islands; no conceptual-future-state page creation required +- **Existing pattern reused**: + - `docs/ui-ux-enterprise-audit/page-reports/ui-006-customer-review-workspace.md` + - `docs/ui-ux-enterprise-audit/page-reports/ui-004-governance-inbox.md` + - `docs/ui-ux-enterprise-audit/page-reports/ui-009-provider-connections.md` + - `docs/ui-ux-enterprise-audit/route-inventory.md` entries `UI-040` and `UI-077` + - existing target-experience briefs for customer review workspace, governance inbox, provider readiness, and environment dashboard +- **New pattern required**: one bounded review-output-first resolution-guidance contract plus a reusable card/list rendering path only if it removes real duplication; no new global UI framework or taxonomy +- **Screenshot required**: yes, under `specs/350-operator-resolution-guidance-framework-v1/artifacts/screenshots/` +- **Page audit required**: yes, update the relevant existing page reports for any surfaces actually touched during implementation. Because `UI-040` is currently unresolved in the registry, implementation must update `docs/ui-ux-enterprise-audit/unresolved-pages.md` unless it adds a dedicated review-detail page report. +- **Customer-safe review required**: yes, because review-output guidance is the first required visible consumer +- **Dangerous-action review required**: conditional only. V1 default bias is navigation, qualified download, or disclosure. If an existing source-owned executable action is surfaced, it must preserve its current confirmation, authorization, and audit behavior. +- **Coverage files updated or explicitly not needed**: + - [ ] `docs/ui-ux-enterprise-audit/route-inventory.md` + - [ ] `docs/ui-ux-enterprise-audit/design-coverage-matrix.md` + - [x] `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` + - [x] `docs/ui-ux-enterprise-audit/unresolved-pages.md` + - [ ] `N/A - no reachable UI surface impact` +- **No-impact rationale when applicable**: N/A + +## 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)**: status messaging, next-action guidance, readiness cards, decision-first summaries, proof links, evidence/report viewers, customer-safe disclosure, and cross-surface resolution actions +- **Systems touched**: + - `App\Support\ReviewPacks\ReviewPackOutputResolutionGuidance` + - `App\Support\OpsUx\OperationUxPresenter` + - `App\Support\Ui\OperatorExplanation\OperatorExplanationPattern` + - `App\Support\Ui\EnterpriseDetail\EnterpriseDetailSectionFactory` + - `App\Filament\Pages\Governance\GovernanceInbox` + - `App\Support\EnvironmentDashboard\EnvironmentDashboardSummaryBuilder` + - `App\Support\Providers\TargetScope\ProviderConnectionSurfaceSummary` + - `App\Filament\Pages\EnvironmentRequiredPermissions` +- **Existing pattern(s) to extend**: existing output-guidance, operator-explanation, primary-next-step, next-recommended-item, readiness-card, and required-permissions guidance paths +- **Shared contract / presenter / builder / renderer to reuse**: + - `ReviewPackOutputResolutionGuidance` + - `OperationUxPresenter::surfaceGuidance()` and related detail helpers + - `OperatorExplanationPattern` + - `EnterpriseDetailSectionFactory::primaryNextStep()` + - current scoped URL helpers and `OperationRunLinks` +- **Why the existing shared path is sufficient or insufficient**: the repo already has strong local guidance producers, but they do not share one explicit, scope-carrying, source-traceable, action-safe case envelope that later consumers can reuse without copying logic +- **Allowed deviation and why**: one bounded `ResolutionCase` / `ResolutionAction` normalizer plus adapter layer is allowed if it wraps current truth and avoids creating a third independent explanation framework +- **Consistency impact**: the same issue class should expose the same reading direction: title, reason, impact, one dominant action, secondary actions, and source references +- **Review focus**: prevent a parallel workflow engine, prevent new persisted state, prevent a second generic explanation taxonomy, and verify reuse over the existing guidance producers instead of replacement-by-rewrite + +## 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, but only by standardizing follow-up guidance and deep-link metadata over existing run detail/proof surfaces +- **Shared OperationRun UX contract/layer reused**: + - `OperationRunLinks` + - `OperationUxPresenter` + - existing run-detail and proof-link destinations +- **Delegated start/completion UX behaviors**: unchanged; queueing, dedupe, toasts, and terminal notification behavior remain owned by existing OperationRun UX flows +- **Local surface-owned behavior that remains**: dominant resolution ranking, action selection, and grouped supporting details per resolution case +- **Queued DB-notification policy**: unchanged +- **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?**: yes +- **Boundary classification**: mixed +- **Seams affected**: provider readiness, required-permissions guidance, verification follow-up, and shared operator vocabulary for cross-surface resolution cases +- **Neutral platform terms preserved or introduced**: provider, workspace, environment, evidence basis, operation, resolution case, resolution action, source reference +- **Provider-specific semantics retained and why**: provider-owned adapters may still reference provider-specific permission or verification details where the underlying surface is already provider-owned and repo-real +- **Why this does not deepen provider coupling accidentally**: the framework core names neutral case/action fields and keeps provider-specific labels inside the provider adapter, provider surface, and existing verification/required-permissions truth +- **Follow-up path**: deeper provider readiness productization remains a follow-up slice, not part of the core contract + +## 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 | +|---|---|---|---|---|---|---| +| Customer Review Workspace output guidance | yes | Native Filament page + existing Blade composition | review/output guidance, customer-safe disclosure | page, detail, URL-query | no | first required visible consumer; preserve current findings and accepted-risk follow-up overrides | +| Environment Review detail output-guidance summary | yes | Native Filament resource/detail | review/output guidance, qualified download | detail | no | paired with workspace handoff; preserve customer-workspace detail-mode CTA suppression | +| Governance Inbox next-recommended item | possible | Native Filament page | next recommended action, queue-clearing guidance | page | yes | only if reuse remains bounded | +| Provider readiness / required permissions guidance | possible | Native Filament resource/page | readiness, permission gap, safe next action | page, detail | yes | only if current truth can map cleanly | +| Environment dashboard readiness cards | possible | Existing builder + dashboard view | readiness, blocker, next action | page | yes | only if no new dashboard taxonomy is needed | + +## 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 | +|---|---|---|---|---|---|---|---| +| Customer Review Workspace | Primary Decision Surface | Decide whether the current output is customer-safe, limited, or blocked | title, reason, impact, one primary action | evidence basis, technical details, operation proof | primary because this is the first review-output handoff surface | review consumption and handoff | removes warning-wall interpretation | +| Environment Review detail | Secondary Context | Understand why the current review cannot be repaired directly or can be shared | review status, output readiness, publication/sharing state, next action | sections, truth details, proof links | secondary because it deepens the chosen review | detail follow-up | avoids duplicate status dialects | +| Governance Inbox | Primary Decision Surface when consumed | Decide which governance item to clear next | lane, reason, impact, dominant action | source detail, diagnostics, decision history | primary for internal queue work | queue-clearing | keeps source-family detail secondary | +| Provider readiness | Primary Decision Surface when consumed | Decide whether readiness is blocked and what safe step resolves it | readiness posture, gap, one action | verification detail, permission matrix, audit trace | primary for integration readiness | provider recovery | reduces raw integration-detail scanning | +| Environment dashboard | Secondary Context when consumed | Decide which blocked domain deserves the next drilldown | blocker, impact, one next action | backup, recovery, operations, provider detail | secondary because it routes into domain owners | environment command surface | avoids equal-weight dashboard signals | + +## 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 | +|---|---|---|---|---|---|---|---| +| Review-output surfaces | customer-safe, operator-MSP | title, reason, impact, action, boundary | technical details, evidence basis, operation proof | raw payloads, internal diagnostics | one explicit next step | raw/support detail hidden or secondary | workspace states the issue once; detail adds proof | +| Internal operator surfaces | operator-MSP, manager, support | title, reason, impact, dominant action, scope | source refs, verification detail, run detail | raw provider/runtime payloads | one explicit next step | raw/support detail stays secondary or capability-gated | top card/lane explains first; later sections add evidence | + +## 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 | +|---|---|---|---|---|---|---|---|---|---|---|---|---|---| +| Customer Review Workspace | Utility / Workspace Decision | Customer-safe workspace hub | create/open draft, refresh inputs, open evidence, or download pack depending on case | explicit CTA in decision card | forbidden | grouped secondary links/disclosure | none in spec core | `/admin/reviews/workspace` | existing Environment Review and Review Pack detail/download | workspace + visible environment filter | Review output | issue, reason, impact, primary action | none | +| Environment Review detail | Detail / Artifact + Review Context | Existing review detail | inspect current review or follow next action | existing detail view | current repo-real behavior only | secondary links/disclosures | existing lifecycle actions stay separate | existing review collection route | existing review detail route | workspace/environment + customer-workspace context | Environment review | issue, status dimensions, primary action | none | +| Governance Inbox | Queue / Decision Surface | Existing governance queue | review the top open item | existing lane card / top recommendation | existing repo-real behavior | source detail / more context | existing mutations remain source-owned | `/admin/governance/inbox` | existing finding/exception/decision/review destinations | workspace + visible environment filter | Governance item | reason, impact, primary action | only if consumed | +| Provider readiness | Readiness / Configuration Surface | Existing provider list/detail or required-permissions page | open required permissions, rerun verification, or review readiness | explicit CTA in summary card | existing resource behavior | grouped links/disclosure | existing high-impact actions stay separately governed | existing provider routes | existing provider detail/required-permissions routes | workspace/environment | Provider readiness | gap, impact, primary action | only if consumed | + +## 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 | +|---|---|---|---|---|---|---|---|---|---|---| +| Resolution-guided review output | Customer-safe reader / MSP operator | Understand whether output is trustworthy and what next step is allowed | review workspace + review detail | What is wrong, why, and what do I do next? | title, reason, impact, one action, explicit scope | technical details, source refs, proof links | review status, output readiness, sharing boundary | existing review or evidence flows only | open review, open evidence, qualified download, existing follow-up override targets | existing publish/archive remain separate | +| Resolution-guided internal follow-up | Operator / manager | Review readiness, provider, or operation blockers without diagnostics-first scanning | queue/dashboard/readiness summary when an optional consumer is explicitly adopted | What blocks progress now, and which safe path should I take? | title, reason, impact, one action, explicit scope | run detail, verification detail, source detail | readiness, evidence, operation follow-up | existing domain actions only | open required permissions, open operation proof, open review context | existing high-impact actions remain source-owned | + +## Proportionality Review *(mandatory when structural complexity is introduced)* + +- **New source of truth?**: no +- **New persisted entity/table/artifact?**: no +- **New abstraction?**: yes, one bounded derived resolution-case/action contract plus thin adapters +- **New enum/state/reason family?**: not by default. If presentation-only vocabularies are still needed after implementation review, they must stay non-persisted and narrower than the current source truth. +- **New cross-domain UI framework/taxonomy?**: no. This spec allows one bounded case/action envelope over already-existing guidance producers, not a new UI framework. +- **Current operator problem**: different strategic surfaces already know something is blocked, risky, stale, or follow-up-required, but they do not all tell the operator the next safe action in the same traceable way +- **Existing structure is insufficient because**: current guidance producers are local and incompatible in shape; cross-surface reuse currently means copy/paste mapping or divergent local heuristics +- **Narrowest correct implementation**: add a thin derived contract and bounded adapters that wrap existing truth and existing actions instead of replacing existing explanation/guidance systems +- **Ownership cost**: new support-layer vocabulary, adapter tests, cross-surface review burden, and care to avoid a parallel framework +- **Alternative intentionally rejected**: local copy-only fixes, a new workflow engine, a fully generic issue taxonomy, new persistence, and a broad provider-neutral execution framework +- **Release truth**: current-release productization and trust hardening over already-existing guidance paths; AI remains documentation-only + +### 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**: Unit + Feature + Browser +- **Validation lane(s)**: fast-feedback + confidence + browser +- **Why this classification and these lanes are sufficient**: the central risk is deterministic mapping and first-screen operator behavior, not DB engine semantics or heavy suite discovery +- **New or expanded test families**: one small `ResolutionGuidance` unit family plus focused integrations for review/workspace/detail consumers +- **Fixture / helper cost impact**: reuse existing review, evidence, provider, and operation fixtures where possible; do not widen default helpers +- **Heavy-family visibility / justification**: browser coverage is explicit because this spec changes first-screen decision hierarchy on strategic surfaces +- **Special surface test profile**: `global-context-shell` + `shared-detail-family` + bounded strategic surface smoke +- **Standard-native relief or required special coverage**: special coverage required because this feature changes one-primary-action, grouped disclosure, and action-safe next-step behavior across shared surfaces +- **Reviewer handoff**: verify the new contract stays derived-only, that each adapter reuses existing truth, and that validation commands stay focused +- **Budget / baseline / trend impact**: low to moderate; one new focused browser smoke, a small unit family, and several focused feature tests +- **Escalation needed**: document-in-feature if a proposed consumer needs a broader dashboard or provider rewrite +- **Active feature PR close-out entry**: Guardrail / Smoke Coverage +- **Planned validation commands**: + - focused Spec 350 unit/feature tests for contract + adapters + - focused review/workspace/detail integration tests + - one bounded browser smoke + - filtered regressions for Specs 347/349, plus Spec 346 only if a Governance Inbox consumer or shared inbox-facing helper is adopted + - `pint --dirty` + - `git diff --check` + +## User Scenarios & Testing *(mandatory)* + +### User Story 1 - See one safe next step for blocked review output (Priority: P1) + +An operator opening Customer Review Workspace or review detail must understand immediately whether the current review output is customer-safe, limited, or blocked, and which repo-real next step is safest. + +**Why this priority**: This is the clearest current sellability and trust surface, and it already has real guidance logic from Specs 347 and 349. + +**Independent Test**: Seed published-blocked, draft-blocked, and customer-safe-ready review states and confirm the surface shows one dominant action with scope-safe secondary proof. + +**Acceptance Scenarios**: + +1. **Given** a published review whose evidence basis is incomplete, **When** the operator opens the workspace, **Then** the page explains that the output is not customer-ready and recommends the correct next repo-real step instead of a warning wall. +2. **Given** a current draft review that can still be refreshed, **When** the operator opens the same surface, **Then** the page recommends refreshing review inputs or opening the draft instead of claiming the published review can be fixed directly. + +--- + +### User Story 2 - Reuse the same issue/action structure across bounded additional consumers (Priority: P2) + +An operator working across review output and any additional bounded provider-readiness or operation-follow-up consumer needs the same reading direction: issue, reason, impact, one action, supporting proof. + +**Why this priority**: The product already has multiple real guidance islands; consistency is the current productization gap, not raw missing data. + +**Independent Test**: Confirm the required review-output contract shape is deterministic, and if an additional provider-readiness or operation-follow-up adapter is adopted in-scope, confirm it emits the same contract shape with explicit scope, source refs, and one primary action. + +**Acceptance Scenarios**: + +1. **Given** a provider permission gap or an operation follow-up-required run is adopted as an in-scope consumer, **When** the adapter builds a resolution case, **Then** the case contains exactly one primary action, explicit scope, and source references. +2. **Given** a consumer surface lacks the safety fields required to expose an existing executable path safely, **When** the case is rendered, **Then** the primary action degrades to navigation, qualified download, disclosure, or `none` instead of exposing a fake fix button. + +--- + +### User Story 3 - Keep execution safe and AI deferred (Priority: P3) + +A workspace owner or reviewer needs resolution actions to remain capability-aware, confirmation-aware, auditable, and clearly human-approved when they reuse existing executable flows, with any future AI connection documented but not active. + +**Why this priority**: A shared guidance contract becomes dangerous if it implies executable authority without preserving the existing safe-execution rules. + +**Independent Test**: Review mutating and non-mutating action cases, confirm safety metadata is present or the action degrades, and confirm no AI call or AI-visible runtime field is introduced. + +**Acceptance Scenarios**: + +1. **Given** a case whose primary action reuses an existing source-owned executable path, **When** the contract is built, **Then** the action includes capability, confirmation, audit, and `OperationRun` hints where applicable, or becomes non-executable. +2. **Given** the future AI extension document, **When** a reviewer inspects the package, **Then** they can see the reserved extension fields and mandatory gates without any active AI execution path in scope. + +## Requirements *(mandatory)* + +### Functional Requirements + +- **FR-350-001**: The system MUST define a shared derived `ResolutionCase` contract for operator guidance and a shared derived `ResolutionAction` contract for the dominant next step. +- **FR-350-002**: The contract MUST be scope-explicit and traceable to repo-backed source records; no hidden session or shell scope is allowed. +- **FR-350-003**: Each resolution case MUST expose exactly one primary resolution action. +- **FR-350-004**: V1 primary actions MUST default to navigation, qualified download, disclosure, or `none` unless the action is reusing an existing source-owned executable path that already has a repo-real safety envelope. +- **FR-350-005**: If a primary action reuses an existing source-owned executable path, it MUST carry the safety metadata required to preserve existing capability, confirmation, audit, and `OperationRun` behavior. +- **FR-350-006**: If full safe-execution metadata cannot be provided, the action MUST degrade to navigation, qualified download, disclosure, or `none` instead of rendering a fake fix path. +- **FR-350-007**: The required v1 adapter set MUST cover review-pack output guidance, including evidence-basis gaps already surfaced through `ReviewPackOutputResolutionGuidance`; v1 MUST NOT introduce a standalone evidence-basis adapter. +- **FR-350-008**: Customer Review Workspace and Environment Review detail MUST be the first required visible consumers of the shared contract, while preserving current `findings_follow_up_required`, `accepted_risk_follow_up_required`, and customer-workspace detail-mode CTA-suppression behavior. +- **FR-350-009**: If operation follow-up is adopted as an in-scope consumer, the adapter MUST reuse existing `OperationUxPresenter`, `OperatorExplanationPattern`, and existing proof-link destinations where applicable. +- **FR-350-010**: If provider readiness is adopted as an in-scope consumer, the adapter MUST reuse existing required-permissions, verification, and provider-surface truth instead of inventing a new provider readiness engine. +- **FR-350-011**: Governance Inbox, provider readiness surfaces, required-permissions surfaces, and environment readiness cards MAY consume the same contract only when the reuse remains bounded and does not require a broader surface redesign. +- **FR-350-012**: Technical details, source references, and supporting evidence MUST remain secondary to issue, reason, impact, and primary action. +- **FR-350-013**: The framework MUST not introduce persistence unless a later spec proves independent lifecycle truth is necessary. +- **FR-350-014**: The framework MUST not introduce a workflow engine, approval engine, queue family, or mandatory standalone evidence-guidance subsystem. +- **FR-350-015**: The framework MUST document a future AI/HITL extension point, but MUST NOT render or execute AI suggestions in v1. + +### Non-Functional Requirements + +- **NFR-350-001**: Default-visible guidance must be calm, enterprise-safe, and decision-first: one issue, one reason, one impact, one primary action. +- **NFR-350-002**: The core contract must remain provider-neutral even when a provider-owned adapter exposes provider-specific detail. +- **NFR-350-003**: Guidance derivation must stay bounded to the already-loaded or already-scoped records of the current surface wherever possible. +- **NFR-350-004**: The contract must be testable as deterministic data without requiring browser-only proof for every adapter. +- **NFR-350-005**: Existing workspace/environment isolation, signed-download safety, and authorization boundaries must remain unchanged. + +## Risks + +- **Risk 1 - A third parallel guidance framework appears**: Mitigation: require reuse of existing guidance producers and forbid replacement-by-rewrite. +- **Risk 2 - The spec becomes too abstract**: Mitigation: keep the required proof on already-productized review surfaces, keep evidence-basis guidance inside the review-output adapter, and defer any broader provider/dashboard/gov adoption unless it stays bounded. +- **Risk 3 - Provider surfaces force a broader rewrite**: Mitigation: make deeper provider readiness adoption optional and bounded inside this spec; escalate broader gaps as follow-up work. +- **Risk 4 - Suggested actions imply authority the runtime does not actually have**: Mitigation: degrade to navigation/disclosure when safety metadata is incomplete. + +## Deferred Follow-Up Candidates + +- Sellable smoke matrix for governance, review, evidence, and export flows +- Provider readiness deeper productization +- Customer portal output-boundary contract +- Private AI resolution suggestion runtime consumer diff --git a/specs/350-operator-resolution-guidance-framework-v1/tasks.md b/specs/350-operator-resolution-guidance-framework-v1/tasks.md new file mode 100644 index 00000000..5179481f --- /dev/null +++ b/specs/350-operator-resolution-guidance-framework-v1/tasks.md @@ -0,0 +1,131 @@ +# Tasks: Spec 350 - Operator Resolution Guidance Framework v1 + +**Input**: `specs/350-operator-resolution-guidance-framework-v1/spec.md`, `plan.md`, `repo-truth-map.md`, `contracts/`, and `checklists/requirements.md` + +**Tests**: Required. This is a cross-surface operator-guidance and trust-surface change over existing Filament pages, detail surfaces, and support-layer guidance producers. + +## Test Governance Checklist + +- [x] Lane assignment is explicit and narrow: Unit for contract/adapters, Feature for surface integration, Browser for first-screen trust proof. +- [x] New or changed tests stay in the smallest honest family, and the browser addition is explicit. +- [x] Shared helpers, factories, seeds, fixtures, and context defaults stay cheap by default. +- [x] Planned validation commands cover the change without pulling unrelated lane cost. +- [x] The declared surface profiles (`global-context-shell` and `shared-detail-family`) are explicit. +- [x] Any new abstraction remains derived-only and does not create hidden persistence or a workflow engine. + +## Phase 1: Preparation And Repo Truth + +**Purpose**: Keep the implementation bounded to the existing guidance-producing runtime paths and prevent a third parallel framework. + +- [x] T001 Re-read `spec.md`, `plan.md`, `repo-truth-map.md`, all contract docs, and `checklists/requirements.md` before runtime changes. +- [x] T002 Re-read related historical context only: Specs 161, 312, 338, 346, 347, and 349. Do not modify their artifacts. +- [x] T003 Re-verify the current runtime truth in `apps/platform/app/Support/ReviewPacks/ReviewPackOutputResolutionGuidance.php`. +- [x] T004 Re-verify the current runtime truth in `apps/platform/app/Support/OpsUx/OperationUxPresenter.php`. +- [x] T005 Re-verify the current runtime truth in `apps/platform/app/Support/Ui/OperatorExplanation/OperatorExplanationPattern.php` and `apps/platform/app/Support/Ui/EnterpriseDetail/EnterpriseDetailSectionFactory.php`. +- [x] T006 Re-verify the current runtime truth in `apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php` and `apps/platform/app/Filament/Resources/EnvironmentReviewResource.php`. +- [x] T007 Re-verify the current runtime truth in `apps/platform/app/Filament/Pages/Governance/GovernanceInbox.php`, `apps/platform/app/Support/EnvironmentDashboard/EnvironmentDashboardSummaryBuilder.php`, `apps/platform/app/Support/Providers/TargetScope/ProviderConnectionSurfaceSummary.php`, and `apps/platform/app/Filament/Pages/EnvironmentRequiredPermissions.php`. +- [x] T008 Keep `specs/350-operator-resolution-guidance-framework-v1/repo-truth-map.md` current if runtime inspection reveals a narrower or broader bounded truth. +- [x] T009 Confirm no migration, package, env var, queue family, scheduler change, storage-topology change, panel/provider change, or global-search change is required. +- [x] T010 Confirm Filament v5 / Livewire v4.0+ compliance and that panel provider registration remains `apps/platform/bootstrap/providers.php`. + +## Phase 2: Tests First + +**Purpose**: Lock the central contract, adapter semantics, and first visible consumers before runtime refactor. + +- [x] T011 Add `apps/platform/tests/Unit/ResolutionGuidance/Spec350ResolutionCaseContractTest.php`. +- [x] T012 Add `apps/platform/tests/Unit/ResolutionGuidance/Spec350ReviewPackResolutionAdapterTest.php`. +- [ ] T013 Only if a provider-readiness adapter is adopted in-scope, add `apps/platform/tests/Unit/ResolutionGuidance/Spec350ProviderReadinessResolutionAdapterTest.php`. +- [ ] T014 Only if an operation-follow-up adapter is adopted in-scope, add `apps/platform/tests/Unit/ResolutionGuidance/Spec350OperationFollowUpResolutionAdapterTest.php`. +- [x] T015 Add `apps/platform/tests/Feature/Filament/Spec350CustomerReviewWorkspaceGuidanceIntegrationTest.php`. +- [x] T016 Add `apps/platform/tests/Feature/EnvironmentReview/Spec350EnvironmentReviewResolutionGuidanceTest.php`. +- [x] T017 Add `apps/platform/tests/Browser/Spec350OperatorResolutionGuidanceSmokeTest.php`. +- [x] T018 Add assertions that every case has explicit scope, source refs, evidence refs where applicable, exactly one primary action, and no fake execution paths. +- [x] T019 Add assertions that executable actions are modeled only when a source-owned safety envelope exists, otherwise they degrade to navigation, qualified download, disclosure, or `none`. +- [x] T020 Reuse or extend existing Spec 347/349 regressions instead of duplicating their full runtime coverage; pull in Spec 346 only if a Governance Inbox consumer or inbox-facing shared helper is adopted. + +## Phase 3: Core Contract + +**Purpose**: Introduce the narrowest shared case/action envelope that can wrap the existing guidance producers. + +- [x] T021 Choose the narrowest contract shape under `apps/platform/app/Support/ResolutionGuidance/`, preferring validated arrays unless small readonly value objects clearly reduce review risk. +- [ ] T022 If value objects are the narrowest shape, create `apps/platform/app/Support/ResolutionGuidance/ResolutionCase.php`. +- [ ] T023 If value objects are the narrowest shape, create `apps/platform/app/Support/ResolutionGuidance/ResolutionAction.php`. +- [ ] T024 Only add presentation-only supporting enums/value objects for severity, status, or action type if plain strings/constants prove insufficient. +- [x] T025 Ensure the contract stays derived-only and request-scoped; do not add persistence or request-crossing cache behavior. +- [x] T026 Ensure the contract carries explicit scope, one primary action, secondary actions, source refs, evidence refs where applicable, and technical-detail disclosure payloads. +- [x] T027 Ensure the contract shape can wrap existing `ReviewPackOutputResolutionGuidance`, `OperationUxPresenter`, `OperatorExplanationPattern`, and `primaryNextStep` semantics without replacing them. +- [x] T028 Add validation/mapping tests proving unsupported or unsafe executable actions degrade to navigation, qualified download, disclosure, or `none`. + +## Phase 4: Review-Pack Adapter And Review-Output Guardrails + +**Purpose**: Reuse the existing review-output guidance work and extend it into the shared contract without reopening Spec 347 or Spec 349 truth. + +- [x] T029 Create `apps/platform/app/Support/ResolutionGuidance/Adapters/ReviewPackOutputResolutionAdapter.php`. +- [x] T030 Wrap `ReviewPackOutputResolutionGuidance` so review-output cases expose explicit scope, source refs, evidence refs, and safe action typing. +- [x] T031 Keep evidence-basis guidance inside the review-output adapter for v1; do not introduce a standalone `EvidenceBasisResolutionAdapter`. +- [x] T032 Keep published-versus-draft review immutability and next-step rules aligned with current repo truth and current customer-workspace/detail behavior. +- [x] T033 Preserve current `CustomerReviewWorkspace` findings-follow-up and accepted-risk follow-up overrides instead of flattening them into the shared contract. +- [x] T034 Preserve current customer-workspace detail-mode CTA suppression in `EnvironmentReviewResource`. +- [x] T035 Add focused tests for blocked published review, draft-refresh path, evidence-missing path, follow-up override behavior, and safe disclosure fallback. + +## Phase 5: Optional Provider And Operation Adapters + +**Purpose**: Standardize provider-readiness and operation-follow-up guidance only if a concrete same-slice consumer can adopt them without rebuilding those domains. + +- [ ] T036 Only if a provider-readiness consumer is adopted in-scope, create `apps/platform/app/Support/ResolutionGuidance/Adapters/ProviderReadinessResolutionAdapter.php`. +- [ ] T037 If the provider adapter is adopted, wrap existing provider summary, required-permissions, verification, and provider-owned next-step truth without inventing a new provider readiness engine. +- [ ] T038 If the provider adapter is adopted, keep provider-specific terms and permission details inside the provider adapter and provider surfaces, not in the core contract. +- [ ] T039 Only if an operation-follow-up consumer is adopted in-scope, create `apps/platform/app/Support/ResolutionGuidance/Adapters/OperationFollowUpResolutionAdapter.php`. +- [ ] T040 If the operation adapter is adopted, wrap `OperationUxPresenter`, existing proof links, and operator explanation truth into explicit follow-up cases with safe action typing. +- [ ] T041 If the operation adapter is adopted, ensure operation-follow-up cases do not change queueing, dedupe, terminal notification, or run lifecycle behavior. +- [ ] T042 Only if optional adapters are adopted, add focused tests for provider gaps or operation follow-up plus proof-link-based fallback behavior. + +## Phase 6: Rendering And First Consumers + +**Purpose**: Apply the shared contract where it already has the strongest repo-real value and keep broader rollout bounded. + +- [ ] T043 Create `apps/platform/resources/views/components/resolution-guidance-card.blade.php` only if it reduces real duplication across the first consumers. +- [ ] T044 Create `apps/platform/resources/views/components/resolution-guidance-list.blade.php` only if the list wrapper reduces duplication without creating a new global UI framework. +- [x] T045 Update `apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php` to consume the shared contract via the review-output adapter without regressing current follow-up overrides. +- [x] T046 Update `apps/platform/resources/views/filament/pages/reviews/customer-review-workspace.blade.php` so the first decision block renders the shared case shape. +- [x] T047 Update `apps/platform/app/Filament/Resources/EnvironmentReviewResource.php` to expose the shared case shape for output guidance and qualified download behavior while preserving customer-workspace detail mode. +- [x] T048 Update `apps/platform/app/Filament/Resources/EnvironmentReviewResource/Pages/ViewEnvironmentReview.php` or its supporting state so the detail surface uses the same case/action reading direction without reintroducing repeated primary-action rails. +- [x] T049 Keep technical details collapsed or clearly secondary in the first visible consumers. +- [ ] T050 Only if reuse remains bounded, integrate the same contract into the Governance Inbox top recommendation without replacing the existing lane model. +- [ ] T051 Only if reuse remains bounded, integrate the same contract into provider readiness or required-permissions summary surfaces without redesigning the full provider surface. +- [ ] T052 Only if reuse remains bounded, integrate the same contract into environment dashboard readiness/recommended-action summaries without introducing a new dashboard taxonomy. + +## Phase 7: Copy, Audit, And Browser Proof + +**Purpose**: Align copy, audit artifacts, and screenshots with the shared contract. + +- [x] T053 Update only the required guidance localization keys in `apps/platform/lang/en/localization.php` when new copy is actually required; existing copy remained sufficient in this slice. +- [x] T054 Update matching keys in `apps/platform/lang/de/localization.php` when new copy is actually required; existing copy remained sufficient in this slice. +- [x] T055 Update `docs/ui-ux-enterprise-audit/page-reports/ui-006-customer-review-workspace.md` for the required first consumer changes. +- [x] T056 Resolve `UI-040` in `docs/ui-ux-enterprise-audit/unresolved-pages.md` unless a dedicated Environment Review detail report is added in the implementation PR. +- [ ] T057 If Governance Inbox is consumed, update `docs/ui-ux-enterprise-audit/page-reports/ui-004-governance-inbox.md`. +- [ ] T058 If provider readiness or required permissions is consumed, update `docs/ui-ux-enterprise-audit/page-reports/ui-009-provider-connections.md` and any current `UI-077` registry artifact that records Required Permissions coverage. +- [x] T059 Capture screenshots under `specs/350-operator-resolution-guidance-framework-v1/artifacts/screenshots/`. + +## Phase 8: Validation + +**Purpose**: Prove the contract stays bounded and preserves existing trust/safety rules. + +- [x] T060 Run `cd apps/platform && ./vendor/bin/sail php vendor/bin/pest tests/Unit/ResolutionGuidance/Spec350ResolutionCaseContractTest.php tests/Unit/ResolutionGuidance/Spec350ReviewPackResolutionAdapterTest.php --compact`. +- [x] T061 Run `cd apps/platform && ./vendor/bin/sail artisan test tests/Feature/Filament/Spec350CustomerReviewWorkspaceGuidanceIntegrationTest.php tests/Feature/EnvironmentReview/Spec350EnvironmentReviewResolutionGuidanceTest.php --compact`. +- [x] T062 Run `cd apps/platform && ./vendor/bin/sail php vendor/bin/pest tests/Browser/Spec350OperatorResolutionGuidanceSmokeTest.php --compact`. +- [ ] T063 Only if a Governance Inbox consumer or inbox-facing shared helper is adopted, run `cd apps/platform && ./vendor/bin/sail artisan test --compact --filter=Spec346`. +- [x] T064 Run `cd apps/platform && ./vendor/bin/sail artisan test --compact --filter=Spec347`. +- [x] T065 Run `cd apps/platform && ./vendor/bin/sail artisan test --compact --filter=Spec349`. +- [x] T066 Run `cd apps/platform && ./vendor/bin/sail artisan test --compact --filter=CustomerReviewWorkspace`. +- [ ] T067 Only if optional provider-readiness or operation consumers are adopted, run their focused unit tests and any additional surface regressions. +- [x] T068 Run `cd apps/platform && ./vendor/bin/sail pint --dirty` and `git diff --check`. + +## Non-Goals Checklist + +- [ ] NT001 Do not create a new persisted resolution entity, table, or runtime-owned state machine. +- [ ] NT002 Do not create a workflow engine, approval engine, or queue family. +- [ ] NT003 Do not replace `ReviewPackOutputResolutionGuidance`, `OperationUxPresenter`, or `OperatorExplanationPattern` with a greenfield subsystem. +- [ ] NT004 Do not broaden dashboard, governance inbox, or provider readiness into redesign work if bounded consumption proves insufficient. +- [ ] NT005 Do not add AI execution, AI summaries, or AI-visible runtime suggestions. +- [ ] NT006 Do not weaken current workspace/environment scope, authorization, signed-download safety, or existing destructive-action safeguards.