feat: operator resolution guidance framework v1 (spec 350)
Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 59s

Implemented the first version of the operator resolution guidance framework. Added new foundation classes (ResolutionCase, ResolutionAction) and a ReviewPackOutputResolutionAdapter. Updated the Customer Review Workspace and Environment Review Resource to use the new adapter. Added extensive test coverage for the framework and UI integrations.
This commit is contained in:
Ahmed Darrazi 2026-06-03 15:36:33 +02:00
parent 9b46c0e435
commit 02cc12f73f
28 changed files with 2590 additions and 65 deletions

View File

@ -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<array{label:string,url:?string,kind:string,icon:string}>,
* secondary_actions:list<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}>,
* resolution_case:array<string, mixed>,
* output_guidance:array<string, mixed>
* }
*/
@ -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<string, mixed> $outputGuidance
* @return array<string, mixed>
*/
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<string, mixed> $baseCase
* @param array<string, mixed> $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<string, mixed>
*/
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,

View File

@ -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;

View File

@ -0,0 +1,203 @@
<?php
declare(strict_types=1);
namespace App\Support\ResolutionGuidance\Adapters;
use App\Models\EnvironmentReview;
use App\Models\EvidenceSnapshot;
use App\Models\ReviewPack;
use App\Support\ResolutionGuidance\ResolutionAction;
use App\Support\ResolutionGuidance\ResolutionCase;
use App\Support\ReviewPacks\ReviewPackOutputResolutionGuidance;
final class ReviewPackOutputResolutionAdapter
{
/**
* @param array<string, mixed> $guidance
* @return array{
* key:string,
* scope:array<string, int|string>,
* 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<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
* }>,
* source_refs:list<array{type:string,id:int|string}>,
* evidence_refs:list<array{type:string,id:int|string}>,
* technical_details:array<string, string>
* }
*/
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<string, mixed> $guidance
* @return list<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
* }>
*/
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<array{type:string,id:int|string}>
*/
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<array{type:string,id:int|string}>
*/
private static function evidenceRefs(EnvironmentReview $review): array
{
if (! $review->evidenceSnapshot instanceof EvidenceSnapshot) {
return [];
}
return [[
'type' => 'evidence_snapshot',
'id' => (int) $review->evidenceSnapshot->getKey(),
]];
}
/**
* @param array<string, mixed> $guidance
* @return array<string, string>
*/
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',
};
}
}

View File

