feat: operator resolution guidance framework v1 (spec 350) #421
@ -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,
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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',
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
108
apps/platform/app/Support/ResolutionGuidance/ResolutionCase.php
Normal file
108
apps/platform/app/Support/ResolutionGuidance/ResolutionCase.php
Normal 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,
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -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'),
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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']"
|
||||
|
||||
@ -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];
|
||||
}
|
||||
@ -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');
|
||||
});
|
||||
@ -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);
|
||||
}
|
||||
@ -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');
|
||||
});
|
||||
@ -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);
|
||||
});
|
||||
@ -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
|
||||
|
||||
@ -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.
|
||||
@ -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. |
|
||||
|
||||
@ -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 |
@ -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.
|
||||
@ -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.
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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.
|
||||
241
specs/350-operator-resolution-guidance-framework-v1/plan.md
Normal file
241
specs/350-operator-resolution-guidance-framework-v1/plan.md
Normal 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
|
||||
@ -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
|
||||
352
specs/350-operator-resolution-guidance-framework-v1/spec.md
Normal file
352
specs/350-operator-resolution-guidance-framework-v1/spec.md
Normal 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
|
||||
131
specs/350-operator-resolution-guidance-framework-v1/tasks.md
Normal file
131
specs/350-operator-resolution-guidance-framework-v1/tasks.md
Normal 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.
|
||||
Loading…
Reference in New Issue
Block a user