@ -0,0 +1,206 @@
<?php
declare(strict_types=1);
namespace App\Support\ResolutionGuidance;
final class ResolutionAction
{
public const string TYPE_NAVIGATION = 'navigation';
public const string TYPE_DOWNLOAD = 'download';
public const string TYPE_DISCLOSURE = 'disclosure';
public const string TYPE_NONE = 'none';
public const string TYPE_DOMAIN_ACTION = 'domain_action';
public const string TYPE_OPERATION_ACTION = 'operation_action';
/**
* @param array{
* key?:mixed,
* label?:mixed,
* type?:mixed,
* url?:mixed,
* icon?:mixed,
* kind?:mixed,
* capability?:mixed,
* requires_confirmation?:mixed,
* audit_event?:mixed,
* operation_run_type?:mixed,
* disabled_reason?:mixed
* }|null $action
* @return 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
* }
*/
public static function fromArray(?array $action, string $fallbackKey, string $fallbackLabel = 'Unavailable'): array
{
$label = is_string($action['label'] ?? null) && trim((string) $action['label']) !== ''
? trim((string) $action['label'])
: $fallbackLabel;
$url = is_string($action['url'] ?? null) && trim((string) $action['url']) !== ''
? trim((string) $action['url'])
: null;
$rawType = is_string($action['type'] ?? null) && trim((string) $action['type']) !== ''
? trim((string) $action['type'])
: null;
$rawKind = is_string($action['kind'] ?? null) && trim((string) $action['kind']) !== ''
? trim((string) $action['kind'])
: null;
$key = is_string($action['key'] ?? null) && trim((string) $action['key']) !== ''
? trim((string) $action['key'])
: $fallbackKey;
$type = $rawType ?? self::typeFromKind($rawKind, $url);
$capability = is_string($action['capability'] ?? null) && trim((string) $action['capability']) !== ''
? trim((string) $action['capability'])
: null;
$requiresConfirmation = (bool) ($action['requires_confirmation'] ?? false);
$auditEvent = is_string($action['audit_event'] ?? null) && trim((string) $action['audit_event']) !== ''
? trim((string) $action['audit_event'])
: null;
$operationRunType = is_string($action['operation_run_type'] ?? null) && trim((string) $action['operation_run_type']) !== ''
? trim((string) $action['operation_run_type'])
: null;
if (self::isUnsafeExecutable($type, $capability, $auditEvent, $requiresConfirmation, $operationRunType)) {
$type = self::fallbackType($rawKind, $url);
$capability = null;
$requiresConfirmation = false;
$auditEvent = null;
$operationRunType = null;
}
$kind = self::kindFromType($type, $rawKind);
$icon = is_string($action['icon'] ?? null) && trim((string) $action['icon']) !== ''
? trim((string) $action['icon'])
: self::iconForType($type);
$disabledReason = is_string($action['disabled_reason'] ?? null) && trim((string) $action['disabled_reason']) !== ''
? trim((string) $action['disabled_reason'])
: null;
return [
'key' => $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;
}
}

View File

@ -0,0 +1,108 @@
<?php
declare(strict_types=1);
namespace App\Support\ResolutionGuidance;
final class ResolutionCase
{
/**
* @param array<string, int|string> $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<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
* }> $secondaryActions
* @param list<array{type:string,id:int|string}> $sourceRefs
* @param list<array{type:string,id:int|string}> $evidenceRefs
* @param array<string, string> $technicalDetails
* @return array{
* key:string,
* scope:array<string, int|string>,
* 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<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
* }>,
* source_refs:list<array{type:string,id:int|string}>,
* evidence_refs:list<array{type:string,id:int|string}>,
* technical_details:array<string, string>
* }
*/
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,
];
}
}

View File

@ -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<array{label:string,url:?string,kind:string,icon:string}>,
* limitations:list<array{key:string,label:string,severity:string,reason:string,action:?array{label:string,url:?string,kind:string,icon:string},details:list<string>}>,
* primary_action:array{key:string,label:string,url:?string,kind:string,icon:string}|null,
* secondary_actions:list<array{key:string,label:string,url:?string,kind:string,icon:string}>,
* limitations:list<array{key:string,label:string,severity:string,reason:string,action:?array{key:string,label:string,url:?string,kind:string,icon:string},details:list<string>}>,
* technical_details:array<string, string>
* }
*/
@ -174,7 +174,7 @@ private static function sectionStateCounts(Collection $sections): array
}
/**
* @param list<array{key:string,label:string,severity:string,reason:string,action:?array{label:string,url:?string,kind:string,icon:string},details:list<string>,priority:int}> $limitations
* @param list<array{key:string,label:string,severity:string,reason:string,action:?array{key:string,label:string,url:?string,kind:string,icon:string},details:list<string>,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<string, mixed> $readiness
* @param array{download?:?string,review?:?string,evidence?:?string,operation?:?string} $urls
* @return list<array{key:string,label:string,severity:string,reason:string,action:?array{label:string,url:?string,kind:string,icon:string},details:list<string>,priority:int}>
* @return list<array{key:string,label:string,severity:string,reason:string,action:?array{key:string,label:string,url:?string,kind:string,icon:string},details:list<string>,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<array{label:string,url:?string,kind:string,icon:string}>
* @param array{key:string,label:string,url:?string,kind:string,icon:string}|null $primaryAction
* @return list<array{key:string,label:string,url:?string,kind:string,icon:string}>
*/
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'),

View File

@ -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
<div class="space-y-4">
@ -52,11 +55,14 @@
</div>
<div class="mt-4 space-y-2">
<h3 class="text-lg font-semibold text-gray-950 dark:text-white">
{{ $resolutionCase['title'] ?? ($state['label'] ?? __('localization.review.requires_review')) }}
</h3>
<p class="text-sm text-gray-700 dark:text-gray-200">
{{ $state['primary_reason'] ?? __('localization.review.review_pack_with_limitations_description') }}
{{ $resolutionCase['reason'] ?? ($state['primary_reason'] ?? __('localization.review.review_pack_with_limitations_description')) }}
</p>
<p class="text-sm text-gray-600 dark:text-gray-300">
{{ $state['impact'] ?? __('localization.review.published_with_limitations_impact') }}
{{ $resolutionCase['impact'] ?? ($state['impact'] ?? __('localization.review.published_with_limitations_impact')) }}
</p>
@if (filled($contextNote))
<p class="text-sm text-gray-600 dark:text-gray-300">
@ -67,14 +73,14 @@
@unless ($detailMode)
<div class="mt-4 flex flex-wrap gap-2">
@if (filled(data_get($state, 'primary_action.url')))
@if (filled(data_get($resolutionCase, 'primary_action.url', data_get($state, 'primary_action.url'))))
<x-filament::button
tag="a"
:href="$state['primary_action']['url']"
:icon="$state['primary_action']['icon']"
:href="data_get($resolutionCase, 'primary_action.url', data_get($state, 'primary_action.url'))"
:icon="data_get($resolutionCase, 'primary_action.icon', data_get($state, 'primary_action.icon'))"
target="_blank"
>
{{ $state['primary_action']['label'] }}
{{ data_get($resolutionCase, 'primary_action.label', data_get($state, 'primary_action.label')) }}
</x-filament::button>
@endif

View File

@ -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
</div>
<div class="space-y-2">
<h2 class="text-xl font-semibold text-gray-950 dark:text-white">
<div class="text-[0.7rem] font-semibold uppercase leading-4 tracking-wide text-gray-500 dark:text-gray-400">
{{ $readiness['question'] }}
</div>
<h2 class="text-xl font-semibold text-gray-950 dark:text-white">
{{ $resolutionCase['title'] ?? $readiness['label'] }}
</h2>
<p class="max-w-3xl text-sm text-gray-600 dark:text-gray-300">
{{ $readiness['reason'] }}
{{ $resolutionCase['reason'] ?? $readiness['reason'] }}
</p>
</div>
@ -88,7 +92,7 @@ class="rounded-xl border border-gray-200 bg-white p-4 shadow-sm dark:border-whit
{{ __('localization.review.impact') }}
</div>
<p class="text-sm leading-6 text-gray-700 dark:text-gray-200">
{{ $readiness['impact'] }}
{{ $resolutionCase['impact'] ?? $readiness['impact'] }}
</p>
</div>
@ -106,19 +110,19 @@ class="rounded-xl border border-gray-200 bg-white p-4 shadow-sm dark:border-whit
</div>
<div class="flex flex-wrap gap-2">
@if ($readiness['primary_action_url'])
@if (filled(data_get($resolutionCase, 'primary_action.url', $readiness['primary_action_url'])))
<x-filament::button
tag="a"
:href="$readiness['primary_action_url']"
:icon="$readiness['primary_action_icon']"
:href="data_get($resolutionCase, 'primary_action.url', $readiness['primary_action_url'])"
:icon="data_get($resolutionCase, 'primary_action.icon', $readiness['primary_action_icon'])"
target="_blank"
data-testid="customer-review-primary-action"
>
{{ $readiness['primary_action_label'] }}
{{ data_get($resolutionCase, 'primary_action.label', $readiness['primary_action_label']) }}
</x-filament::button>
@endif
@foreach ($readiness['secondary_actions'] as $secondaryAction)
@foreach ((is_array($resolutionCase['secondary_actions'] ?? null) ? $resolutionCase['secondary_actions'] : $readiness['secondary_actions']) as $secondaryAction)
<x-filament::button
tag="a"
:href="$secondaryAction['url']"

View File

@ -0,0 +1,198 @@
<?php
declare(strict_types=1);
use App\Filament\Pages\Reviews\CustomerReviewWorkspace;
use App\Filament\Resources\EnvironmentReviewResource;
use App\Models\EnvironmentReview;
use App\Models\EvidenceSnapshot;
use App\Models\ManagedEnvironment;
use App\Models\ReviewPack;
use App\Models\User;
use App\Support\EnvironmentReviewCompletenessState;
use App\Support\EnvironmentReviewStatus;
use App\Support\Governance\Controls\ComplianceEvidenceMappingV1;
use App\Support\Workspaces\WorkspaceContext;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Storage;
uses(RefreshDatabase::class);
pest()->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<string, mixed> $summaryOverrides
* @param array<string, mixed> $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];
}

View File

@ -0,0 +1,119 @@
<?php
declare(strict_types=1);
use App\Filament\Pages\Reviews\CustomerReviewWorkspace;
use App\Filament\Resources\EnvironmentReviewResource;
use App\Models\ManagedEnvironment;
use App\Models\ReviewPack;
use App\Support\EnvironmentReviewStatus;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Storage;
uses(RefreshDatabase::class);
beforeEach(function (): void {
Storage::fake('exports');
});
it('adds the shared resolution case to the environment review output guidance state', function (): void {
$tenant = ManagedEnvironment::factory()->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');
});

View File

@ -0,0 +1,130 @@
<?php
declare(strict_types=1);
use App\Filament\Pages\Reviews\CustomerReviewWorkspace;
use App\Models\Finding;
use App\Models\ManagedEnvironment;
use App\Models\ReviewPack;
use App\Models\User;
use App\Support\EnvironmentReviewStatus;
use App\Support\Workspaces\WorkspaceContext;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Storage;
use Livewire\Livewire;
uses(RefreshDatabase::class);
beforeEach(function (): void {
Storage::fake('exports');
});
it('renders the shared resolution case on the customer review workspace decision card', function (): void {
$environment = ManagedEnvironment::factory()->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);
}

View File

@ -0,0 +1,94 @@
<?php
declare(strict_types=1);
use App\Support\ResolutionGuidance\ResolutionAction;
use App\Support\ResolutionGuidance\ResolutionCase;
it('normalizes navigation and download actions into the shared contract shape', function (): void {
$navigation = ResolutionAction::fromArray([
'key' => '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');
});

View File

@ -0,0 +1,109 @@
<?php
declare(strict_types=1);
use App\Models\EnvironmentReview;
use App\Models\EvidenceSnapshot;
use App\Models\ReviewPack;
use App\Support\EnvironmentReviewCompletenessState;
use App\Support\ResolutionGuidance\Adapters\ReviewPackOutputResolutionAdapter;
use App\Support\ResolutionGuidance\ResolutionAction;
use App\Support\ReviewPacks\ReviewPackOutputReadiness;
use App\Support\ReviewPacks\ReviewPackOutputResolutionGuidance;
it('maps publication-blocked review output guidance into one shared resolution case', function (): void {
$readiness = ReviewPackOutputReadiness::derive(
reviewStatus: 'published',
reviewCompletenessState: EnvironmentReviewCompletenessState::Complete->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);
});

View File

@ -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

View File

@ -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.

View File

@ -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. |

View File

@ -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. |

Binary file not shown.

After

Width:  |  Height:  |  Size: 324 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 463 KiB

View File

@ -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.

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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.