feat: customer review workspace output resolution guidance (spec 349) (#420)
Implemented the output resolution guidance for the customer review workspace and internal views. Added ReviewPackOutputResolutionGuidance, updated CustomerReviewWorkspace and EnvironmentReviewResource, and added related blade views and tests. Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #420
This commit is contained in:
parent
12ea7f9924
commit
9b46c0e435
@ -34,6 +34,7 @@
|
||||
use App\Support\OperationRunLinks;
|
||||
use App\Support\ReviewPackStatus;
|
||||
use App\Support\ReviewPacks\ReviewPackOutputReadiness;
|
||||
use App\Support\ReviewPacks\ReviewPackOutputResolutionGuidance;
|
||||
use App\Support\Ui\ActionSurface\ActionSurfaceDeclaration;
|
||||
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceInspectAffordance;
|
||||
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceProfile;
|
||||
@ -373,6 +374,12 @@ public function latestReviewConsumptionPayload(): ?array
|
||||
$reviewUrl = $this->latestReviewUrl($tenant);
|
||||
$evidenceUrl = $this->evidenceSnapshotUrlForReview($review, $tenant);
|
||||
$outputReadiness = $this->reviewPackOutputReadinessForReview($review);
|
||||
$outputGuidance = $this->reviewOutputGuidanceForReview(
|
||||
review: $review,
|
||||
downloadUrl: $downloadUrl,
|
||||
reviewUrl: $reviewUrl,
|
||||
evidenceUrl: $evidenceUrl,
|
||||
);
|
||||
$decision = $this->decisionSummaryForReview($review);
|
||||
$acceptedRisks = $this->acceptedRisksForReview($review);
|
||||
$hasAcceptedRiskFollowUp = $this->acceptedRiskFollowUpRequiredForReview($review);
|
||||
@ -383,6 +390,7 @@ public function latestReviewConsumptionPayload(): ?array
|
||||
review: $review,
|
||||
packageAvailability: $packageAvailability,
|
||||
outputReadiness: $outputReadiness,
|
||||
outputGuidance: $outputGuidance,
|
||||
downloadUrl: $downloadUrl,
|
||||
reviewUrl: $reviewUrl,
|
||||
evidenceUrl: $evidenceUrl,
|
||||
@ -405,7 +413,7 @@ public function latestReviewConsumptionPayload(): ?array
|
||||
'package_color' => $this->governancePackageAvailabilityColor($tenant),
|
||||
'package_description' => $packageAvailability['description'],
|
||||
'primary_action_label' => $downloadUrl !== null
|
||||
? __('localization.review.download_review_pack')
|
||||
? $outputGuidance['qualified_download_label']
|
||||
: __('localization.review.open_latest_review'),
|
||||
'primary_action_url' => $downloadUrl ?? $reviewUrl,
|
||||
'primary_action_icon' => $downloadUrl !== null
|
||||
@ -622,6 +630,7 @@ private function reviewScopePayload(ManagedEnvironment $tenant): array
|
||||
/**
|
||||
* @param array{state:string,label:string,description:string} $packageAvailability
|
||||
* @param array<string, mixed> $outputReadiness
|
||||
* @param array<string, mixed> $outputGuidance
|
||||
* @return array{
|
||||
* question:string,
|
||||
* label:string,
|
||||
@ -634,7 +643,9 @@ private function reviewScopePayload(ManagedEnvironment $tenant): array
|
||||
* primary_action_url:?string,
|
||||
* primary_action_icon:string,
|
||||
* secondary_action_label:?string,
|
||||
* secondary_action_url:?string
|
||||
* secondary_action_url:?string,
|
||||
* secondary_actions:list<array{label:string,url:?string,kind:string,icon:string}>,
|
||||
* output_guidance:array<string, mixed>
|
||||
* }
|
||||
*/
|
||||
private function reviewReadinessForTenant(
|
||||
@ -642,6 +653,7 @@ private function reviewReadinessForTenant(
|
||||
EnvironmentReview $review,
|
||||
array $packageAvailability,
|
||||
array $outputReadiness,
|
||||
array $outputGuidance,
|
||||
?string $downloadUrl,
|
||||
?string $reviewUrl,
|
||||
?string $evidenceUrl,
|
||||
@ -664,28 +676,62 @@ private function reviewReadinessForTenant(
|
||||
reviewUrl: $reviewUrl,
|
||||
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);
|
||||
|
||||
return [
|
||||
'question' => __('localization.review.review_pack_output_status'),
|
||||
'label' => $this->workspaceReadinessLabel($effectiveState),
|
||||
'color' => $this->workspaceReadinessColor($effectiveState),
|
||||
'boundary_label' => $this->workspaceBoundaryLabel((string) ($outputReadiness['customer_safe_state'] ?? 'requires_review')),
|
||||
'boundary_color' => $this->workspaceBoundaryColor((string) ($outputReadiness['customer_safe_state'] ?? 'requires_review')),
|
||||
'reason' => $this->workspaceReadinessReason(
|
||||
reasonCode: $reasonCode,
|
||||
outputReadiness: $outputReadiness,
|
||||
findingPanel: $findingPanel,
|
||||
packageAvailability: $packageAvailability,
|
||||
),
|
||||
'impact' => $this->workspaceReadinessImpact(
|
||||
state: $effectiveState,
|
||||
reasonCode: $reasonCode,
|
||||
),
|
||||
'primary_action_label' => $actions['primary_label'],
|
||||
'primary_action_url' => $actions['primary_url'],
|
||||
'primary_action_icon' => $actions['primary_icon'],
|
||||
'secondary_action_label' => $actions['secondary_label'],
|
||||
'secondary_action_url' => $actions['secondary_url'],
|
||||
'label' => $followUpOverride
|
||||
? $this->workspaceReadinessLabel($effectiveState)
|
||||
: (string) ($outputGuidance['label'] ?? $this->workspaceReadinessLabel($effectiveState)),
|
||||
'color' => $followUpOverride
|
||||
? $this->workspaceReadinessColor($effectiveState)
|
||||
: (string) ($outputGuidance['color'] ?? $this->workspaceReadinessColor($effectiveState)),
|
||||
'boundary_label' => $followUpOverride
|
||||
? $this->workspaceBoundaryLabel((string) ($outputReadiness['customer_safe_state'] ?? 'requires_review'))
|
||||
: (string) ($outputGuidance['boundary_label'] ?? $this->workspaceBoundaryLabel((string) ($outputReadiness['customer_safe_state'] ?? 'requires_review'))),
|
||||
'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)),
|
||||
'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,
|
||||
'output_guidance' => $outputGuidance,
|
||||
];
|
||||
}
|
||||
|
||||
@ -2248,29 +2294,33 @@ private function evidenceSnapshotUrlForReview(EnvironmentReview $review, Managed
|
||||
*/
|
||||
private function reviewPackOutputReadinessForReview(EnvironmentReview $review): array
|
||||
{
|
||||
$review->loadMissing(['sections', 'evidenceSnapshot', 'currentExportReviewPack']);
|
||||
return ReviewPackOutputResolutionGuidance::readinessForReview($review);
|
||||
}
|
||||
|
||||
$pack = $review->currentExportReviewPack;
|
||||
$snapshot = $review->evidenceSnapshot;
|
||||
$summary = is_array($review->summary) ? $review->summary : [];
|
||||
$sections = $this->reviewPackOutputSections($review, $pack);
|
||||
$sectionStateCounts = $this->reviewPackSectionStateCounts($sections);
|
||||
$requiredSections = $sections->filter(static fn (mixed $section): bool => (bool) $section->required)->values();
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function reviewOutputGuidanceForReview(
|
||||
EnvironmentReview $review,
|
||||
?string $downloadUrl,
|
||||
?string $reviewUrl,
|
||||
?string $evidenceUrl,
|
||||
): array {
|
||||
$operationUrl = null;
|
||||
$operationRun = $review->currentExportReviewPack?->operationRun ?? $review->operationRun;
|
||||
|
||||
return ReviewPackOutputReadiness::derive(
|
||||
reviewStatus: (string) $review->status,
|
||||
reviewCompletenessState: (string) $review->completeness_state,
|
||||
evidenceCompletenessState: $snapshot instanceof EvidenceSnapshot
|
||||
? (string) $snapshot->completeness_state
|
||||
: EnvironmentReviewCompletenessState::Missing->value,
|
||||
sectionStateCounts: $sectionStateCounts,
|
||||
requiredSectionCount: $requiredSections->count(),
|
||||
requiredSectionStateCounts: $this->reviewPackSectionStateCounts($requiredSections),
|
||||
publishBlockers: is_array($summary['publish_blockers'] ?? null) ? $summary['publish_blockers'] : [],
|
||||
hasReadyExport: $this->reviewPackHasReadyExport($pack),
|
||||
includePii: (bool) (is_array($pack?->options ?? null) ? ($pack->options['include_pii'] ?? true) : true),
|
||||
protectedValuesHidden: true,
|
||||
disclosurePresent: $this->reviewPackDisclosurePresent($review),
|
||||
if ($operationRun instanceof OperationRun) {
|
||||
$operationUrl = OperationRunLinks::tenantlessView((int) $operationRun->getKey());
|
||||
}
|
||||
|
||||
return ReviewPackOutputResolutionGuidance::fromReadiness(
|
||||
$this->reviewPackOutputReadinessForReview($review),
|
||||
[
|
||||
'download' => $downloadUrl,
|
||||
'review' => $reviewUrl,
|
||||
'evidence' => $evidenceUrl,
|
||||
'operation' => $operationUrl,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@ -2291,34 +2341,6 @@ private function reviewPackHasReadyExport(?ReviewPack $pack): bool
|
||||
return filled($pack->file_path) && filled($pack->file_disk);
|
||||
}
|
||||
|
||||
private function reviewPackIncludesOperations(?ReviewPack $pack): bool
|
||||
{
|
||||
return (bool) (is_array($pack?->options ?? null) ? ($pack->options['include_operations'] ?? true) : true);
|
||||
}
|
||||
|
||||
private function reviewPackOutputSections(EnvironmentReview $review, ?ReviewPack $pack): Collection
|
||||
{
|
||||
return $review->sections
|
||||
->filter(fn (mixed $section): bool => $this->reviewPackIncludesOperations($pack) || $section->section_key !== 'operations_health')
|
||||
->values();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, int>
|
||||
*/
|
||||
private function reviewPackSectionStateCounts(Collection $sections): array
|
||||
{
|
||||
return $sections
|
||||
->countBy(static fn (mixed $section): string => (string) $section->completeness_state)
|
||||
->map(static fn (int $count): int => max(0, $count))
|
||||
->all();
|
||||
}
|
||||
|
||||
private function reviewPackDisclosurePresent(EnvironmentReview $review): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $outputReadiness
|
||||
*/
|
||||
|
||||
@ -33,6 +33,7 @@
|
||||
use App\Support\Rbac\UiEnforcement;
|
||||
use App\Support\ReasonTranslation\ReasonPresenter;
|
||||
use App\Support\ReviewPackStatus;
|
||||
use App\Support\ReviewPacks\ReviewPackOutputResolutionGuidance;
|
||||
use App\Support\Ui\ActionSurface\ActionSurfaceDeclaration;
|
||||
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceInspectAffordance;
|
||||
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceProfile;
|
||||
@ -200,6 +201,7 @@ public static function infolist(Schema $schema): Schema
|
||||
Section::make(__('localization.review.review'))
|
||||
->schema([
|
||||
TextEntry::make('status')
|
||||
->label(__('localization.review.review_status'))
|
||||
->badge()
|
||||
->formatStateUsing(BadgeRenderer::label(BadgeDomain::EnvironmentReviewStatus))
|
||||
->color(BadgeRenderer::color(BadgeDomain::EnvironmentReviewStatus))
|
||||
@ -237,6 +239,15 @@ public static function infolist(Schema $schema): Schema
|
||||
])
|
||||
->columns(2)
|
||||
->columnSpanFull(),
|
||||
Section::make(__('localization.review.output_guidance'))
|
||||
->schema([
|
||||
ViewEntry::make('output_guidance')
|
||||
->hiddenLabel()
|
||||
->view('filament.infolists.entries.review-pack-output-guidance')
|
||||
->state(fn (EnvironmentReview $record): array => static::outputGuidanceState($record))
|
||||
->columnSpanFull(),
|
||||
])
|
||||
->columnSpanFull(),
|
||||
Section::make(__('localization.review.executive_posture'))
|
||||
->schema([
|
||||
ViewEntry::make('review_summary')
|
||||
@ -1032,4 +1043,87 @@ private static function appendQuery(string $url, array $query): string
|
||||
|
||||
return $url.(str_contains($url, '?') ? '&' : '?').http_build_query($query);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function outputGuidanceState(EnvironmentReview $record): array
|
||||
{
|
||||
$tenant = $record->tenant;
|
||||
$user = auth()->user();
|
||||
$reviewUrl = $tenant instanceof ManagedEnvironment
|
||||
? static::environmentScopedUrl('view', ['record' => $record], $tenant)
|
||||
: null;
|
||||
$evidenceUrl = null;
|
||||
$operationUrl = null;
|
||||
|
||||
if (static::isCustomerWorkspaceMode() && $reviewUrl !== null) {
|
||||
$reviewUrl = static::appendQuery($reviewUrl, [
|
||||
CustomerReviewWorkspace::DETAIL_CONTEXT_QUERY_KEY => 1,
|
||||
]);
|
||||
}
|
||||
|
||||
if ($record->evidenceSnapshot instanceof EvidenceSnapshot && $tenant instanceof ManagedEnvironment && $user instanceof User && $user->can(Capabilities::EVIDENCE_VIEW, $tenant)) {
|
||||
$evidenceUrl = EvidenceSnapshotResource::getUrl('view', ['record' => $record->evidenceSnapshot], tenant: $tenant);
|
||||
|
||||
if (static::isCustomerWorkspaceMode()) {
|
||||
$evidenceUrl = static::appendQuery($evidenceUrl, static::customerWorkspaceEvidenceQuery($record));
|
||||
}
|
||||
}
|
||||
|
||||
$operationRun = $record->currentExportReviewPack?->operationRun ?? $record->operationRun;
|
||||
|
||||
if ($operationRun instanceof \App\Models\OperationRun) {
|
||||
$operationUrl = OperationRunLinks::tenantlessView((int) $operationRun->getKey());
|
||||
}
|
||||
|
||||
$guidance = ReviewPackOutputResolutionGuidance::fromReview($record, [
|
||||
'download' => static::currentReviewPackDownloadUrlFor($record),
|
||||
'review' => $reviewUrl,
|
||||
'evidence' => $evidenceUrl,
|
||||
'operation' => $operationUrl,
|
||||
]);
|
||||
|
||||
if (! static::isCustomerWorkspaceMode()) {
|
||||
return $guidance;
|
||||
}
|
||||
|
||||
$guidance['detail_mode'] = true;
|
||||
$guidance['primary_action'] = null;
|
||||
$guidance['secondary_actions'] = [];
|
||||
$guidance['next_step_label'] = __('localization.review.review_limitations_below');
|
||||
$guidance['context_note'] = __('localization.review.output_guidance_detail_mode_note');
|
||||
|
||||
return $guidance;
|
||||
}
|
||||
|
||||
public static function currentReviewPackDownloadUrlFor(EnvironmentReview $record): ?string
|
||||
{
|
||||
$pack = $record->currentExportReviewPack;
|
||||
$tenant = $record->tenant;
|
||||
$user = auth()->user();
|
||||
|
||||
if (! $pack instanceof ReviewPack || ! $tenant instanceof ManagedEnvironment || ! $user instanceof User) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (! $user->can(Capabilities::REVIEW_PACK_VIEW, $tenant)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($pack->status !== ReviewPackStatus::Ready->value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($pack->expires_at !== null && $pack->expires_at->isPast()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return app(ReviewPackService::class)->generateDownloadUrl($pack, [
|
||||
'source_surface' => CustomerReviewWorkspace::SOURCE_SURFACE,
|
||||
'review_id' => (int) $record->getKey(),
|
||||
'tenant_filter_id' => request()->query('tenant_filter_id'),
|
||||
'interpretation_version' => $record->controlInterpretationVersion(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -355,7 +355,11 @@ private function archiveReviewAction(): Actions\Action
|
||||
private function downloadCurrentReviewPackAction(): Actions\Action
|
||||
{
|
||||
return Actions\Action::make('download_current_review_pack')
|
||||
->label(__('localization.review.download_governance_package'))
|
||||
->label(function (): string {
|
||||
$guidance = EnvironmentReviewResource::outputGuidanceState($this->record);
|
||||
|
||||
return (string) ($guidance['qualified_download_label'] ?? __('localization.review.download_governance_package'));
|
||||
})
|
||||
->icon('heroicon-o-arrow-down-tray')
|
||||
->color('primary')
|
||||
->disabled(fn (): bool => $this->currentReviewPackDownloadUrl() === null)
|
||||
@ -366,32 +370,7 @@ private function downloadCurrentReviewPackAction(): Actions\Action
|
||||
|
||||
private function currentReviewPackDownloadUrl(): ?string
|
||||
{
|
||||
$pack = $this->record->currentExportReviewPack;
|
||||
$tenant = $this->record->tenant;
|
||||
$user = auth()->user();
|
||||
|
||||
if (! $pack instanceof ReviewPack || ! $tenant instanceof ManagedEnvironment || ! $user instanceof User) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (! $user->can(Capabilities::REVIEW_PACK_VIEW, $tenant)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($pack->status !== ReviewPackStatus::Ready->value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($pack->expires_at !== null && $pack->expires_at->isPast()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return app(ReviewPackService::class)->generateDownloadUrl($pack, [
|
||||
'source_surface' => CustomerReviewWorkspace::SOURCE_SURFACE,
|
||||
'review_id' => (int) $this->record->getKey(),
|
||||
'tenant_filter_id' => request()->query('tenant_filter_id'),
|
||||
'interpretation_version' => $this->record->controlInterpretationVersion(),
|
||||
]);
|
||||
return EnvironmentReviewResource::currentReviewPackDownloadUrlFor($this->record);
|
||||
}
|
||||
|
||||
private function currentReviewPackUnavailableReason(): ?string
|
||||
|
||||
@ -0,0 +1,576 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Support\ReviewPacks;
|
||||
|
||||
use App\Models\EnvironmentReview;
|
||||
use App\Models\EvidenceSnapshot;
|
||||
use App\Models\ReviewPack;
|
||||
use App\Support\EnvironmentReviewCompletenessState;
|
||||
use App\Support\ReviewPackStatus;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
final class ReviewPackOutputResolutionGuidance
|
||||
{
|
||||
public const string STATE_CUSTOMER_SAFE_READY = 'customer_safe_ready';
|
||||
|
||||
public const string STATE_PUBLISHED_WITH_LIMITATIONS = 'published_with_limitations';
|
||||
|
||||
public const string STATE_PUBLICATION_BLOCKED = 'publication_blocked';
|
||||
|
||||
public const string STATE_INTERNAL_ONLY = 'internal_only';
|
||||
|
||||
public const string STATE_EXPORT_NOT_READY = 'export_not_ready';
|
||||
|
||||
public const string STATE_UNKNOWN = 'unknown';
|
||||
|
||||
/**
|
||||
* @param array{download?:?string,review?:?string,evidence?:?string,operation?:?string} $urls
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function fromReview(EnvironmentReview $review, array $urls = []): array
|
||||
{
|
||||
return self::fromReadiness(self::readinessForReview($review), $urls);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function readinessForReview(EnvironmentReview $review): array
|
||||
{
|
||||
$review->loadMissing(['sections', 'evidenceSnapshot', 'currentExportReviewPack']);
|
||||
|
||||
$pack = $review->currentExportReviewPack;
|
||||
$snapshot = $review->evidenceSnapshot;
|
||||
$summary = is_array($review->summary) ? $review->summary : [];
|
||||
$sections = self::outputSections($review, $pack);
|
||||
$requiredSections = $sections
|
||||
->filter(static fn (mixed $section): bool => (bool) $section->required)
|
||||
->values();
|
||||
|
||||
return ReviewPackOutputReadiness::derive(
|
||||
reviewStatus: (string) $review->status,
|
||||
reviewCompletenessState: (string) $review->completeness_state,
|
||||
evidenceCompletenessState: $snapshot instanceof EvidenceSnapshot
|
||||
? (string) $snapshot->completeness_state
|
||||
: EnvironmentReviewCompletenessState::Missing->value,
|
||||
sectionStateCounts: self::sectionStateCounts($sections),
|
||||
requiredSectionCount: $requiredSections->count(),
|
||||
requiredSectionStateCounts: self::sectionStateCounts($requiredSections),
|
||||
publishBlockers: is_array($summary['publish_blockers'] ?? null) ? $summary['publish_blockers'] : [],
|
||||
hasReadyExport: self::hasReadyExport($pack),
|
||||
includePii: (bool) (is_array($pack?->options ?? null) ? ($pack->options['include_pii'] ?? true) : true),
|
||||
protectedValuesHidden: true,
|
||||
disclosurePresent: true,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $readiness
|
||||
* @param array{download?:?string,review?:?string,evidence?:?string,operation?:?string} $urls
|
||||
* @return array{
|
||||
* state:string,
|
||||
* label:string,
|
||||
* status_label:string,
|
||||
* color:string,
|
||||
* severity:string,
|
||||
* boundary_state:string,
|
||||
* boundary_label:string,
|
||||
* boundary_color:string,
|
||||
* primary_reason:string,
|
||||
* impact:string,
|
||||
* qualified_download_label:string,
|
||||
* 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>}>,
|
||||
* technical_details:array<string, string>
|
||||
* }
|
||||
*/
|
||||
public static function fromReadiness(array $readiness, array $urls = []): array
|
||||
{
|
||||
$limitations = self::limitations($readiness, $urls);
|
||||
$state = self::state($readiness, $limitations);
|
||||
$primaryLimitation = $limitations[0] ?? null;
|
||||
$primaryAction = self::primaryAction(
|
||||
state: $state,
|
||||
primaryLimitationKey: is_array($primaryLimitation) ? (string) $primaryLimitation['key'] : null,
|
||||
urls: $urls,
|
||||
);
|
||||
$secondaryActions = self::secondaryActions($state, $primaryAction, $urls);
|
||||
$boundaryState = self::boundaryState($state, $readiness);
|
||||
|
||||
return [
|
||||
'state' => $state,
|
||||
'label' => self::label($state),
|
||||
'status_label' => self::statusLabel($state),
|
||||
'color' => self::color($state),
|
||||
'severity' => self::severity($state),
|
||||
'boundary_state' => $boundaryState,
|
||||
'boundary_label' => self::boundaryLabel($boundaryState),
|
||||
'boundary_color' => self::boundaryColor($boundaryState),
|
||||
'primary_reason' => is_array($primaryLimitation)
|
||||
? self::shortReason((string) $primaryLimitation['key'])
|
||||
: self::defaultPrimaryReason($state),
|
||||
'impact' => self::impact($state),
|
||||
'qualified_download_label' => self::qualifiedDownloadLabel($state),
|
||||
'limitation_count' => count($limitations),
|
||||
'limitation_summary' => $limitations === []
|
||||
? null
|
||||
: trans_choice('localization.review.output_limitations_summary', count($limitations), ['count' => count($limitations)]),
|
||||
'action_help' => self::actionHelp($state),
|
||||
'primary_action' => $primaryAction,
|
||||
'secondary_actions' => $secondaryActions,
|
||||
'limitations' => $limitations,
|
||||
'technical_details' => self::technicalDetails($readiness),
|
||||
];
|
||||
}
|
||||
|
||||
private static function hasReadyExport(?ReviewPack $pack): bool
|
||||
{
|
||||
if (! $pack instanceof ReviewPack) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($pack->status !== ReviewPackStatus::Ready->value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($pack->expires_at !== null && $pack->expires_at->isPast()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return filled($pack->file_path) && filled($pack->file_disk);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, mixed>
|
||||
*/
|
||||
private static function outputSections(EnvironmentReview $review, ?ReviewPack $pack): Collection
|
||||
{
|
||||
return $review->sections
|
||||
->filter(static fn (mixed $section): bool => self::includesOperations($pack) || $section->section_key !== 'operations_health')
|
||||
->values();
|
||||
}
|
||||
|
||||
private static function includesOperations(?ReviewPack $pack): bool
|
||||
{
|
||||
return (bool) (is_array($pack?->options ?? null) ? ($pack->options['include_operations'] ?? true) : true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Collection<int, mixed> $sections
|
||||
* @return array<string, int>
|
||||
*/
|
||||
private static function sectionStateCounts(Collection $sections): array
|
||||
{
|
||||
return $sections
|
||||
->countBy(static fn (mixed $section): string => (string) $section->completeness_state)
|
||||
->map(static fn (int $count): int => max(0, $count))
|
||||
->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
private static function state(array $readiness, array $limitations): string
|
||||
{
|
||||
$limitationKeys = collect($limitations)->pluck('key');
|
||||
|
||||
return match (true) {
|
||||
$limitationKeys->contains('publish_blockers_present') => self::STATE_PUBLICATION_BLOCKED,
|
||||
! (bool) ($readiness['has_ready_export'] ?? false) => self::STATE_EXPORT_NOT_READY,
|
||||
(bool) ($readiness['contains_pii'] ?? false) => self::STATE_INTERNAL_ONLY,
|
||||
$limitations !== [] => self::STATE_PUBLISHED_WITH_LIMITATIONS,
|
||||
(string) ($readiness['readiness_state'] ?? '') === ReviewPackOutputReadiness::STATE_CUSTOMER_SAFE_READY => self::STATE_CUSTOMER_SAFE_READY,
|
||||
default => self::STATE_UNKNOWN,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @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}>
|
||||
*/
|
||||
private static function limitations(array $readiness, array $urls): array
|
||||
{
|
||||
$sectionSummary = is_array($readiness['section_summary'] ?? null) ? $readiness['section_summary'] : [];
|
||||
$sectionCounts = is_array($readiness['section_state_counts'] ?? null) ? $readiness['section_state_counts'] : [];
|
||||
$limitations = collect(is_array($readiness['limitations'] ?? null) ? $readiness['limitations'] : [])
|
||||
->filter(static fn (mixed $limitation): bool => is_array($limitation) && is_string($limitation['code'] ?? null))
|
||||
->map(function (array $limitation) use ($readiness, $sectionSummary, $sectionCounts, $urls): ?array {
|
||||
$code = (string) $limitation['code'];
|
||||
|
||||
return match ($code) {
|
||||
'publish_blockers_present' => [
|
||||
'key' => $code,
|
||||
'label' => __('localization.review.publication_blocked'),
|
||||
'severity' => 'danger',
|
||||
'reason' => __('localization.review.publication_blocked_description'),
|
||||
'action' => self::action('resolve_review_blockers', $urls['review'] ?? $urls['evidence'] ?? null),
|
||||
'details' => [
|
||||
__('localization.review.technical_detail_review_status_value', ['value' => (string) ($readiness['review_status'] ?? __('localization.review.unavailable'))]),
|
||||
],
|
||||
'priority' => 100,
|
||||
],
|
||||
'export_not_ready' => [
|
||||
'key' => $code,
|
||||
'label' => __('localization.review.export_not_ready'),
|
||||
'severity' => 'warning',
|
||||
'reason' => __('localization.review.export_not_ready_guidance_reason'),
|
||||
'action' => self::action('review_output_limitations', $urls['review'] ?? $urls['evidence'] ?? null),
|
||||
'details' => [
|
||||
__('localization.review.technical_detail_ready_export_value', ['value' => __('localization.review.no')]),
|
||||
],
|
||||
'priority' => 90,
|
||||
],
|
||||
'evidence_basis_missing', 'evidence_basis_stale', 'evidence_basis_incomplete' => [
|
||||
'key' => $code,
|
||||
'label' => __('localization.review.evidence_basis_incomplete_guidance'),
|
||||
'severity' => 'warning',
|
||||
'reason' => __('localization.review.evidence_basis_incomplete_guidance_reason'),
|
||||
'action' => self::action('open_evidence_basis', $urls['evidence'] ?? $urls['review'] ?? null),
|
||||
'details' => [
|
||||
__('localization.review.technical_detail_evidence_state_value', ['value' => (string) ($readiness['evidence_completeness_state'] ?? __('localization.review.unavailable'))]),
|
||||
],
|
||||
'priority' => match ($code) {
|
||||
'evidence_basis_missing' => 82,
|
||||
'evidence_basis_stale' => 81,
|
||||
default => 80,
|
||||
},
|
||||
],
|
||||
'required_sections_incomplete' => [
|
||||
'key' => $code,
|
||||
'label' => __('localization.review.required_review_sections_missing'),
|
||||
'severity' => 'warning',
|
||||
'reason' => trans_choice(
|
||||
'localization.review.required_review_sections_missing_reason',
|
||||
(int) ($sectionSummary['required_limited'] ?? 0),
|
||||
['count' => (int) ($sectionSummary['required_limited'] ?? 0)],
|
||||
),
|
||||
'action' => self::action('review_section_limitations', $urls['review'] ?? $urls['evidence'] ?? null),
|
||||
'details' => [
|
||||
__('localization.review.technical_detail_section_counts_value', [
|
||||
'complete' => (int) ($sectionCounts[EnvironmentReviewCompletenessState::Complete->value] ?? 0),
|
||||
'partial' => (int) ($sectionCounts[EnvironmentReviewCompletenessState::Partial->value] ?? 0),
|
||||
'missing' => (int) ($sectionCounts[EnvironmentReviewCompletenessState::Missing->value] ?? 0),
|
||||
'stale' => (int) ($sectionCounts[EnvironmentReviewCompletenessState::Stale->value] ?? 0),
|
||||
]),
|
||||
],
|
||||
'priority' => 70,
|
||||
],
|
||||
'contains_pii' => [
|
||||
'key' => $code,
|
||||
'label' => __('localization.review.internal_package_includes_pii'),
|
||||
'severity' => 'warning',
|
||||
'reason' => __('localization.review.internal_package_includes_pii_reason'),
|
||||
'action' => self::action('review_pii_redaction_state', $urls['review'] ?? $urls['download'] ?? null),
|
||||
'details' => [
|
||||
__('localization.review.technical_detail_contains_pii_value', ['value' => __('localization.review.yes')]),
|
||||
],
|
||||
'priority' => 60,
|
||||
],
|
||||
'disclosure_missing' => [
|
||||
'key' => $code,
|
||||
'label' => __('localization.review.output_disclosure_missing'),
|
||||
'severity' => 'warning',
|
||||
'reason' => __('localization.review.output_disclosure_missing_reason'),
|
||||
'action' => self::action('review_output_limitations', $urls['review'] ?? $urls['download'] ?? null),
|
||||
'details' => [
|
||||
__('localization.review.technical_detail_disclosure_present_value', ['value' => __('localization.review.no')]),
|
||||
],
|
||||
'priority' => 50,
|
||||
],
|
||||
default => null,
|
||||
};
|
||||
})
|
||||
->filter()
|
||||
->sortByDesc('priority')
|
||||
->values()
|
||||
->all();
|
||||
|
||||
return $limitations;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{download?:?string,review?:?string,evidence?:?string,operation?:?string} $urls
|
||||
* @return array{label:string,url:?string,kind:string,icon:string}|null
|
||||
*/
|
||||
private static function primaryAction(string $state, ?string $primaryLimitationKey, array $urls): ?array
|
||||
{
|
||||
$actionKey = match ($primaryLimitationKey) {
|
||||
'publish_blockers_present' => 'resolve_review_blockers',
|
||||
'evidence_basis_missing', 'evidence_basis_stale', 'evidence_basis_incomplete' => 'open_evidence_basis',
|
||||
'required_sections_incomplete' => 'review_section_limitations',
|
||||
'contains_pii' => 'review_pii_redaction_state',
|
||||
'disclosure_missing' => 'review_output_limitations',
|
||||
'export_not_ready' => 'review_output_limitations',
|
||||
default => match ($state) {
|
||||
self::STATE_CUSTOMER_SAFE_READY => 'download_customer_safe_review_pack',
|
||||
self::STATE_INTERNAL_ONLY => 'review_pii_redaction_state',
|
||||
self::STATE_EXPORT_NOT_READY => 'review_output_limitations',
|
||||
self::STATE_PUBLICATION_BLOCKED => 'resolve_review_blockers',
|
||||
self::STATE_PUBLISHED_WITH_LIMITATIONS => 'review_output_limitations',
|
||||
default => 'review_output_limitations',
|
||||
},
|
||||
};
|
||||
|
||||
return self::action($actionKey, self::primaryActionUrl($actionKey, $urls));
|
||||
}
|
||||
|
||||
/**
|
||||
* @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}>
|
||||
*/
|
||||
private static function secondaryActions(string $state, ?array $primaryAction, array $urls): array
|
||||
{
|
||||
$actions = match ($state) {
|
||||
self::STATE_CUSTOMER_SAFE_READY => [
|
||||
self::action('open_review', $urls['review'] ?? null),
|
||||
],
|
||||
self::STATE_INTERNAL_ONLY => [
|
||||
self::action('download_internal_review_pack', $urls['download'] ?? null),
|
||||
self::action('open_review', $urls['review'] ?? null),
|
||||
],
|
||||
self::STATE_EXPORT_NOT_READY => [
|
||||
self::action('open_evidence_basis', $urls['evidence'] ?? null),
|
||||
self::action('open_review', $urls['review'] ?? null),
|
||||
],
|
||||
self::STATE_PUBLICATION_BLOCKED => [
|
||||
self::action('download_review_pack_with_limitations', $urls['download'] ?? null),
|
||||
self::action('open_evidence_basis', $urls['evidence'] ?? null),
|
||||
self::action('open_operation_proof', $urls['operation'] ?? null),
|
||||
],
|
||||
self::STATE_PUBLISHED_WITH_LIMITATIONS => [
|
||||
self::action('download_review_pack_with_limitations', $urls['download'] ?? null),
|
||||
self::action('open_review', $urls['review'] ?? null),
|
||||
self::action('open_evidence_basis', $urls['evidence'] ?? null),
|
||||
],
|
||||
default => [
|
||||
self::action('open_review', $urls['review'] ?? null),
|
||||
],
|
||||
};
|
||||
|
||||
return collect($actions)
|
||||
->filter(static fn (?array $action): bool => is_array($action) && filled($action['url']))
|
||||
->reject(static fn (array $action): bool => $primaryAction !== null
|
||||
&& $action['label'] === $primaryAction['label']
|
||||
&& $action['url'] === $primaryAction['url'])
|
||||
->unique(static fn (array $action): string => $action['label'].'|'.$action['url'])
|
||||
->values()
|
||||
->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{download?:?string,review?:?string,evidence?:?string,operation?:?string} $urls
|
||||
*/
|
||||
private static function primaryActionUrl(string $actionKey, array $urls): ?string
|
||||
{
|
||||
return match ($actionKey) {
|
||||
'download_customer_safe_review_pack', 'download_internal_review_pack', 'download_review_pack_with_limitations' => $urls['download'] ?? null,
|
||||
'open_evidence_basis' => $urls['evidence'] ?? $urls['review'] ?? null,
|
||||
'review_section_limitations', 'resolve_review_blockers', 'review_output_limitations', 'review_pii_redaction_state', 'open_review' => $urls['review'] ?? $urls['evidence'] ?? $urls['download'] ?? null,
|
||||
'open_operation_proof' => $urls['operation'] ?? null,
|
||||
default => $urls['review'] ?? $urls['download'] ?? $urls['evidence'] ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{label:string,url:?string,kind:string,icon:string}|null
|
||||
*/
|
||||
private static function action(string $actionKey, ?string $url): ?array
|
||||
{
|
||||
return [
|
||||
'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'),
|
||||
'download_review_pack_with_limitations' => __('localization.review.download_review_pack_with_limitations'),
|
||||
'open_evidence_basis' => __('localization.review.open_evidence_basis'),
|
||||
'review_section_limitations' => __('localization.review.review_section_limitations'),
|
||||
'review_pii_redaction_state' => __('localization.review.review_pii_redaction_state'),
|
||||
'resolve_review_blockers' => __('localization.review.resolve_review_blockers'),
|
||||
'open_operation_proof' => __('localization.review.open_operation_proof'),
|
||||
'open_review' => __('localization.review.open_review'),
|
||||
default => __('localization.review.review_output_limitations'),
|
||||
},
|
||||
'url' => $url,
|
||||
'kind' => str_starts_with($actionKey, 'download_') ? 'download' : 'environment_link',
|
||||
'icon' => str_starts_with($actionKey, 'download_')
|
||||
? 'heroicon-o-arrow-down-tray'
|
||||
: 'heroicon-o-arrow-top-right-on-square',
|
||||
];
|
||||
}
|
||||
|
||||
private static function label(string $state): string
|
||||
{
|
||||
return match ($state) {
|
||||
self::STATE_PUBLICATION_BLOCKED => __('localization.review.output_not_customer_ready'),
|
||||
self::STATE_CUSTOMER_SAFE_READY => __('localization.review.customer_safe_review_pack_ready'),
|
||||
self::STATE_INTERNAL_ONLY => __('localization.review.internal_review_package_available'),
|
||||
self::STATE_EXPORT_NOT_READY => __('localization.review.export_not_ready'),
|
||||
self::STATE_PUBLISHED_WITH_LIMITATIONS => __('localization.review.published_with_limitations'),
|
||||
default => __('localization.review.requires_review'),
|
||||
};
|
||||
}
|
||||
|
||||
private static function statusLabel(string $state): string
|
||||
{
|
||||
return match ($state) {
|
||||
self::STATE_PUBLICATION_BLOCKED => __('localization.review.publication_blocked'),
|
||||
self::STATE_CUSTOMER_SAFE_READY => __('localization.review.customer_safe_review_pack_ready'),
|
||||
self::STATE_INTERNAL_ONLY => __('localization.review.internal_review_package_available'),
|
||||
self::STATE_EXPORT_NOT_READY => __('localization.review.export_not_ready'),
|
||||
self::STATE_PUBLISHED_WITH_LIMITATIONS => __('localization.review.published_with_limitations'),
|
||||
default => __('localization.review.requires_review'),
|
||||
};
|
||||
}
|
||||
|
||||
private static function color(string $state): string
|
||||
{
|
||||
return match ($state) {
|
||||
self::STATE_CUSTOMER_SAFE_READY => 'success',
|
||||
self::STATE_PUBLICATION_BLOCKED => 'danger',
|
||||
self::STATE_EXPORT_NOT_READY => 'gray',
|
||||
default => 'warning',
|
||||
};
|
||||
}
|
||||
|
||||
private static function severity(string $state): string
|
||||
{
|
||||
return match ($state) {
|
||||
self::STATE_CUSTOMER_SAFE_READY => 'success',
|
||||
self::STATE_PUBLICATION_BLOCKED => 'danger',
|
||||
self::STATE_EXPORT_NOT_READY => 'warning',
|
||||
default => 'warning',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $readiness
|
||||
*/
|
||||
private static function boundaryState(string $state, array $readiness): string
|
||||
{
|
||||
return match ($state) {
|
||||
self::STATE_CUSTOMER_SAFE_READY => 'customer_safe_ready',
|
||||
self::STATE_INTERNAL_ONLY => 'internal_only',
|
||||
self::STATE_EXPORT_NOT_READY => 'not_ready',
|
||||
self::STATE_PUBLICATION_BLOCKED, self::STATE_PUBLISHED_WITH_LIMITATIONS => 'requires_review',
|
||||
default => (string) ($readiness['customer_safe_state'] ?? 'requires_review'),
|
||||
};
|
||||
}
|
||||
|
||||
private static function boundaryLabel(string $state): string
|
||||
{
|
||||
return match ($state) {
|
||||
'customer_safe_ready' => __('localization.review.customer_safe'),
|
||||
'internal_only' => __('localization.review.internal_only'),
|
||||
'not_ready' => __('localization.review.not_ready'),
|
||||
default => __('localization.review.requires_review'),
|
||||
};
|
||||
}
|
||||
|
||||
private static function boundaryColor(string $state): string
|
||||
{
|
||||
return match ($state) {
|
||||
'customer_safe_ready' => 'success',
|
||||
'internal_only', 'requires_review' => 'warning',
|
||||
default => 'gray',
|
||||
};
|
||||
}
|
||||
|
||||
private static function defaultPrimaryReason(string $state): string
|
||||
{
|
||||
return match ($state) {
|
||||
self::STATE_PUBLICATION_BLOCKED => __('localization.review.publication_blocked_description'),
|
||||
self::STATE_EXPORT_NOT_READY => __('localization.review.export_not_ready_guidance_reason'),
|
||||
self::STATE_INTERNAL_ONLY => __('localization.review.internal_package_includes_pii_reason'),
|
||||
self::STATE_CUSTOMER_SAFE_READY => __('localization.review.customer_safe_review_pack_ready_reason'),
|
||||
default => __('localization.review.review_pack_with_limitations_description'),
|
||||
};
|
||||
}
|
||||
|
||||
private static function shortReason(string $limitationKey): string
|
||||
{
|
||||
return match ($limitationKey) {
|
||||
'publish_blockers_present' => __('localization.review.publication_blocked_short_reason'),
|
||||
'export_not_ready' => __('localization.review.export_not_ready_short_reason'),
|
||||
'evidence_basis_missing', 'evidence_basis_stale', 'evidence_basis_incomplete' => __('localization.review.evidence_basis_incomplete_short_reason'),
|
||||
'required_sections_incomplete' => __('localization.review.required_review_sections_missing_short_reason'),
|
||||
'contains_pii' => __('localization.review.internal_package_includes_pii_short_reason'),
|
||||
'disclosure_missing' => __('localization.review.output_disclosure_missing_short_reason'),
|
||||
default => __('localization.review.review_output_limitations'),
|
||||
};
|
||||
}
|
||||
|
||||
private static function impact(string $state): string
|
||||
{
|
||||
return match ($state) {
|
||||
self::STATE_PUBLICATION_BLOCKED => __('localization.review.publication_blocked_impact'),
|
||||
self::STATE_EXPORT_NOT_READY => __('localization.review.export_not_ready_impact'),
|
||||
self::STATE_INTERNAL_ONLY => __('localization.review.internal_review_package_available_impact'),
|
||||
self::STATE_CUSTOMER_SAFE_READY => __('localization.review.customer_safe_review_pack_ready_impact'),
|
||||
default => __('localization.review.published_with_limitations_impact'),
|
||||
};
|
||||
}
|
||||
|
||||
private static function actionHelp(string $state): ?string
|
||||
{
|
||||
return match ($state) {
|
||||
self::STATE_PUBLICATION_BLOCKED => __('localization.review.output_action_help_publication_blocked'),
|
||||
self::STATE_PUBLISHED_WITH_LIMITATIONS => __('localization.review.output_action_help_published_with_limitations'),
|
||||
self::STATE_INTERNAL_ONLY => __('localization.review.output_action_help_internal_only'),
|
||||
self::STATE_EXPORT_NOT_READY => __('localization.review.output_action_help_export_not_ready'),
|
||||
self::STATE_CUSTOMER_SAFE_READY => __('localization.review.output_action_help_customer_safe_ready'),
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
private static function qualifiedDownloadLabel(string $state): string
|
||||
{
|
||||
return match ($state) {
|
||||
self::STATE_CUSTOMER_SAFE_READY => __('localization.review.download_customer_safe_review_pack'),
|
||||
self::STATE_INTERNAL_ONLY => __('localization.review.download_internal_review_pack'),
|
||||
default => __('localization.review.download_review_pack_with_limitations'),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $readiness
|
||||
* @return array<string, string>
|
||||
*/
|
||||
private static function technicalDetails(array $readiness): array
|
||||
{
|
||||
$sectionSummary = is_array($readiness['section_summary'] ?? null) ? $readiness['section_summary'] : [];
|
||||
|
||||
return [
|
||||
__('localization.review.review_status') => (string) ($readiness['review_status'] ?? __('localization.review.unavailable')),
|
||||
__('localization.review.output_readiness') => self::statusLabel(self::state($readiness, self::limitations($readiness, []))),
|
||||
__('localization.review.publication_sharing_state') => self::boundaryLabel(self::boundaryState(
|
||||
self::state($readiness, self::limitations($readiness, [])),
|
||||
$readiness,
|
||||
)),
|
||||
__('localization.review.has_ready_export') => (bool) ($readiness['has_ready_export'] ?? false)
|
||||
? __('localization.review.yes')
|
||||
: __('localization.review.no'),
|
||||
__('localization.review.evidence_basis_state') => (string) ($readiness['evidence_completeness_state'] ?? __('localization.review.unavailable')),
|
||||
__('localization.review.section_completeness') => __('localization.review.technical_detail_required_sections_value', [
|
||||
'complete' => (int) ($sectionSummary['required_complete'] ?? 0),
|
||||
'total' => (int) ($sectionSummary['required_total'] ?? 0),
|
||||
'limited' => (int) ($sectionSummary['required_limited'] ?? 0),
|
||||
]),
|
||||
__('localization.review.pii_state') => (bool) ($readiness['contains_pii'] ?? false)
|
||||
? __('localization.review.yes')
|
||||
: __('localization.review.no'),
|
||||
__('localization.review.protected_values') => (bool) ($readiness['protected_values_hidden'] ?? true)
|
||||
? __('localization.review.protected_values_hidden')
|
||||
: __('localization.review.unavailable'),
|
||||
__('localization.review.disclosure') => (bool) ($readiness['disclosure_present'] ?? false)
|
||||
? __('localization.review.disclosure_present')
|
||||
: __('localization.review.no'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -506,6 +506,20 @@
|
||||
'tenant' => 'Umgebung',
|
||||
'latest_review' => 'Letztes Review',
|
||||
'review_status' => 'Review-Status',
|
||||
'output_guidance' => 'Output-Guidance',
|
||||
'output_readiness' => 'Output-Bereitschaft',
|
||||
'publication_sharing_state' => 'Publikations-/Freigabestatus',
|
||||
'output_not_customer_ready' => 'Output nicht kundenbereit',
|
||||
'publication_blocked' => 'Publikation blockiert',
|
||||
'publication_blocked_description' => 'Review-Blocker müssen aufgelöst werden, bevor dieser Output als kundenbereit behandelt werden kann.',
|
||||
'publication_blocked_short_reason' => 'Für diesen Output sind weiterhin Review-Blocker erfasst.',
|
||||
'publication_blocked_impact' => 'Behandeln Sie diese Review-Ausgabe erst als kundenbereit, wenn die Blocker aufgelöst sind.',
|
||||
'output_limitations' => 'Output-Einschränkungen',
|
||||
'output_limitations_summary' => '{1} 1 Einschränkung benötigt Prüfung|[2,*] :count Einschränkungen benötigen Prüfung',
|
||||
'technical_details' => 'Technische Details',
|
||||
'has_ready_export' => 'Bereiter Export',
|
||||
'yes' => 'Ja',
|
||||
'no' => 'Nein',
|
||||
'status' => 'Status',
|
||||
'control' => 'Control',
|
||||
'control_interpretation' => 'Control-Readiness-Interpretation',
|
||||
@ -554,7 +568,32 @@
|
||||
'download_governance_package' => 'Governance-Paket herunterladen',
|
||||
'review_package_contents' => 'Paketinhalt prüfen',
|
||||
'review_output_limitations' => 'Output-Einschränkungen prüfen',
|
||||
'review_limitations_below' => 'Einschränkungen unten prüfen',
|
||||
'review_section_limitations' => 'Abschnittseinschränkungen prüfen',
|
||||
'review_pii_redaction_state' => 'PII-/Redaktionsstatus prüfen',
|
||||
'resolve_review_blockers' => 'Review-Blocker prüfen',
|
||||
'open_operation_proof' => 'Operationsnachweis öffnen',
|
||||
'open_evidence_basis' => 'Evidence-Basis öffnen',
|
||||
'output_guidance_detail_mode_note' => 'Sie befinden sich bereits auf der Review-Detailseite für diesen Output. Nutzen Sie die Einschränkungen und technischen Details unten, um Blocker, Evidence-Status und den aktuellen Export zu prüfen.',
|
||||
'output_action_help_publication_blocked' => 'Die primäre Aktion öffnet die Review-Detailseite mit Blockern, Evidence-Status und nächsten Schritten. Die sekundären Aktionen laden das aktuelle Paket herunter oder springen zum Evidence-Snapshot und Operationsnachweis.',
|
||||
'output_action_help_published_with_limitations' => 'Die primäre Aktion öffnet die Review-Detailseite zur aktuellen Einschränkung. Die sekundären Aktionen laden das aktuelle Paket herunter oder springen zur Evidence-Basis.',
|
||||
'output_action_help_internal_only' => 'Die primäre Aktion öffnet die Review-Detailseite für PII- und Redaktionsprüfungen. Die sekundären Aktionen erlauben den Download des internen Pakets oder öffnen den veröffentlichten Datensatz.',
|
||||
'output_action_help_export_not_ready' => 'Die primäre Aktion öffnet die Review-Detailseite mit den aktuellen Output-Einschränkungen. Die sekundären Aktionen springen zur Evidence-Basis oder zum veröffentlichten Review.',
|
||||
'output_action_help_customer_safe_ready' => 'Die primäre Aktion lädt das kundensichere Paket herunter. Über die sekundäre Aktion öffnen Sie die Detailseite des veröffentlichten Reviews.',
|
||||
'evidence_basis_incomplete_guidance' => 'Evidence-Basis unvollständig',
|
||||
'evidence_basis_incomplete_guidance_reason' => 'Das Review-Paket ist an einen Evidence-Snapshot mit fehlenden oder unvollständigen Nachweisen gebunden.',
|
||||
'evidence_basis_incomplete_short_reason' => 'Die Evidence-Basis ist unvollständig.',
|
||||
'required_review_sections_missing' => 'Erforderliche Review-Abschnitte fehlen',
|
||||
'required_review_sections_missing_reason' => '{1} 1 erforderlicher Abschnitt ist teilweise, fehlend oder veraltet.|[2,*] :count erforderliche Abschnitte sind teilweise, fehlend oder veraltet.',
|
||||
'required_review_sections_missing_short_reason' => 'Erforderliche Review-Abschnitte fehlen oder sind unvollständig.',
|
||||
'internal_package_includes_pii' => 'Internes Paket enthält PII',
|
||||
'internal_package_includes_pii_reason' => 'Dieser Export enthält PII-tragende Details und sollte vor externer Weitergabe geprüft werden.',
|
||||
'internal_package_includes_pii_short_reason' => 'Dieser Export enthält interne oder PII-tragende Details.',
|
||||
'export_not_ready_guidance_reason' => 'Das Review-Paket existiert, aber der Export-Bereitschaftsvertrag ist noch nicht erfüllt.',
|
||||
'export_not_ready_short_reason' => 'Der aktuelle Export ist noch nicht bereit.',
|
||||
'output_disclosure_missing' => 'Erforderliche Offenlegung fehlt',
|
||||
'output_disclosure_missing_reason' => 'Die kundensichere Offenlegung fehlt im aktuellen Output-Paket.',
|
||||
'output_disclosure_missing_short_reason' => 'Die erforderliche Offenlegung fehlt im aktuellen Output.',
|
||||
'governance_package' => 'Governance-Paket',
|
||||
'governance_decisions' => 'Governance-Entscheidungen',
|
||||
'governance_decisions_requiring_awareness' => 'Governance-Entscheidungen mit Aufmerksamkeitsbedarf',
|
||||
@ -690,6 +729,13 @@
|
||||
'outcome' => 'Ergebnis',
|
||||
'export' => 'Export',
|
||||
'next_step' => 'Nächster Schritt',
|
||||
'technical_detail_review_status_value' => 'Review-Status: :value',
|
||||
'technical_detail_ready_export_value' => 'Bereiter Export: :value',
|
||||
'technical_detail_evidence_state_value' => 'Evidence-Status: :value',
|
||||
'technical_detail_section_counts_value' => 'Abschnitte - vollständig: :complete, teilweise: :partial, fehlend: :missing, veraltet: :stale',
|
||||
'technical_detail_contains_pii_value' => 'Enthält PII: :value',
|
||||
'technical_detail_disclosure_present_value' => 'Offenlegung vorhanden: :value',
|
||||
'technical_detail_required_sections_value' => ':complete von :total erforderlich vollständig, :limited eingeschränkt',
|
||||
'workspace_next_step_evidence_review' => 'Nachweise prüfen',
|
||||
'workspace_next_step_review_open' => 'Review öffnen',
|
||||
'workspace_next_step_package_review' => 'Paket prüfen',
|
||||
|
||||
@ -506,6 +506,20 @@
|
||||
'tenant' => 'Environment',
|
||||
'latest_review' => 'Latest review',
|
||||
'review_status' => 'Review status',
|
||||
'output_guidance' => 'Output guidance',
|
||||
'output_readiness' => 'Output readiness',
|
||||
'publication_sharing_state' => 'Publication/sharing state',
|
||||
'output_not_customer_ready' => 'Output not customer-ready',
|
||||
'publication_blocked' => 'Publication blocked',
|
||||
'publication_blocked_description' => 'Review blockers must be resolved before this output can be treated as customer-ready.',
|
||||
'publication_blocked_short_reason' => 'Review blockers are still recorded for this output.',
|
||||
'publication_blocked_impact' => 'Do not present this review output as customer-ready until the blockers are resolved.',
|
||||
'output_limitations' => 'Output limitations',
|
||||
'output_limitations_summary' => '{1} 1 limitation requires review|[2,*] :count limitations require review',
|
||||
'technical_details' => 'Technical details',
|
||||
'has_ready_export' => 'Ready export',
|
||||
'yes' => 'Yes',
|
||||
'no' => 'No',
|
||||
'status' => 'Status',
|
||||
'control' => 'Control',
|
||||
'control_interpretation' => 'Control readiness interpretation',
|
||||
@ -554,7 +568,32 @@
|
||||
'download_governance_package' => 'Download governance package',
|
||||
'review_package_contents' => 'Review package contents',
|
||||
'review_output_limitations' => 'Review output limitations',
|
||||
'review_limitations_below' => 'Review limitations below',
|
||||
'review_section_limitations' => 'Review section limitations',
|
||||
'review_pii_redaction_state' => 'Review PII/redaction state',
|
||||
'resolve_review_blockers' => 'Inspect review blockers',
|
||||
'open_operation_proof' => 'Open operation proof',
|
||||
'open_evidence_basis' => 'Open evidence basis',
|
||||
'output_guidance_detail_mode_note' => 'You are already on the review detail for this output. Use the limitations and technical details below to inspect blockers, evidence state, and the current export.',
|
||||
'output_action_help_publication_blocked' => 'The primary action opens the review detail with blockers, evidence status, and next steps. The secondary actions download the current package or jump to the evidence snapshot and operation proof.',
|
||||
'output_action_help_published_with_limitations' => 'The primary action opens the review detail for the current limitation. The secondary actions download the current package or jump to the evidence basis.',
|
||||
'output_action_help_internal_only' => 'The primary action opens the review detail for PII and redaction checks. The secondary actions let you download the internal package or review the released record.',
|
||||
'output_action_help_export_not_ready' => 'The primary action opens the review detail with the current output limitations. The secondary actions jump to the evidence basis or the released review.',
|
||||
'output_action_help_customer_safe_ready' => 'The primary action downloads the customer-safe package. Use the secondary action to open the released review detail.',
|
||||
'evidence_basis_incomplete_guidance' => 'Evidence basis incomplete',
|
||||
'evidence_basis_incomplete_guidance_reason' => 'The review pack is anchored to an evidence snapshot with missing or incomplete evidence.',
|
||||
'evidence_basis_incomplete_short_reason' => 'Evidence basis is incomplete.',
|
||||
'required_review_sections_missing' => 'Required review sections missing',
|
||||
'required_review_sections_missing_reason' => '{1} 1 required section is partial, missing, or stale.|[2,*] :count required sections are partial, missing, or stale.',
|
||||
'required_review_sections_missing_short_reason' => 'Required review sections are missing or incomplete.',
|
||||
'internal_package_includes_pii' => 'Internal package includes PII',
|
||||
'internal_package_includes_pii_reason' => 'This export includes PII-bearing detail and should be reviewed before external sharing.',
|
||||
'internal_package_includes_pii_short_reason' => 'This export includes internal or PII-bearing detail.',
|
||||
'export_not_ready_guidance_reason' => 'The review package exists, but the export-readiness contract has not passed.',
|
||||
'export_not_ready_short_reason' => 'The current export is not ready yet.',
|
||||
'output_disclosure_missing' => 'Required disclosure missing',
|
||||
'output_disclosure_missing_reason' => 'The customer-safe disclosure is missing from the current output package.',
|
||||
'output_disclosure_missing_short_reason' => 'Required disclosure is missing from the current output.',
|
||||
'governance_package' => 'Governance package',
|
||||
'governance_decisions' => 'Governance decisions',
|
||||
'governance_decisions_requiring_awareness' => 'Governance decisions requiring awareness',
|
||||
@ -690,6 +729,13 @@
|
||||
'outcome' => 'Outcome',
|
||||
'export' => 'Export',
|
||||
'next_step' => 'Next step',
|
||||
'technical_detail_review_status_value' => 'Review status: :value',
|
||||
'technical_detail_ready_export_value' => 'Ready export: :value',
|
||||
'technical_detail_evidence_state_value' => 'Evidence state: :value',
|
||||
'technical_detail_section_counts_value' => 'Sections - complete: :complete, partial: :partial, missing: :missing, stale: :stale',
|
||||
'technical_detail_contains_pii_value' => 'Contains PII: :value',
|
||||
'technical_detail_disclosure_present_value' => 'Disclosure present: :value',
|
||||
'technical_detail_required_sections_value' => ':complete of :total required complete, :limited limited',
|
||||
'workspace_next_step_evidence_review' => 'Review evidence',
|
||||
'workspace_next_step_review_open' => 'Open review',
|
||||
'workspace_next_step_package_review' => 'Review package',
|
||||
|
||||
@ -0,0 +1,153 @@
|
||||
@php
|
||||
$state = $getState();
|
||||
$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'] : [];
|
||||
$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'));
|
||||
@endphp
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="rounded-xl border border-gray-200 bg-white p-4 shadow-sm dark:border-gray-800 dark:bg-gray-900">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<x-filament::badge :color="$state['color'] ?? 'gray'">
|
||||
{{ $state['label'] ?? __('localization.review.requires_review') }}
|
||||
</x-filament::badge>
|
||||
<x-filament::badge :color="$state['boundary_color'] ?? 'gray'">
|
||||
{{ $state['boundary_label'] ?? __('localization.review.requires_review') }}
|
||||
</x-filament::badge>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 grid gap-3 md:grid-cols-3">
|
||||
<div class="rounded-md border border-gray-100 bg-gray-50 px-3 py-2 dark:border-gray-800 dark:bg-gray-950/60">
|
||||
<div class="text-[11px] font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
||||
{{ __('localization.review.output_readiness') }}
|
||||
</div>
|
||||
<div class="mt-1 text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
{{ $state['status_label'] ?? __('localization.review.requires_review') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-md border border-gray-100 bg-gray-50 px-3 py-2 dark:border-gray-800 dark:bg-gray-950/60">
|
||||
<div class="text-[11px] font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
||||
{{ __('localization.review.publication_sharing_state') }}
|
||||
</div>
|
||||
<div class="mt-1 text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
{{ $state['boundary_label'] ?? __('localization.review.requires_review') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-md border border-gray-100 bg-gray-50 px-3 py-2 dark:border-gray-800 dark:bg-gray-950/60">
|
||||
<div class="text-[11px] font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
||||
{{ __('localization.review.next_step') }}
|
||||
</div>
|
||||
<div class="mt-1 text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
{{ $nextStepLabel }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 space-y-2">
|
||||
<p class="text-sm text-gray-700 dark:text-gray-200">
|
||||
{{ $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') }}
|
||||
</p>
|
||||
@if (filled($contextNote))
|
||||
<p class="text-sm text-gray-600 dark:text-gray-300">
|
||||
{{ $contextNote }}
|
||||
</p>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@unless ($detailMode)
|
||||
<div class="mt-4 flex flex-wrap gap-2">
|
||||
@if (filled(data_get($state, 'primary_action.url')))
|
||||
<x-filament::button
|
||||
tag="a"
|
||||
:href="$state['primary_action']['url']"
|
||||
:icon="$state['primary_action']['icon']"
|
||||
target="_blank"
|
||||
>
|
||||
{{ $state['primary_action']['label'] }}
|
||||
</x-filament::button>
|
||||
@endif
|
||||
|
||||
@foreach ($secondaryActions as $secondaryAction)
|
||||
@continue(! filled($secondaryAction['url'] ?? null))
|
||||
|
||||
<x-filament::button
|
||||
tag="a"
|
||||
:href="$secondaryAction['url']"
|
||||
:icon="$secondaryAction['icon']"
|
||||
color="gray"
|
||||
target="_blank"
|
||||
size="sm"
|
||||
>
|
||||
{{ $secondaryAction['label'] }}
|
||||
</x-filament::button>
|
||||
@endforeach
|
||||
</div>
|
||||
@endunless
|
||||
</div>
|
||||
|
||||
@if ($limitations !== [])
|
||||
<details class="rounded-xl border border-gray-200 bg-white p-4 shadow-sm dark:border-gray-800 dark:bg-gray-900">
|
||||
<summary class="cursor-pointer list-none text-sm font-semibold text-gray-950 dark:text-white">
|
||||
{{ __('localization.review.output_limitations') }}
|
||||
<span class="ml-2 text-xs font-medium text-gray-500 dark:text-gray-400">
|
||||
{{ $state['limitation_summary'] ?? '' }}
|
||||
</span>
|
||||
</summary>
|
||||
|
||||
<div class="mt-3 space-y-3">
|
||||
@foreach ($limitations as $limitation)
|
||||
<div class="rounded-lg border border-gray-100 bg-gray-50 px-3 py-3 dark:border-gray-800 dark:bg-gray-950/60">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
{{ $limitation['label'] }}
|
||||
</div>
|
||||
<x-filament::badge :color="$limitation['severity'] === 'danger' ? 'danger' : 'warning'" size="sm">
|
||||
{{ \Illuminate\Support\Str::headline($limitation['severity']) }}
|
||||
</x-filament::badge>
|
||||
</div>
|
||||
|
||||
<p class="mt-2 text-sm text-gray-600 dark:text-gray-300">
|
||||
{{ $limitation['reason'] }}
|
||||
</p>
|
||||
|
||||
@if (($limitation['details'] ?? []) !== [])
|
||||
<ul class="mt-2 space-y-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
@foreach ($limitation['details'] as $detail)
|
||||
<li>{{ $detail }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
@endif
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</details>
|
||||
@endif
|
||||
|
||||
@if ($technicalDetails !== [])
|
||||
<details class="rounded-xl border border-dashed border-gray-200 bg-white p-4 shadow-sm dark:border-gray-800 dark:bg-gray-900">
|
||||
<summary class="cursor-pointer list-none text-sm font-semibold text-gray-950 dark:text-white">
|
||||
{{ __('localization.review.technical_details') }}
|
||||
</summary>
|
||||
|
||||
<dl class="mt-3 grid gap-2 md:grid-cols-2">
|
||||
@foreach ($technicalDetails as $label => $value)
|
||||
<div class="rounded-md border border-gray-100 bg-gray-50 px-3 py-2 dark:border-gray-800 dark:bg-gray-950/60">
|
||||
<dt class="text-[11px] font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">{{ $label }}</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">{{ $value }}</dd>
|
||||
</div>
|
||||
@endforeach
|
||||
</dl>
|
||||
</details>
|
||||
@endif
|
||||
</div>
|
||||
@ -118,18 +118,113 @@ class="rounded-xl border border-gray-200 bg-white p-4 shadow-sm dark:border-whit
|
||||
</x-filament::button>
|
||||
@endif
|
||||
|
||||
@if ($readiness['secondary_action_url'])
|
||||
@foreach ($readiness['secondary_actions'] as $secondaryAction)
|
||||
<x-filament::button
|
||||
tag="a"
|
||||
:href="$readiness['secondary_action_url']"
|
||||
:href="$secondaryAction['url']"
|
||||
color="gray"
|
||||
icon="heroicon-o-arrow-top-right-on-square"
|
||||
:icon="$secondaryAction['icon']"
|
||||
data-testid="customer-review-secondary-action"
|
||||
>
|
||||
{{ $readiness['secondary_action_label'] }}
|
||||
{{ $secondaryAction['label'] }}
|
||||
</x-filament::button>
|
||||
@endif
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
@php
|
||||
$outputGuidance = $readiness['output_guidance'] ?? [];
|
||||
$actionHelp = is_string($outputGuidance['action_help'] ?? null) ? $outputGuidance['action_help'] : null;
|
||||
$outputLimitations = is_array($outputGuidance['limitations'] ?? null) ? $outputGuidance['limitations'] : [];
|
||||
$technicalDetails = is_array($outputGuidance['technical_details'] ?? null) ? $outputGuidance['technical_details'] : [];
|
||||
@endphp
|
||||
|
||||
@if (filled($actionHelp))
|
||||
<p
|
||||
class="text-xs leading-5 text-gray-500 dark:text-gray-400"
|
||||
data-testid="customer-review-action-help"
|
||||
>
|
||||
{{ $actionHelp }}
|
||||
</p>
|
||||
@endif
|
||||
|
||||
@if ($outputLimitations !== [])
|
||||
<details
|
||||
class="rounded-lg border border-gray-200 bg-gray-50 px-4 py-3 text-sm dark:border-white/10 dark:bg-white/5"
|
||||
data-testid="customer-review-output-limitations"
|
||||
>
|
||||
<summary class="flex cursor-pointer list-none items-center justify-between gap-3 text-sm font-semibold text-gray-950 dark:text-white">
|
||||
<span>{{ __('localization.review.output_limitations') }}</span>
|
||||
<span class="text-xs font-medium text-gray-500 dark:text-gray-400">
|
||||
{{ $outputGuidance['limitation_summary'] }}
|
||||
</span>
|
||||
</summary>
|
||||
|
||||
<div class="mt-3 space-y-3">
|
||||
@foreach ($outputLimitations as $limitation)
|
||||
<div class="rounded-lg border border-gray-200 bg-white p-3 dark:border-white/10 dark:bg-gray-900">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<div class="text-sm font-medium text-gray-950 dark:text-white">
|
||||
{{ $limitation['label'] }}
|
||||
</div>
|
||||
<x-filament::badge :color="$limitation['severity'] === 'danger' ? 'danger' : 'warning'" size="sm">
|
||||
{{ \Illuminate\Support\Str::headline($limitation['severity']) }}
|
||||
</x-filament::badge>
|
||||
</div>
|
||||
|
||||
<p class="mt-2 text-sm leading-6 text-gray-600 dark:text-gray-300">
|
||||
{{ $limitation['reason'] }}
|
||||
</p>
|
||||
|
||||
@if (($limitation['details'] ?? []) !== [])
|
||||
<ul class="mt-2 space-y-1 text-xs leading-5 text-gray-500 dark:text-gray-400">
|
||||
@foreach ($limitation['details'] as $detail)
|
||||
<li>{{ $detail }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
@endif
|
||||
|
||||
@if (filled(data_get($limitation, 'action.url')))
|
||||
<div class="mt-3">
|
||||
<x-filament::button
|
||||
tag="a"
|
||||
:href="$limitation['action']['url']"
|
||||
color="gray"
|
||||
:icon="$limitation['action']['icon']"
|
||||
size="sm"
|
||||
>
|
||||
{{ $limitation['action']['label'] }}
|
||||
</x-filament::button>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</details>
|
||||
@endif
|
||||
|
||||
@if ($technicalDetails !== [])
|
||||
<details
|
||||
class="rounded-lg border border-dashed border-gray-200 px-4 py-3 text-sm dark:border-white/10"
|
||||
data-testid="customer-review-technical-details"
|
||||
>
|
||||
<summary class="cursor-pointer list-none text-sm font-semibold text-gray-950 dark:text-white">
|
||||
{{ __('localization.review.technical_details') }}
|
||||
</summary>
|
||||
|
||||
<dl class="mt-3 grid gap-2 sm:grid-cols-2">
|
||||
@foreach ($technicalDetails as $detailLabel => $detailValue)
|
||||
<div class="rounded-md border border-gray-200 bg-gray-50 px-3 py-2 dark:border-white/10 dark:bg-white/5">
|
||||
<dt class="text-[0.7rem] font-semibold uppercase leading-4 tracking-wide text-gray-500 dark:text-gray-400">
|
||||
{{ $detailLabel }}
|
||||
</dt>
|
||||
<dd class="mt-1 text-sm text-gray-700 dark:text-gray-200">
|
||||
{{ $detailValue }}
|
||||
</dd>
|
||||
</div>
|
||||
@endforeach
|
||||
</dl>
|
||||
</details>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -116,7 +116,7 @@
|
||||
->assertSee('Kunden-Workspace öffnen')
|
||||
->assertNoJavaScriptErrors()
|
||||
->assertNoConsoleLogs()
|
||||
->click('Kunden-Workspace öffnen')
|
||||
->click('a.fi-link[href*="/admin/reviews/workspace?environment_id="]')
|
||||
->waitForText('Kundensichere Review-Pakete')
|
||||
->assertSee('Filter löschen')
|
||||
->assertSee('Review öffnen')
|
||||
|
||||
@ -108,8 +108,8 @@
|
||||
|
||||
$page = visit(CustomerReviewWorkspace::environmentFilterUrl($notReadyEnvironment))
|
||||
->resize(1236, 900)
|
||||
->waitForText('Published with limitations')
|
||||
->assertSee('The review package is published, but the evidence basis is incomplete.')
|
||||
->waitForText('Output not customer-ready')
|
||||
->assertSee('Review blockers are still recorded for this output.')
|
||||
->assertSee('Needs review')
|
||||
->assertSee('Download review pack with limitations')
|
||||
->assertSee('Review consumption flow')
|
||||
|
||||
@ -92,8 +92,8 @@
|
||||
spec347CopyBrowserScreenshot('01-customer-safe-ready');
|
||||
|
||||
$page = visit(CustomerReviewWorkspace::environmentFilterUrl($limitedEnvironment))
|
||||
->waitForText('Published with limitations')
|
||||
->assertSee('The review package is published, but the evidence basis is incomplete.')
|
||||
->waitForText('Output not customer-ready')
|
||||
->assertSee('Review blockers are still recorded for this output.')
|
||||
->assertSee('Download review pack with limitations')
|
||||
->assertSee('Requires review')
|
||||
->assertNoJavaScriptErrors()
|
||||
@ -104,6 +104,7 @@
|
||||
$page = visit(CustomerReviewWorkspace::environmentFilterUrl($internalEnvironment))
|
||||
->waitForText('Internal review package available')
|
||||
->assertSee('Contains PII')
|
||||
->assertSee('Review PII/redaction state')
|
||||
->assertSee('Download internal review pack')
|
||||
->assertSee('Internal only')
|
||||
->assertNoJavaScriptErrors()
|
||||
|
||||
@ -0,0 +1,267 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Filament\Pages\Reviews\CustomerReviewWorkspace;
|
||||
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('Spec349 smokes output resolution guidance states and collapsed disclosures', function (): void {
|
||||
[$user, $readyEnvironment] = createUserWithTenant(role: 'owner', workspaceRole: 'manager');
|
||||
$readyEnvironment->forceFill(['name' => 'Spec349 Browser Ready'])->save();
|
||||
$blockedEnvironment = spec349BrowserEnvironmentFor($user, $readyEnvironment, 'Spec349 Browser Blocked');
|
||||
$internalEnvironment = spec349BrowserEnvironmentFor($user, $readyEnvironment, 'Spec349 Browser Internal');
|
||||
|
||||
spec349BrowserCreatePublishedReviewWithPack(
|
||||
$readyEnvironment,
|
||||
$user,
|
||||
seedEnvironmentReviewEvidence($readyEnvironment, findingCount: 0, driftCount: 0),
|
||||
[],
|
||||
[
|
||||
'include_pii' => false,
|
||||
'include_operations' => true,
|
||||
],
|
||||
'review-packs/spec349-browser-ready.zip',
|
||||
markReady: true,
|
||||
);
|
||||
|
||||
spec349BrowserCreatePublishedReviewWithPack(
|
||||
$blockedEnvironment,
|
||||
$user,
|
||||
seedPartialEnvironmentReviewEvidence($blockedEnvironment, findingCount: 0, driftCount: 0),
|
||||
[
|
||||
'governance_package' => [
|
||||
'decision_summary' => [
|
||||
'status' => 'incomplete',
|
||||
'evidence_state' => EnvironmentReviewCompletenessState::Partial->value,
|
||||
'decision_data_state' => 'incomplete',
|
||||
'total_count' => 1,
|
||||
'summary' => 'Decision evidence is incomplete for this released review.',
|
||||
'next_action' => 'Review the evidence basis before relying on the decision summary.',
|
||||
'entries' => [],
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'include_pii' => false,
|
||||
'include_operations' => true,
|
||||
],
|
||||
'review-packs/spec349-browser-blocked.zip',
|
||||
markReady: false,
|
||||
);
|
||||
|
||||
spec349BrowserCreatePublishedReviewWithPack(
|
||||
$internalEnvironment,
|
||||
$user,
|
||||
seedEnvironmentReviewEvidence($internalEnvironment, findingCount: 0, driftCount: 0),
|
||||
[],
|
||||
[
|
||||
'include_pii' => true,
|
||||
'include_operations' => true,
|
||||
],
|
||||
'review-packs/spec349-browser-internal.zip',
|
||||
markReady: true,
|
||||
);
|
||||
|
||||
spec349AuthenticateBrowser($this, $user, $readyEnvironment);
|
||||
|
||||
$page = visit(CustomerReviewWorkspace::environmentFilterUrl($blockedEnvironment))
|
||||
->resize(1236, 900)
|
||||
->waitForText('Output not customer-ready')
|
||||
->assertSee('Inspect review blockers')
|
||||
->assertSee('Download review pack with limitations')
|
||||
->assertSee('The primary action opens the review detail with blockers, evidence status, and next steps.')
|
||||
->assertScript('Array.from(document.querySelectorAll("[data-testid=\"customer-review-decision-card\"] [data-testid=\"customer-review-secondary-action\"]")).some((element) => element.innerText.includes("Open review")) === false', true)
|
||||
->assertSee('Requires review')
|
||||
->assertScript('document.querySelector("[data-testid=\"customer-review-output-limitations\"]")?.open === false', true)
|
||||
->assertScript('document.querySelector("[data-testid=\"customer-review-technical-details\"]")?.open === false', true)
|
||||
->assertNoJavaScriptErrors()
|
||||
->assertNoConsoleLogs();
|
||||
$page->screenshot(true, spec349BrowserScreenshotName('01-output-blocked'));
|
||||
spec349CopyBrowserScreenshot('01-output-blocked');
|
||||
|
||||
$page = visit(CustomerReviewWorkspace::environmentFilterUrl($internalEnvironment))
|
||||
->waitForText('Internal review package available')
|
||||
->assertSee('Review PII/redaction state')
|
||||
->assertSee('Download internal review pack')
|
||||
->assertSee('Internal only')
|
||||
->assertNoJavaScriptErrors()
|
||||
->assertNoConsoleLogs();
|
||||
$page->screenshot(true, spec349BrowserScreenshotName('02-internal-only'));
|
||||
spec349CopyBrowserScreenshot('02-internal-only');
|
||||
|
||||
$page = visit(CustomerReviewWorkspace::environmentFilterUrl($readyEnvironment))
|
||||
->waitForText('Customer-safe review pack ready')
|
||||
->assertSee('Download customer-safe review pack')
|
||||
->assertDontSee('Ready to share')
|
||||
->assertNoJavaScriptErrors()
|
||||
->assertNoConsoleLogs();
|
||||
$page->screenshot(true, spec349BrowserScreenshotName('03-customer-safe-ready'));
|
||||
spec349CopyBrowserScreenshot('03-customer-safe-ready');
|
||||
});
|
||||
|
||||
function spec349BrowserScreenshotName(string $name): string
|
||||
{
|
||||
return 'spec349-output-resolution-guidance-'.$name;
|
||||
}
|
||||
|
||||
function spec349CopyBrowserScreenshot(string $name): void
|
||||
{
|
||||
$filename = spec349BrowserScreenshotName($name).'.png';
|
||||
$source = base_path('tests/Browser/Screenshots/'.$filename);
|
||||
$targetDirectory = repo_path('specs/349-customer-review-workspace-output-resolution-guidance/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 spec349AuthenticateBrowser(mixed $test, User $user, ManagedEnvironment $environment): void
|
||||
{
|
||||
$workspaceId = (int) $environment->workspace_id;
|
||||
|
||||
$test->actingAs($user)->withSession([
|
||||
WorkspaceContext::SESSION_KEY => $workspaceId,
|
||||
WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => [
|
||||
(string) $workspaceId => (int) $environment->getKey(),
|
||||
],
|
||||
]);
|
||||
|
||||
session()->put(WorkspaceContext::SESSION_KEY, $workspaceId);
|
||||
session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [
|
||||
(string) $workspaceId => (int) $environment->getKey(),
|
||||
]);
|
||||
|
||||
setAdminPanelContext($environment);
|
||||
}
|
||||
|
||||
function spec349BrowserEnvironmentFor(User $user, ManagedEnvironment $baseEnvironment, string $name): ManagedEnvironment
|
||||
{
|
||||
$environment = ManagedEnvironment::factory()->active()->create([
|
||||
'workspace_id' => (int) $baseEnvironment->workspace_id,
|
||||
'name' => $name,
|
||||
]);
|
||||
|
||||
createUserWithTenant(tenant: $environment, user: $user, role: 'owner', workspaceRole: 'manager');
|
||||
|
||||
return $environment;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $summaryOverrides
|
||||
* @param array<string, mixed> $packOptions
|
||||
* @return array{0: EnvironmentReview, 1: ReviewPack}
|
||||
*/
|
||||
function spec349BrowserCreatePublishedReviewWithPack(
|
||||
ManagedEnvironment $environment,
|
||||
User $user,
|
||||
EvidenceSnapshot $snapshot,
|
||||
array $summaryOverrides = [],
|
||||
array $packOptions = [],
|
||||
string $filePath = 'review-packs/spec349-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.',
|
||||
],
|
||||
],
|
||||
],
|
||||
'governance_package' => [
|
||||
'decision_summary' => [
|
||||
'status' => $markReady ? 'none' : 'incomplete',
|
||||
'evidence_state' => $markReady ? EnvironmentReviewCompletenessState::Complete->value : EnvironmentReviewCompletenessState::Partial->value,
|
||||
'decision_data_state' => $markReady ? 'complete' : 'incomplete',
|
||||
'total_count' => $markReady ? 0 : 1,
|
||||
'summary' => $markReady
|
||||
? 'No governance decisions require customer awareness.'
|
||||
: 'Decision evidence is incomplete for this released review.',
|
||||
'next_action' => $markReady
|
||||
? 'Open the current customer review pack.'
|
||||
: 'Review the evidence basis before relying on the decision summary.',
|
||||
'entries' => [],
|
||||
],
|
||||
],
|
||||
],
|
||||
$summaryOverrides,
|
||||
);
|
||||
|
||||
Storage::disk('exports')->put($filePath, 'PK-spec349-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->refresh(), $pack->refresh()];
|
||||
}
|
||||
@ -221,7 +221,16 @@ function environmentReviewContractHeaderActions(Testable $component): array
|
||||
->assertActionDoesNotExist('export_executive_pack')
|
||||
->assertActionDoesNotExist('archive_review');
|
||||
|
||||
$component->assertActionExists('download_current_review_pack', fn (Action $action): bool => $action->getLabel() === 'Download governance package');
|
||||
$component->assertActionExists('download_current_review_pack', function (Action $action): bool {
|
||||
$label = $action->getLabel();
|
||||
|
||||
return $label !== 'Download governance package'
|
||||
&& in_array($label, [
|
||||
'Download customer-safe review pack',
|
||||
'Download review pack with limitations',
|
||||
'Download internal review pack',
|
||||
], true);
|
||||
});
|
||||
|
||||
$topLevelActionNames = collect(environmentReviewContractHeaderActions($component))
|
||||
->map(static fn ($action): ?string => $action instanceof Action ? $action->getName() : null)
|
||||
|
||||
@ -0,0 +1,161 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Filament\Pages\Reviews\CustomerReviewWorkspace;
|
||||
use App\Filament\Resources\EnvironmentReviewResource;
|
||||
use App\Filament\Resources\EnvironmentReviewResource\Pages\ViewEnvironmentReview;
|
||||
use App\Models\ManagedEnvironment;
|
||||
use App\Models\ReviewPack;
|
||||
use App\Models\User;
|
||||
use App\Support\EnvironmentReviewStatus;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Livewire\Livewire;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function (): void {
|
||||
Storage::fake('exports');
|
||||
});
|
||||
|
||||
it('separates review status, output readiness, and publication sharing state on the review detail page', function (): void {
|
||||
$tenant = ManagedEnvironment::factory()->create();
|
||||
[$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/spec349-detail-blocked.zip', 'PK-spec349-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/spec349-detail-blocked.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], $tenant))
|
||||
->assertOk()
|
||||
->assertSee('Review status')
|
||||
->assertSee('Output readiness')
|
||||
->assertSee('Publication/sharing state')
|
||||
->assertSee('Publication blocked')
|
||||
->assertSee('Inspect review blockers')
|
||||
->assertSee('Technical details')
|
||||
->assertDontSee('Ready to share');
|
||||
});
|
||||
|
||||
it('removes the repeated action rail from the customer-workspace detail context', function (): void {
|
||||
$tenant = ManagedEnvironment::factory()->create();
|
||||
[$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/spec349-detail-context-blocked.zip', 'PK-spec349-detail-context-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/spec349-detail-context-blocked.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 guidance')
|
||||
->assertSee('Review limitations below')
|
||||
->assertSee('You are already on the review detail for this output.')
|
||||
->assertDontSee('Inspect review blockers')
|
||||
->assertDontSee('Open evidence basis')
|
||||
->assertDontSee('Open operation proof');
|
||||
});
|
||||
|
||||
it('qualifies the customer-workspace detail download action instead of using a generic package label', function (): void {
|
||||
$tenant = ManagedEnvironment::factory()->create();
|
||||
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'readonly');
|
||||
$snapshot = seedEnvironmentReviewEvidence($tenant, findingCount: 0, driftCount: 0);
|
||||
$review = composeEnvironmentReviewForTest($tenant, $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/spec349-detail-internal.zip', 'PK-spec349-detail-internal');
|
||||
|
||||
$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) $user->getKey(),
|
||||
'options' => [
|
||||
'include_pii' => true,
|
||||
'include_operations' => true,
|
||||
],
|
||||
'file_path' => 'review-packs/spec349-detail-internal.zip',
|
||||
'file_disk' => 'exports',
|
||||
'generated_at' => now()->subMinutes(3),
|
||||
]);
|
||||
|
||||
$review->forceFill(['current_export_review_pack_id' => (int) $pack->getKey()])->save();
|
||||
|
||||
setAdminEnvironmentContext($tenant);
|
||||
|
||||
Livewire::withQueryParams([CustomerReviewWorkspace::DETAIL_CONTEXT_QUERY_KEY => 1])
|
||||
->actingAs($user)
|
||||
->test(ViewEnvironmentReview::class, ['record' => $review->getKey()])
|
||||
->assertActionVisible('download_current_review_pack')
|
||||
->assertActionEnabled('download_current_review_pack')
|
||||
->assertActionExists('download_current_review_pack', fn (\Filament\Actions\Action $action): bool => $action->getLabel() === 'Download internal review pack');
|
||||
});
|
||||
@ -149,8 +149,8 @@
|
||||
$review->forceFill(['current_export_review_pack_id' => (int) $pack->getKey()])->save();
|
||||
|
||||
$component = spec342WorkspaceComponent($user, $environment)
|
||||
->assertSee('Published with limitations')
|
||||
->assertSee('The review package is published, but the evidence basis is incomplete.')
|
||||
->assertSee('Output not customer-ready')
|
||||
->assertSee('Review blockers are still recorded for this output.')
|
||||
->assertSee('Customer-safe output')
|
||||
->assertSee('Needs review')
|
||||
->assertSee('Diagnostics')
|
||||
|
||||
@ -47,7 +47,7 @@
|
||||
->assertDontSee('Ready to share');
|
||||
});
|
||||
|
||||
it('shows published-with-limitations when evidence is incomplete even if a pack exists', function (): void {
|
||||
it('shows blocked output guidance when evidence is incomplete and review blockers remain recorded', function (): void {
|
||||
$environment = ManagedEnvironment::factory()->create(['name' => 'Spec347 Limitations']);
|
||||
[$user, $environment] = createUserWithTenant(tenant: $environment, role: 'readonly');
|
||||
$snapshot = seedPartialEnvironmentReviewEvidence($environment, findingCount: 0, driftCount: 0);
|
||||
@ -72,8 +72,8 @@
|
||||
$review->forceFill(['current_export_review_pack_id' => (int) $pack->getKey()])->save();
|
||||
|
||||
spec347WorkspaceComponent($user, $environment)
|
||||
->assertSee('Published with limitations')
|
||||
->assertSee('The review package is published, but the evidence basis is incomplete.')
|
||||
->assertSee('Output not customer-ready')
|
||||
->assertSee('Review blockers are still recorded for this output.')
|
||||
->assertSee('Requires review')
|
||||
->assertSee('Download review pack with limitations')
|
||||
->assertDontSee('Ready to share');
|
||||
@ -108,7 +108,7 @@
|
||||
->assertSee('Internal review package available')
|
||||
->assertSee('Internal only')
|
||||
->assertSee('Contains PII')
|
||||
->assertSee('Review package contents')
|
||||
->assertSee('Review PII/redaction state')
|
||||
->assertSee('Download internal review pack')
|
||||
->assertDontSee('Customer-safe review pack ready');
|
||||
});
|
||||
|
||||
@ -0,0 +1,127 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Filament\Pages\Reviews\CustomerReviewWorkspace;
|
||||
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('groups multiple output limitations behind one primary action and keeps the details disclosure collapsed', function (): void {
|
||||
$environment = ManagedEnvironment::factory()->create(['name' => 'Spec349 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/spec349-blocked.zip', 'PK-spec349-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' => true,
|
||||
'include_operations' => true,
|
||||
],
|
||||
'file_path' => 'review-packs/spec349-blocked.zip',
|
||||
'file_disk' => 'exports',
|
||||
'generated_at' => now()->subMinutes(3),
|
||||
]);
|
||||
|
||||
$review->forceFill(['current_export_review_pack_id' => (int) $pack->getKey()])->save();
|
||||
|
||||
$component = spec349WorkspaceComponent($user, $environment)
|
||||
->assertSee('Output not customer-ready')
|
||||
->assertSee('Inspect review blockers')
|
||||
->assertSee('Evidence basis incomplete')
|
||||
->assertSee('Required review sections missing')
|
||||
->assertSee('Internal package includes PII')
|
||||
->assertSee('The primary action opens the review detail with blockers, evidence status, and next steps.')
|
||||
->assertSee('Technical details');
|
||||
|
||||
$html = $component->html();
|
||||
|
||||
expect(substr_count($html, 'data-testid="customer-review-primary-action"'))->toBe(1)
|
||||
->and($html)->toContain('data-testid="customer-review-output-limitations"')
|
||||
->and($html)->not->toContain('data-testid="customer-review-output-limitations" open')
|
||||
->and($html)->toContain('data-testid="customer-review-action-help"')
|
||||
->and($html)->toContain('data-testid="customer-review-technical-details"')
|
||||
->and($html)->not->toContain('data-testid="customer-review-technical-details" open')
|
||||
->and($html)->not->toContain('Ready to share');
|
||||
});
|
||||
|
||||
it('keeps the visible environment_id workspace filter contract while qualifying the internal download label', function (): void {
|
||||
$environment = ManagedEnvironment::factory()->create(['name' => 'Spec349 Internal']);
|
||||
[$user, $environment] = createUserWithTenant(tenant: $environment, role: 'readonly');
|
||||
$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/spec349-internal.zip', 'PK-spec349-internal');
|
||||
|
||||
$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' => true,
|
||||
'include_operations' => true,
|
||||
],
|
||||
'file_path' => 'review-packs/spec349-internal.zip',
|
||||
'file_disk' => 'exports',
|
||||
'generated_at' => now()->subMinutes(3),
|
||||
]);
|
||||
|
||||
$review->forceFill(['current_export_review_pack_id' => (int) $pack->getKey()])->save();
|
||||
|
||||
Livewire::withQueryParams([
|
||||
'environment_id' => (int) $environment->getKey(),
|
||||
])
|
||||
->actingAs($user)
|
||||
->test(CustomerReviewWorkspace::class)
|
||||
->assertSee('Environment filter:')
|
||||
->assertSee('Spec349 Internal')
|
||||
->assertSee('Internal review package available')
|
||||
->assertSee('Download internal review pack')
|
||||
->assertDontSee('Download governance package')
|
||||
->assertDontSee('Ready to share');
|
||||
});
|
||||
|
||||
function spec349WorkspaceComponent(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,143 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Support\EnvironmentReviewCompletenessState;
|
||||
use App\Support\ReviewPacks\ReviewPackOutputReadiness;
|
||||
use App\Support\ReviewPacks\ReviewPackOutputResolutionGuidance;
|
||||
|
||||
it('maps publication blockers into one primary guidance state and groups the remaining limitations', function (): void {
|
||||
$readiness = spec349Readiness(
|
||||
evidenceState: EnvironmentReviewCompletenessState::Partial->value,
|
||||
hasReadyExport: false,
|
||||
includePii: true,
|
||||
publishBlockers: ['Operator approval note is still missing.'],
|
||||
requiredSectionStates: [
|
||||
EnvironmentReviewCompletenessState::Complete->value => 3,
|
||||
EnvironmentReviewCompletenessState::Missing->value => 2,
|
||||
],
|
||||
);
|
||||
|
||||
$guidance = ReviewPackOutputResolutionGuidance::fromReadiness($readiness, [
|
||||
'download' => '/review-packs/1/download',
|
||||
'review' => '/environment-reviews/1',
|
||||
'evidence' => '/evidence/1',
|
||||
'operation' => '/operations/1',
|
||||
]);
|
||||
|
||||
expect($guidance['state'])->toBe('publication_blocked')
|
||||
->and($guidance['label'])->toBe('Output not customer-ready')
|
||||
->and($guidance['primary_action']['label'])->toBe('Inspect review blockers')
|
||||
->and($guidance['action_help'])->toContain('opens the review detail with blockers, evidence status, and next steps')
|
||||
->and($guidance['limitations'])->toHaveCount(5)
|
||||
->and($guidance['limitations'][0]['key'])->toBe('publish_blockers_present')
|
||||
->and(collect($guidance['secondary_actions'])->pluck('label')->all())->not->toContain('Open review')
|
||||
->and(collect($guidance['limitations'])->pluck('key')->all())->toEqual([
|
||||
'publish_blockers_present',
|
||||
'export_not_ready',
|
||||
'evidence_basis_incomplete',
|
||||
'required_sections_incomplete',
|
||||
'contains_pii',
|
||||
]);
|
||||
});
|
||||
|
||||
it('maps incomplete evidence to a published-with-limitations guidance item', function (): void {
|
||||
$readiness = spec349Readiness(
|
||||
evidenceState: EnvironmentReviewCompletenessState::Missing->value,
|
||||
hasReadyExport: true,
|
||||
);
|
||||
|
||||
$guidance = ReviewPackOutputResolutionGuidance::fromReadiness($readiness, [
|
||||
'review' => '/environment-reviews/1',
|
||||
'evidence' => '/evidence/1',
|
||||
]);
|
||||
|
||||
expect($guidance['state'])->toBe('published_with_limitations')
|
||||
->and($guidance['primary_reason'])->toBe('Evidence basis is incomplete.')
|
||||
->and($guidance['primary_action']['label'])->toBe('Open evidence basis')
|
||||
->and($guidance['limitations'])->toHaveCount(1)
|
||||
->and($guidance['limitations'][0]['label'])->toBe('Evidence basis incomplete');
|
||||
});
|
||||
|
||||
it('renders required section limitation reasons through pluralization instead of raw translation ranges', function (): void {
|
||||
$readiness = spec349Readiness(
|
||||
hasReadyExport: true,
|
||||
requiredSectionStates: [
|
||||
EnvironmentReviewCompletenessState::Complete->value => 1,
|
||||
EnvironmentReviewCompletenessState::Partial->value => 4,
|
||||
EnvironmentReviewCompletenessState::Missing->value => 2,
|
||||
],
|
||||
);
|
||||
|
||||
$guidance = ReviewPackOutputResolutionGuidance::fromReadiness($readiness, [
|
||||
'review' => '/environment-reviews/1',
|
||||
]);
|
||||
|
||||
expect($guidance['limitations'])->toHaveCount(1)
|
||||
->and($guidance['limitations'][0]['key'])->toBe('required_sections_incomplete')
|
||||
->and($guidance['limitations'][0]['reason'])->toBe('6 required sections are partial, missing, or stale.')
|
||||
->and($guidance['limitations'][0]['reason'])->not->toContain('{1}')
|
||||
->and($guidance['limitations'][0]['reason'])->not->toContain('[2,*]');
|
||||
});
|
||||
|
||||
it('marks pii-bearing ready exports as internal-only and qualifies the download action', function (): void {
|
||||
$readiness = spec349Readiness(
|
||||
includePii: true,
|
||||
hasReadyExport: true,
|
||||
);
|
||||
|
||||
$guidance = ReviewPackOutputResolutionGuidance::fromReadiness($readiness, [
|
||||
'download' => '/review-packs/1/download',
|
||||
'review' => '/environment-reviews/1',
|
||||
]);
|
||||
|
||||
expect($guidance['state'])->toBe('internal_only')
|
||||
->and($guidance['label'])->toBe('Internal review package available')
|
||||
->and($guidance['primary_action']['label'])->toBe('Review PII/redaction state')
|
||||
->and(collect($guidance['secondary_actions'])->pluck('label')->all())->toContain('Download internal review pack');
|
||||
});
|
||||
|
||||
it('marks complete non-pii exports as customer-safe ready', function (): void {
|
||||
$readiness = spec349Readiness(
|
||||
hasReadyExport: true,
|
||||
);
|
||||
|
||||
$guidance = ReviewPackOutputResolutionGuidance::fromReadiness($readiness, [
|
||||
'download' => '/review-packs/1/download',
|
||||
'review' => '/environment-reviews/1',
|
||||
]);
|
||||
|
||||
expect($guidance['state'])->toBe('customer_safe_ready')
|
||||
->and($guidance['label'])->toBe('Customer-safe review pack ready')
|
||||
->and($guidance['primary_action']['label'])->toBe('Download customer-safe review pack')
|
||||
->and($guidance['limitations'])->toBeEmpty();
|
||||
});
|
||||
|
||||
/**
|
||||
* @param array<string, int> $requiredSectionStates
|
||||
* @param list<string> $publishBlockers
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
function spec349Readiness(
|
||||
string $evidenceState = EnvironmentReviewCompletenessState::Complete->value,
|
||||
bool $hasReadyExport = true,
|
||||
bool $includePii = false,
|
||||
array $publishBlockers = [],
|
||||
array $requiredSectionStates = [
|
||||
EnvironmentReviewCompletenessState::Complete->value => 5,
|
||||
],
|
||||
): array {
|
||||
return ReviewPackOutputReadiness::derive(
|
||||
reviewStatus: 'published',
|
||||
reviewCompletenessState: EnvironmentReviewCompletenessState::Complete->value,
|
||||
evidenceCompletenessState: $evidenceState,
|
||||
sectionStateCounts: $requiredSectionStates,
|
||||
requiredSectionCount: array_sum($requiredSectionStates),
|
||||
requiredSectionStateCounts: $requiredSectionStates,
|
||||
publishBlockers: $publishBlockers,
|
||||
hasReadyExport: $hasReadyExport,
|
||||
includePii: $includePii,
|
||||
protectedValuesHidden: true,
|
||||
disclosurePresent: true,
|
||||
);
|
||||
}
|
||||
@ -200,8 +200,8 @@ function suspendCustomerReviewWorkspacePackAccessWorkspace(ManagedEnvironment $t
|
||||
Livewire::actingAs($user)
|
||||
->test(CustomerReviewWorkspace::class)
|
||||
->assertSee(EnvironmentReviewResource::environmentScopedUrl('view', ['record' => $review->fresh()], $tenant), false)
|
||||
->assertSee('Published with limitations')
|
||||
->assertSee('The review package is published, but the evidence basis is incomplete.')
|
||||
->assertSee('Output not customer-ready')
|
||||
->assertSee('Review blockers are still recorded for this output.')
|
||||
->assertSee('Download review pack with limitations')
|
||||
->assertSee('Available');
|
||||
});
|
||||
|
||||
@ -364,8 +364,8 @@
|
||||
Livewire::actingAs($user)
|
||||
->test(CustomerReviewWorkspace::class)
|
||||
->assertSee('What is the current review pack output state?')
|
||||
->assertSee('Published with limitations')
|
||||
->assertSee('The review package is published, but the evidence basis is incomplete.')
|
||||
->assertSee('Output not customer-ready')
|
||||
->assertSee('Review blockers are still recorded for this output.')
|
||||
->assertSee('No operation proof linked')
|
||||
->assertSee('Export ready')
|
||||
->assertDontSee('Ready to share')
|
||||
|
||||
@ -73,3 +73,37 @@ ### Browser proof
|
||||
### Deferred
|
||||
|
||||
- The review-pack detail resource and surrounding environment-review detail copy remain intentionally narrow; Spec 347 only touches the workspace/readiness path and supporting handoff copy where needed for contract consistency.
|
||||
|
||||
## Spec 349 Follow-up
|
||||
|
||||
Spec 349 productizes the raw Spec-347 readiness semantics into bounded operator guidance instead of exposing a warning wall.
|
||||
|
||||
- The top decision card now resolves to one dominant output state and one dominant next action:
|
||||
- `Output not customer-ready`
|
||||
- `Published with limitations`
|
||||
- `Internal review package available`
|
||||
- `Customer-safe review pack ready`
|
||||
- Multiple readiness limitations are grouped behind one compact disclosure instead of competing as peer alerts.
|
||||
- Technical details stay collapsed by default and remain available as secondary proof.
|
||||
- Download labels are now readiness-qualified across workspace and customer-workspace detail surfaces:
|
||||
- `Download customer-safe review pack`
|
||||
- `Download internal review pack`
|
||||
- `Download review pack with limitations`
|
||||
- Environment Review detail now separates:
|
||||
- `Review status`
|
||||
- `Output readiness`
|
||||
- `Publication/sharing state`
|
||||
|
||||
### Browser proof
|
||||
|
||||
- Spec349 screenshots: `specs/349-customer-review-workspace-output-resolution-guidance/artifacts/screenshots/`
|
||||
- Verified states:
|
||||
- output blocked / publication-blocked guidance
|
||||
- internal-only / PII-bearing export
|
||||
- customer-safe ready
|
||||
- limitations and technical-details disclosures collapsed by default
|
||||
|
||||
### 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`.
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 311 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 314 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 305 KiB |
@ -0,0 +1,43 @@
|
||||
# Requirements Checklist: Spec 349 - Customer Review Workspace Output Resolution Guidance
|
||||
|
||||
**Purpose**: Validate that Spec 349 is bounded, repo-based, constitution-aligned, and ready for later implementation.
|
||||
**Created**: 2026-06-03
|
||||
**Feature**: `specs/349-customer-review-workspace-output-resolution-guidance/spec.md`
|
||||
|
||||
## Candidate Selection And Scope
|
||||
|
||||
- [x] CHK001 The package names the direct user-provided candidate source and its roadmap alignment with Customer Review Workspace v1 completion.
|
||||
- [x] CHK002 Completed adjacent specs are treated as historical context only and are not reopened or normalized.
|
||||
- [x] CHK003 The scope is narrowed to output-guidance productization over existing readiness truth, workspace, and review detail surfaces only.
|
||||
- [x] CHK004 Explicit non-goals block a workflow engine, persistence, portal, renderer, and broad surface redesign.
|
||||
|
||||
## Repo Truth And Architecture
|
||||
|
||||
- [x] CHK005 The spec and plan anchor the work to the existing `ReviewPackOutputReadiness` truth and current scoped routes.
|
||||
- [x] CHK006 The artifacts state that any new guidance state remains derived-only and not a persisted domain state.
|
||||
- [x] CHK007 The plan avoids inventing a new route family, hidden shell context, or second raw-readiness dialect.
|
||||
- [x] CHK008 The user-draft conflict around `ui-009-review-pack-output-contract.md` is corrected explicitly in the spec and repo-truth map.
|
||||
|
||||
## UI/Productization Coverage
|
||||
|
||||
- [x] CHK009 UI Surface Impact is explicit and consistent with the intended workspace/detail changes.
|
||||
- [x] CHK010 UI/Productization Coverage identifies the existing strategic workspace page report and avoids inventing new route/archetype coverage unnecessarily.
|
||||
- [x] CHK011 The spec requires one dominant output state and one dominant next action rather than many equal-weight warnings.
|
||||
- [x] CHK012 Audience-aware disclosure keeps technical details secondary and customer-safe wording conservative.
|
||||
|
||||
## Testing And Validation
|
||||
|
||||
- [x] CHK013 Planned tests cover workspace guidance, review-pack resolution mapping, detail-surface separation, and one bounded browser smoke.
|
||||
- [x] CHK014 The validation commands explicitly rerun Spec 347 regressions plus filtered workspace/review-pack coverage.
|
||||
- [x] CHK015 The artifacts name `pint --dirty` and `git diff --check` as final prep-aligned validation steps.
|
||||
|
||||
## Review Outcome
|
||||
|
||||
- [x] CHK016 Review outcome class: `acceptable-special-case`
|
||||
- [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 remaining implementation choice is intentionally narrow: extend `ReviewPackOutputReadiness` directly or wrap it with one bounded guidance adapter.
|
||||
@ -0,0 +1,247 @@
|
||||
# Implementation Plan: Spec 349 - Customer Review Workspace Output Resolution Guidance
|
||||
|
||||
**Branch**: `349-customer-review-workspace-output-resolution-guidance` | **Date**: 2026-06-03 | **Spec**: `specs/349-customer-review-workspace-output-resolution-guidance/spec.md`
|
||||
**Input**: User-provided Spec 349 draft + repo truth from current Spec 347 readiness work, current Customer Review Workspace runtime, and current Environment Review detail surface.
|
||||
|
||||
## Summary
|
||||
|
||||
Translate existing Review Pack output-readiness truth into calmer operator guidance without changing the underlying workflow model.
|
||||
|
||||
This slice should:
|
||||
|
||||
- reuse current `ReviewPackOutputReadiness` truth
|
||||
- convert the highest-priority limitation into one dominant blocker and one dominant next action
|
||||
- group remaining limitations into compact disclosure
|
||||
- qualify download wording honestly
|
||||
- surface PII/internal-only boundaries explicitly
|
||||
- separate review publication/completeness from output readiness on Environment Review detail
|
||||
|
||||
This slice must not:
|
||||
|
||||
- create persistence
|
||||
- create a new workflow engine or state machine
|
||||
- reopen Review Pack generation semantics beyond the already-completed Spec 347 contract
|
||||
- build a portal or renderer
|
||||
- weaken current authorization, workspace isolation, or signed-download safety
|
||||
|
||||
## 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 Feature/Livewire tests plus one bounded Pest Browser smoke file
|
||||
- **Validation Lanes**: confidence + browser
|
||||
- **Target Platform**: `apps/platform` Laravel monolith; Sail-first locally; Dokploy posture unchanged
|
||||
- **Project Type**: web application with server-rendered Filament/Blade surfaces
|
||||
- **Performance Goals**: no new remote calls during render, no new queue family, and no duplicate read-model layer beyond a bounded derived guidance adapter
|
||||
- **Constraints**: no false customer-safe wording, no warning wall, no hidden shell-scope behavior, no new route family, no new persisted guidance state, and no detail-surface redesign outside the output-guidance slice
|
||||
- **Scale/Scope**: one strategic workspace surface, one review detail surface, existing review-pack proof/download path, focused Feature coverage, and one Browser smoke
|
||||
|
||||
## UI / Surface Guardrail Plan
|
||||
|
||||
- **Guardrail scope**: material change to an existing strategic customer-safe review surface plus an existing review detail surface
|
||||
- **Affected routes/pages/actions/states/navigation/panel/provider surfaces**:
|
||||
- `/admin/reviews/workspace`
|
||||
- `apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php`
|
||||
- `apps/platform/resources/views/filament/pages/reviews/customer-review-workspace.blade.php`
|
||||
- `apps/platform/app/Filament/Resources/EnvironmentReviewResource.php`
|
||||
- `apps/platform/app/Filament/Resources/EnvironmentReviewResource/Pages/ViewEnvironmentReview.php`
|
||||
- existing review-pack download wording as reached from those surfaces
|
||||
- **No-impact class, if applicable**: N/A
|
||||
- **Native vs custom classification summary**: native Filament page/resource plus existing Blade/infolist composition; no new route or panel/provider
|
||||
- **Shared-family relevance**: status messaging, next-action guidance, disclosure, proof links, qualified download wording
|
||||
- **State layers in scope**: page payload, detail payload, URL-query filter on workspace, derived output-guidance payload only
|
||||
- **Audience modes in scope**: operator-MSP, customer-safe review consumer, support where authorized
|
||||
- **Decision/diagnostic/raw hierarchy plan**: one output verdict first, grouped limitations second, technical details third
|
||||
- **Raw/support gating plan**: keep technical details collapsed or clearly secondary; keep support/raw detail capability-gated where already applicable
|
||||
- **One-primary-action / duplicate-truth control**: preserve one dominant next action and remove repeated blocker summaries from lower panels
|
||||
- **Handling modes by drift class or surface**: review-mandatory
|
||||
- **Repository-signal treatment**: review-mandatory because this is a strategic trust surface
|
||||
- **Special surface test profiles**: `global-context-shell` + `shared-detail-family`
|
||||
- **Required tests or manual smoke**: functional-core + browser smoke
|
||||
- **Exception path and spread control**: one bounded guidance adapter is allowed; no cross-domain framework
|
||||
- **Active feature PR close-out entry**: Guardrail / Smoke Coverage
|
||||
- **UI/Productization coverage decision**: update `docs/ui-ux-enterprise-audit/page-reports/ui-006-customer-review-workspace.md`; add a second report only if implementation proves the existing identity cannot absorb the detail-surface notes cleanly
|
||||
- **Coverage artifacts to update**: existing workspace page report only unless repo truth later proves more is required
|
||||
- **Navigation / Filament provider-panel handling**: N/A; no panel/provider change expected
|
||||
- **Screenshot or page-report need**: yes, because this is a strategic customer-safe surface and one bounded browser smoke will produce proof artifacts
|
||||
|
||||
## Shared Pattern & System Fit
|
||||
|
||||
- **Cross-cutting feature marker**: yes
|
||||
- **Systems touched**:
|
||||
- `App\Support\ReviewPacks\ReviewPackOutputReadiness`
|
||||
- `App\Filament\Pages\Reviews\CustomerReviewWorkspace`
|
||||
- `App\Filament\Resources\EnvironmentReviewResource`
|
||||
- `App\Filament\Resources\EnvironmentReviewResource\Pages\ViewEnvironmentReview`
|
||||
- existing review-pack and evidence link helpers
|
||||
- **Shared abstractions reused**:
|
||||
- current limitation codes and readiness fields from `ReviewPackOutputReadiness`
|
||||
- current workspace link/action helper paths
|
||||
- current Environment Review artifact-truth and summary presentation
|
||||
- **New abstraction introduced? why?**: maybe one narrow `ReviewPackOutputResolutionGuidance`-style adapter if direct extension of `ReviewPackOutputReadiness` would blur raw output truth and UI guidance responsibilities
|
||||
- **Why the existing abstraction was sufficient or insufficient**: current readiness truth is sufficient as the source, but it is not yet shaped for grouped operator guidance across multiple surfaces
|
||||
- **Bounded deviation / spread control**: any new guidance adapter must remain local to review output guidance and must not become a generic workflow-resolution framework
|
||||
|
||||
## OperationRun UX Impact
|
||||
|
||||
- **Touches OperationRun start/completion/link UX?**: existing proof-link usage only
|
||||
- **Central contract reused**: existing Review Pack / Environment Review proof links
|
||||
- **Delegated UX behaviors**: unchanged
|
||||
- **Surface-owned behavior kept local**: one-blocker ranking, grouped limitation copy, and qualified next-action wording
|
||||
- **Queued DB-notification policy**: unchanged
|
||||
- **Terminal notification path**: unchanged
|
||||
- **Exception path**: none
|
||||
|
||||
## Provider Boundary & Portability Fit
|
||||
|
||||
- **Shared provider/platform boundary touched?**: no new provider seam
|
||||
- **Provider-owned seams**: N/A
|
||||
- **Platform-core seams**: output readiness, customer-safe/internal-only/blocked guidance vocabulary
|
||||
- **Neutral platform terms / contracts preserved**: review pack, evidence basis, limitation, customer-safe, internal-only, next action
|
||||
- **Retained provider-specific semantics and why**: only where current review/evidence text already carries provider-backed content
|
||||
- **Bounded extraction or follow-up path**: none
|
||||
|
||||
## Current Repo Truth Summary
|
||||
|
||||
- `App\Support\ReviewPacks\ReviewPackOutputReadiness` already derives:
|
||||
- `readiness_state`
|
||||
- `customer_safe_state`
|
||||
- `primary_reason`
|
||||
- `primary_action`
|
||||
- `limitations`
|
||||
- `section_summary`
|
||||
- `CustomerReviewWorkspace` already folds that truth into:
|
||||
- `effectiveWorkspaceReadinessState()`
|
||||
- `workspaceReadinessLabel()`
|
||||
- `workspaceReadinessReason()`
|
||||
- `workspaceReadinessImpact()`
|
||||
- `workspaceReadinessActions()`
|
||||
- current decision card and proof-panel payloads
|
||||
- Current workspace gaps:
|
||||
- no grouped limitation list tied to one dominant blocker
|
||||
- no explicit technical-details disclosure contract for output guidance
|
||||
- existing primary/secondary action mapping is still spread across multiple helper methods
|
||||
- `EnvironmentReviewResource` / `ViewEnvironmentReview` already own the detail surface:
|
||||
- infolist sections for outcome summary, review, executive posture, and sections
|
||||
- customer-workspace mode that narrows header actions
|
||||
- no explicit summary block separating review publication/completeness from output-readiness/sharing state
|
||||
- Route truth is already stable and workspace/environment scoped; no new route family is needed
|
||||
- Existing page audit identity is `ui-006-customer-review-workspace.md`
|
||||
|
||||
## Implementation Approach
|
||||
|
||||
### Phase 0 - Repo Truth Gate
|
||||
|
||||
1. Re-read the prepared `spec.md`, `plan.md`, `tasks.md`, `repo-truth-map.md`, and `checklists/requirements.md` before runtime edits.
|
||||
2. Re-check current runtime truth in:
|
||||
- `apps/platform/app/Support/ReviewPacks/ReviewPackOutputReadiness.php`
|
||||
- `apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php`
|
||||
- `apps/platform/resources/views/filament/pages/reviews/customer-review-workspace.blade.php`
|
||||
- `apps/platform/app/Filament/Resources/EnvironmentReviewResource.php`
|
||||
- `apps/platform/app/Filament/Resources/EnvironmentReviewResource/Pages/ViewEnvironmentReview.php`
|
||||
3. Keep `specs/349-customer-review-workspace-output-resolution-guidance/repo-truth-map.md` current if runtime inspection reveals additional bounded truth.
|
||||
|
||||
### Phase 1 - Tests First
|
||||
|
||||
1. Add focused guidance tests before runtime refactor:
|
||||
- `apps/platform/tests/Feature/ReviewPack/Spec349ReviewPackResolutionGuidanceTest.php`
|
||||
- `apps/platform/tests/Feature/Filament/Spec349CustomerReviewWorkspaceOutputGuidanceTest.php`
|
||||
- `apps/platform/tests/Feature/EnvironmentReview/Spec349EnvironmentReviewOutputGuidanceTest.php`
|
||||
- `apps/platform/tests/Browser/Spec349OutputResolutionGuidanceSmokeTest.php`
|
||||
2. Lock the following before runtime changes:
|
||||
- one primary output state
|
||||
- one primary next action
|
||||
- grouped limitation disclosure
|
||||
- qualified download wording
|
||||
- explicit PII/internal-only warning
|
||||
- collapsed technical details by default
|
||||
- review detail separation of status dimensions
|
||||
3. Reuse current review/evidence/review-pack fixtures; do not widen default helper cost.
|
||||
|
||||
### Phase 2 - Bounded Guidance Adapter
|
||||
|
||||
1. Choose the narrowest implementation home:
|
||||
- extend `ReviewPackOutputReadiness` with derived presentation fields only if raw truth remains legible, or
|
||||
- add a small `ReviewPackOutputResolutionGuidance` companion under `app/Support/ReviewPacks/`
|
||||
2. Keep the guidance layer derived-only:
|
||||
- display state
|
||||
- label
|
||||
- severity
|
||||
- primary reason
|
||||
- impact
|
||||
- primary action
|
||||
- grouped limitations
|
||||
- secondary actions
|
||||
- technical-details payload
|
||||
3. If additional display states such as `publication_blocked` are needed, keep them presentation-only and map them from current limitation codes or publish-blocker truth.
|
||||
|
||||
### Phase 3 - Action Mapping And Copy
|
||||
|
||||
1. Map limitation codes to plain-language guidance:
|
||||
- evidence basis incomplete -> open evidence basis
|
||||
- required sections incomplete -> review section limitations
|
||||
- mapping/control limitations -> review unmapped evidence or control interpretation
|
||||
- publish blockers -> resolve review blockers
|
||||
- contains PII -> review package contents / PII state
|
||||
- export not ready -> review output limitations
|
||||
2. Prefer existing route helpers and scoped resource URLs.
|
||||
3. Keep button and notification vocabulary aligned to current localization patterns: `Verb + Object`, conservative sharing language, no false-ready wording.
|
||||
|
||||
### Phase 4 - Customer Review Workspace Update
|
||||
|
||||
1. Update `CustomerReviewWorkspace` payload building to consume the bounded guidance object instead of scattered reason/action logic.
|
||||
2. Update the Blade view to show:
|
||||
- one output-guidance label
|
||||
- one primary reason
|
||||
- one impact statement
|
||||
- one primary action
|
||||
- compact grouped limitations
|
||||
- qualified secondary download/action wording
|
||||
- collapsed technical details
|
||||
3. Preserve current acknowledgement, accepted-risk, findings, and proof sections unless a minimal copy/order change is required to support the one-blocker hierarchy.
|
||||
|
||||
### Phase 5 - Environment Review Detail Update
|
||||
|
||||
1. Update `EnvironmentReviewResource` / `ViewEnvironmentReview` so the detail surface clearly separates:
|
||||
- review status
|
||||
- output readiness
|
||||
- publication/sharing state
|
||||
2. Keep fingerprint and raw proof detail secondary or hidden in customer-workspace mode.
|
||||
3. Preserve customer-workspace-mode access, download safety, and current lifecycle-action behavior outside that mode.
|
||||
|
||||
### Phase 6 - Localization, Audit, And Browser Proof
|
||||
|
||||
1. Update only the required output-guidance keys in:
|
||||
- `apps/platform/lang/en/localization.php`
|
||||
- `apps/platform/lang/de/localization.php`
|
||||
2. Update `docs/ui-ux-enterprise-audit/page-reports/ui-006-customer-review-workspace.md` with the new guidance model, one-primary-action rule, grouped limitation behavior, and repo-truth note about the missing `ui-009-review-pack-output-contract.md`.
|
||||
3. Capture screenshots under `specs/349-customer-review-workspace-output-resolution-guidance/artifacts/screenshots/`.
|
||||
|
||||
### Phase 7 - Validation And Close-Out
|
||||
|
||||
1. Run focused Feature tests for the new guidance layer and the current Spec 347 regressions.
|
||||
2. Run the bounded Browser smoke for representative states.
|
||||
3. Run `pint --dirty` and `git diff --check`.
|
||||
4. Record any unrelated failures separately without widening scope.
|
||||
|
||||
## Validation Plan
|
||||
|
||||
```bash
|
||||
cd apps/platform
|
||||
./vendor/bin/sail artisan test tests/Feature/ReviewPack/Spec349ReviewPackResolutionGuidanceTest.php tests/Feature/Filament/Spec349CustomerReviewWorkspaceOutputGuidanceTest.php tests/Feature/EnvironmentReview/Spec349EnvironmentReviewOutputGuidanceTest.php --compact
|
||||
./vendor/bin/sail php vendor/bin/pest tests/Browser/Spec349OutputResolutionGuidanceSmokeTest.php --compact
|
||||
./vendor/bin/sail artisan test --compact --filter=Spec347
|
||||
./vendor/bin/sail artisan test --compact --filter=CustomerReviewWorkspace
|
||||
./vendor/bin/sail artisan test --compact --filter=ReviewPack
|
||||
./vendor/bin/sail pint --dirty
|
||||
git diff --check
|
||||
```
|
||||
|
||||
## 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 change
|
||||
@ -0,0 +1,82 @@
|
||||
# Spec 349 - Repo Truth Map
|
||||
|
||||
Status: implemented
|
||||
Created: 2026-06-03
|
||||
Scope: Customer Review Workspace output resolution guidance and Environment Review detail output-guidance separation
|
||||
|
||||
This map records the repo-backed truth that Spec 349 is allowed to harden. It must be updated if runtime inspection during implementation reveals a narrower or broader bounded truth boundary.
|
||||
|
||||
## Implementation Update
|
||||
|
||||
- Shared derived guidance now lives in `apps/platform/app/Support/ReviewPacks/ReviewPackOutputResolutionGuidance.php`.
|
||||
- Workspace top-card rendering consumes that shared mapping in `CustomerReviewWorkspace.php` and `customer-review-workspace.blade.php`.
|
||||
- Environment Review detail consumes the same mapping through `EnvironmentReviewResource::outputGuidanceState()` and `filament/infolists/entries/review-pack-output-guidance.blade.php`.
|
||||
|
||||
## Classification Vocabulary
|
||||
|
||||
- `repo-verified`: directly observed in runtime code, tests, routes, or completed adjacent specs
|
||||
- `derived from existing truth`: can be computed safely from current models or payloads
|
||||
- `gap`: no current operator-guidance contract exists even though raw truth exists
|
||||
- `deferred`: intentionally out of scope for Spec 349
|
||||
|
||||
## Current Output-Readiness Truth
|
||||
|
||||
| Data point | Classification | Repo evidence | Spec 349 handling |
|
||||
|---|---|---|---|
|
||||
| `ReviewPackOutputReadiness` exists | repo-verified | `apps/platform/app/Support/ReviewPacks/ReviewPackOutputReadiness.php` | Reuse as the raw readiness source |
|
||||
| Current readiness states are `customer_safe_ready`, `published_with_limitations`, `internal_review_package_available`, `export_not_ready` | repo-verified | same class constants | Reuse; any extra display state must remain derived-only |
|
||||
| Current limitation codes include `export_not_ready`, evidence-basis codes, `required_sections_incomplete`, `publish_blockers_present`, `contains_pii`, and `disclosure_missing` | repo-verified | same class | Reuse as source for grouped guidance |
|
||||
| Current readiness already exposes `primary_reason`, `primary_action`, `limitations`, and `section_summary` | repo-verified | same class return payload | Prefer reuse before adding new fields |
|
||||
| Current readiness is derived-only and not persisted | repo-verified | same class plus current data model | Preserve; no new persisted truth |
|
||||
|
||||
## Current Customer Review Workspace Truth
|
||||
|
||||
| Data point | Classification | Repo evidence | Spec 349 handling |
|
||||
|---|---|---|---|
|
||||
| Strategic decision card already exists | repo-verified | `apps/platform/resources/views/filament/pages/reviews/customer-review-workspace.blade.php` | Keep as the first decision surface |
|
||||
| Workspace already derives label, reason, impact, primary action, and secondary action from readiness truth | repo-verified | `workspaceReadinessLabel()`, `workspaceReadinessReason()`, `workspaceReadinessImpact()`, `workspaceReadinessActions()` in `CustomerReviewWorkspace.php` | Consolidate into calmer grouped guidance |
|
||||
| Effective state already folds finding follow-up and accepted-risk follow-up into `published_with_limitations` | repo-verified | `effectiveWorkspaceReadinessState()` | Preserve honest customer-safe logic |
|
||||
| Workspace already has proof/detail panels and diagnostics sections | repo-verified | Blade view + page payload methods | Keep proof secondary; do not redesign the page broadly |
|
||||
| Workspace does not yet expose a compact grouped limitation list tied to one dominant blocker | gap | current decision card and proof panel structure | Primary gap to close |
|
||||
| Workspace does not yet expose a dedicated technical-details disclosure contract for output guidance | gap | current payload/view structure | Add bounded disclosure only |
|
||||
|
||||
## Current Environment Review Detail Truth
|
||||
|
||||
| Data point | Classification | Repo evidence | Spec 349 handling |
|
||||
|---|---|---|---|
|
||||
| Detail surface is Filament infolist-driven | repo-verified | `EnvironmentReviewResource::infolist()` | Do not invent a custom detail page unless repo truth later forces it |
|
||||
| Customer-workspace mode narrows header actions on review detail | repo-verified | `ViewEnvironmentReview::getHeaderActions()` | Preserve current access/handoff behavior |
|
||||
| Detail surface already shows artifact truth, review fields, executive posture, and sections | repo-verified | `EnvironmentReviewResource::infolist()` | Reorder or refine only as needed for clearer status separation |
|
||||
| Detail surface does not yet expose an explicit "review status vs output readiness vs sharing/publication state" summary | gap | current infolist sections and labels | Primary gap to close |
|
||||
|
||||
## Current Route And Scope Truth
|
||||
|
||||
| Data point | Classification | Repo evidence | Spec 349 handling |
|
||||
|---|---|---|---|
|
||||
| Workspace route is `/admin/reviews/workspace` | repo-verified | route list | Keep unchanged |
|
||||
| Environment Review detail route is workspace/environment scoped | repo-verified | route list for `environment-reviews/{record}` | Keep unchanged |
|
||||
| Review Pack routes are workspace/environment scoped plus one signed download route | repo-verified | route list for `review-packs` and `admin/review-packs/{reviewPack}/download` | Keep unchanged |
|
||||
| Evidence overview and detail routes already exist | repo-verified | route list for `/admin/evidence/overview` and scoped evidence detail | Reuse for guidance links where needed |
|
||||
| Workspace surface uses explicit `environment_id` query filtering | repo-verified | `CustomerReviewWorkspace::environmentFilterUrl()` and current tests | Preserve; no hidden shell-state fallback |
|
||||
|
||||
## Current Documentation Truth
|
||||
|
||||
| Data point | Classification | Repo evidence | Spec 349 handling |
|
||||
|---|---|---|---|
|
||||
| Durable workspace page report exists as `ui-006-customer-review-workspace.md` | repo-verified | docs audit file | Update this report |
|
||||
| No `ui-009-review-pack-output-contract.md` exists | repo-verified | docs audit inventory; `ui-009` is Provider Connections | Record this user-draft conflict explicitly; do not invent or overwrite `ui-009` |
|
||||
|
||||
## Current Test Truth
|
||||
|
||||
| Test surface | Classification | Repo evidence | Spec 349 handling |
|
||||
|---|---|---|---|
|
||||
| Output-readiness contract and workspace wording already have Spec 347 coverage | repo-verified | `Spec347ReviewPackOutputContractTest.php`, `Spec347ReviewPackReadinessSemanticsTest.php`, `Spec347CustomerReviewWorkspaceOutputReadinessTest.php` | Reuse as regression base |
|
||||
| Customer Review Workspace already has broader page and pack-access tests | repo-verified | `CustomerReviewWorkspacePageTest.php`, `CustomerReviewWorkspacePackAccessTest.php` | Extend or reuse proportionally |
|
||||
| Environment Review detail already has UI contract coverage | repo-verified | `EnvironmentReviewUiContractTest.php` | Extend or reuse proportionally |
|
||||
|
||||
## Primary Repo-Truth Gaps To Close
|
||||
|
||||
1. No grouped output-resolution guidance object exists yet over the raw readiness truth.
|
||||
2. No one-primary-blocker + one-primary-action contract is enforced across workspace and detail surfaces.
|
||||
3. Review detail does not yet distinguish review publication/completeness from output-readiness/sharing truth clearly enough.
|
||||
4. The user-draft audit-doc target conflicts with existing repo numbering and must be corrected explicitly.
|
||||
@ -0,0 +1,394 @@
|
||||
# Feature Specification: Spec 349 - Customer Review Workspace Output Resolution Guidance
|
||||
|
||||
**Feature Branch**: `349-customer-review-workspace-output-resolution-guidance`
|
||||
**Created**: 2026-06-03
|
||||
**Status**: Draft
|
||||
**Type**: Platform productization / operator guidance / output-readiness resolution UX
|
||||
**Runtime posture**: Narrow runtime hardening over existing Review Pack output-readiness truth, Customer Review Workspace guidance, and Environment Review detail surfaces. No new persistence, no workflow engine, no portal, and no PDF/HTML renderer.
|
||||
**Input**: User-provided full Spec 349 draft + repo truth from current Customer Review Workspace, Environment Review detail, Review Pack routes, and completed Spec 347 output-readiness work.
|
||||
|
||||
## Dependencies And Historical Context
|
||||
|
||||
This spec is a bounded follow-up over already repo-real review, evidence, and customer-safe productization work:
|
||||
|
||||
- Spec 258 - Customer Review Workspace Productization
|
||||
- Spec 308 - Decision Register Summary / Review Pack Inclusion
|
||||
- Spec 311 - Workspace / Environment Surface Scope Contract
|
||||
- Spec 326 - Customer Review Workspace v1 Productization
|
||||
- Spec 342 - Customer Review Workspace Final Consumption Productization
|
||||
- Spec 343 - Customer Review Attestation / Accepted Risk Lifecycle
|
||||
- Spec 344 - Customer Review Workspace Density / Audience Polish
|
||||
- Spec 347 - Review Pack Output Contract & Readiness Semantics
|
||||
|
||||
Repo-truth adjustments against the user draft:
|
||||
|
||||
- The runtime already has a bounded derived readiness layer in `App\Support\ReviewPacks\ReviewPackOutputReadiness`; Spec 349 should extend or adapt that truth rather than invent a second raw-readiness dialect.
|
||||
- `CustomerReviewWorkspace` already maps readiness into a primary label, reason, impact, and primary/secondary action, but it does not yet expose a grouped resolution-guidance object with one dominant blocker and compact limitation disclosure.
|
||||
- The review detail surface is Filament infolist-driven through `EnvironmentReviewResource` and `ViewEnvironmentReview.php`; there is no dedicated custom Blade detail page to redesign.
|
||||
- The existing durable audit report for this surface is `docs/ui-ux-enterprise-audit/page-reports/ui-006-customer-review-workspace.md`; the user-draft reference to `ui-009-review-pack-output-contract.md` conflicts with repo truth because `ui-009` is already Provider Connections.
|
||||
- Exact primary runtime routes are already repo-backed:
|
||||
- `/admin/reviews/workspace`
|
||||
- `/admin/workspaces/{workspace}/environments/{environment}/environment-reviews`
|
||||
- `/admin/workspaces/{workspace}/environments/{environment}/environment-reviews/{record}`
|
||||
- `/admin/workspaces/{workspace}/environments/{environment}/review-packs`
|
||||
- `/admin/workspaces/{workspace}/environments/{environment}/review-packs/{record}`
|
||||
- `/admin/review-packs/{reviewPack}/download`
|
||||
- `/admin/evidence/overview`
|
||||
- `/admin/workspaces/{workspace}/environments/{environment}/evidence/{record}`
|
||||
|
||||
## Spec Candidate Check *(mandatory - SPEC-GATE-001)*
|
||||
|
||||
- **Problem**: Spec 347 made Review Pack output truth more explicit, but the operator still has to translate readiness codes, limitation flags, and related proof surfaces into one next action.
|
||||
- **Today's failure**: Customer Review Workspace and Environment Review detail can already show qualified output states such as `Published with limitations`, but the operator still has to infer which limitation matters most, where to go next, and whether the package is customer-safe, internal-only, or simply blocked.
|
||||
- **User-visible improvement**: The product surfaces expose one clear output-guidance state, one dominant next action, a compact grouped limitation list, honest download wording, and collapsed technical details.
|
||||
- **Smallest enterprise-capable version**: Reuse the existing output-readiness truth and current surface routes, add one bounded guidance layer over it, update Customer Review Workspace and Review Detail disclosure, and add focused tests plus browser smoke.
|
||||
- **Explicit non-goals**: No new table, no persisted resolution records, no new queue family, no workflow engine, no portal, no PDF/HTML renderer, no PSA/ITSM handoff, no review-pack generator rewrite, no broad Governance Inbox redesign, no localization overhaul beyond touched output-guidance copy.
|
||||
- **Permanent complexity imported**: One repo-truth map, one requirements checklist, focused plan/tasks artifacts, and likely one bounded guidance presenter/helper or an extension of the existing readiness helper. No new persisted entity, no new public state machine, and no new panel or route family.
|
||||
- **Why now**: Spec 347 solved contract truth first. The next bottleneck is operator comprehension and calm decision-making on the current sellable review-consumption path.
|
||||
- **Why not local**: Copy-only tweaks would leave workspace and detail surfaces free to interpret the same readiness codes differently. This needs one bounded mapping from current readiness truth to operator guidance.
|
||||
- **Approval class**: Workflow Compression.
|
||||
- **Red flags triggered**: Strategic customer-safe surface, status/next-action semantics, and shared interaction-family reuse. Defense: the slice is explicitly derived-only, reuses current routes and readiness truth, and forbids new persistence or a workflow engine.
|
||||
- **Score**: Nutzen: 2 | Dringlichkeit: 2 | Scope: 2 | Komplexitaet: 1 | Produktnaehe: 2 | Wiederverwendung: 2 | **Gesamt: 11/12**
|
||||
- **Decision**: approve.
|
||||
|
||||
## Candidate Source And Completed-Spec Guardrail
|
||||
|
||||
- **Candidate source**:
|
||||
- direct user-provided Spec 349 draft
|
||||
- roadmap lane: Customer Review Workspace v1 Completion / customer-safe review consumption
|
||||
- spec-candidate alignment: bounded follow-up under `customer-review-workspace-v1-completion`
|
||||
- **Completed-spec guardrail result**:
|
||||
- no `specs/349-*` package existed before this prep
|
||||
- Specs 258, 308, 311, 326, 342, 343, 344, and 347 carry completed-task, close-out, validation, or historical implementation signals and are treated as context only
|
||||
- no completed spec is being reopened or normalized by this prep
|
||||
- **Close alternatives deferred**:
|
||||
- Localization v1 customer-facing surfaces
|
||||
- Decision-Based Governance Inbox v1
|
||||
- Provider readiness / onboarding productization
|
||||
- Review Pack PDF/HTML renderer and Customer Portal boundary work
|
||||
- **Smallest viable implementation slice**: existing output-readiness truth plus Customer Review Workspace and Environment Review detail guidance only: one dominant blocker, one primary action, grouped limitations, qualified download labels, collapsed technical details, and focused validation.
|
||||
|
||||
## Spec Scope Fields *(mandatory)*
|
||||
|
||||
- **Scope**: workspace canonical-view plus environment-owned review/evidence/review-pack artifact surfaces.
|
||||
- **Primary Routes**:
|
||||
- `/admin/reviews/workspace`
|
||||
- `/admin/workspaces/{workspace}/environments/{environment}/environment-reviews/{record}`
|
||||
- `/admin/workspaces/{workspace}/environments/{environment}/review-packs/{record}`
|
||||
- `/admin/review-packs/{reviewPack}/download`
|
||||
- `/admin/evidence/overview`
|
||||
- `/admin/workspaces/{workspace}/environments/{environment}/evidence/{record}`
|
||||
- **Data Ownership**:
|
||||
- `EnvironmentReview` remains review publication/completeness truth
|
||||
- `EnvironmentReviewSection` remains section truth
|
||||
- `EvidenceSnapshot` remains evidence-basis truth
|
||||
- `ReviewPack` remains export artifact truth
|
||||
- `ReviewPackOutputReadiness` remains derived output-readiness truth
|
||||
- any new guidance state remains derived presentation only; no new persisted entity is introduced
|
||||
- **RBAC**:
|
||||
- existing workspace membership and managed-environment entitlement remain mandatory
|
||||
- existing capabilities remain authoritative, especially `ENVIRONMENT_REVIEW_VIEW`, `REVIEW_PACK_VIEW`, and `EVIDENCE_VIEW`
|
||||
- non-members and cross-workspace or cross-environment access remain deny-as-not-found
|
||||
- no new public route family, query alias, or hidden shell-context fallback may be introduced
|
||||
|
||||
For canonical-view specs:
|
||||
|
||||
- **Default filter behavior when tenant-context is active**: keep `environment_id` as the explicit page-local filter on `/admin/reviews/workspace`; do not revive hidden shell/session environment fallback.
|
||||
- **Explicit entitlement checks preventing cross-tenant leakage**: workspace, environment-review, review-pack, download, and evidence links must continue to resolve only through current scoped routes, policies, and signed-download checks.
|
||||
|
||||
## 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 (`ViewEnvironmentReview` / `EnvironmentReviewResource` infolist)
|
||||
- Review Pack download wording as reached from those surfaces
|
||||
- **Current or new page archetype**: existing strategic customer-safe review surface plus existing detail/proof surfaces; no new route archetype.
|
||||
- **Design depth**: Strategic Surface for `CustomerReviewWorkspace`; Domain Pattern Surface for Environment Review detail.
|
||||
- **Repo-truth level**: repo-verified existing runtime surface plus repo-verified completed Spec 347 readiness truth.
|
||||
- **Existing pattern reused**: current decision card, current readiness proof panel, current detail infolist, current diagnostics/disclosure collapse pattern.
|
||||
- **New pattern required**: one bounded output-resolution guidance adapter over current readiness truth; no new cross-domain UX framework.
|
||||
- **Screenshot required**: yes, under `specs/349-customer-review-workspace-output-resolution-guidance/artifacts/screenshots/`.
|
||||
- **Page audit required**: update `docs/ui-ux-enterprise-audit/page-reports/ui-006-customer-review-workspace.md`. If implementation later proves a second durable page report is needed for detail/proof coverage, it must use the next repo-real identity instead of overwriting `ui-009`.
|
||||
- **Customer-safe review required**: yes. This spec directly governs customer-safe, internal-only, limited, and blocked output messaging.
|
||||
- **Dangerous-action review required**: no new destructive action is added. Existing acknowledgement, publish, regenerate, and download safety remain authoritative.
|
||||
- **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`
|
||||
- [ ] `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)*
|
||||
|
||||
- **Cross-cutting feature?**: yes
|
||||
- **Interaction class(es)**: status messaging, next-action guidance, action links, proof surfaces, customer-safe disclosure, download wording.
|
||||
- **Systems touched**:
|
||||
- `App\Support\ReviewPacks\ReviewPackOutputReadiness`
|
||||
- `App\Filament\Pages\Reviews\CustomerReviewWorkspace`
|
||||
- `App\Filament\Resources\EnvironmentReviewResource`
|
||||
- `App\Filament\Resources\EnvironmentReviewResource\Pages\ViewEnvironmentReview`
|
||||
- existing review-pack and evidence link helpers/routes
|
||||
- **Existing pattern(s) to extend**:
|
||||
- current output-readiness derivation
|
||||
- current workspace decision card and proof panel
|
||||
- current Environment Review artifact-truth and executive-posture sections
|
||||
- **Shared contract / presenter / builder / renderer to reuse**: current `ReviewPackOutputReadiness` output, existing workspace link helpers, and current detail truth presentation before adding any new formatter.
|
||||
- **Why the existing shared path is sufficient or insufficient**: current readiness truth is sufficient as the raw source, but it is not yet shaped into one grouped operator-guidance object that multiple surfaces can consume without copy drift.
|
||||
- **Allowed deviation and why**: one bounded `ReviewPackOutputResolutionGuidance`-style companion or equivalent page-local formatter is allowed if extending `ReviewPackOutputReadiness` directly would blur raw truth and UI guidance too far.
|
||||
- **Consistency impact**: limitation code -> label/reason/impact/action mapping must stay consistent across workspace, review detail, and qualified download wording.
|
||||
- **Review focus**: block any second readiness dialect, any persisted resolution entity, or any generic workflow engine disguised as guidance.
|
||||
|
||||
## OperationRun UX Impact *(mandatory)*
|
||||
|
||||
- **Touches OperationRun start/completion/link UX?**: existing proof-link and artifact handoff only
|
||||
- **Shared OperationRun UX contract/layer reused**: existing operation proof links and current review-pack generation lifecycle
|
||||
- **Delegated start/completion UX behaviors**: unchanged
|
||||
- **Local surface-owned behavior that remains**: blocker ranking, grouped limitation disclosure, and qualified next-action wording
|
||||
- **Queued DB-notification policy**: unchanged
|
||||
- **Terminal notification path**: unchanged
|
||||
- **Exception required?**: none
|
||||
|
||||
## Provider Boundary / Platform Core Check *(mandatory)*
|
||||
|
||||
- **Shared provider/platform boundary touched?**: no new provider seam
|
||||
- **Boundary classification**: platform-core output guidance over existing review/evidence/export truth
|
||||
- **Seams affected**: review/output vocabulary only
|
||||
- **Neutral platform terms preserved or introduced**: review pack, evidence basis, output readiness, publication blocked, internal-only, customer-safe, limitation, next action
|
||||
- **Provider-specific semantics retained and why**: only where existing review/evidence content already contains provider-backed wording
|
||||
- **Why this does not deepen provider coupling accidentally**: no Graph, provider contract, identifier, or platform-core taxonomy change is introduced
|
||||
- **Follow-up path**: none
|
||||
|
||||
## UI / Surface Guardrail Impact
|
||||
|
||||
| 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 decision card and limitation summary | yes | Native Filament page plus existing Blade composition | readiness, next action, disclosure, download wording | page, URL-query, derived payload | no | Existing route only |
|
||||
| Customer Review Workspace review-pack proof panel | yes | Native Filament page plus existing Blade composition | proof details, evidence basis, section completeness, PII visibility | page payload | no | Derived only |
|
||||
| Environment Review detail summary and posture sections | yes | Native Filament resource/infolist | review status vs output readiness vs sharing state | detail payload | no | Existing detail route only |
|
||||
| Review Pack detail/download handoff wording | no by default | existing Filament resource/detail | artifact handoff wording only if contradiction fix becomes unavoidable | none unless repo truth forces a minimal consistency patch | yes if unexpectedly touched | keep out of scope unless a direct contradiction is proven |
|
||||
|
||||
## Decision-First Surface Role
|
||||
|
||||
| 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 | Operator decides whether the current package is customer-safe, limited, internal-only, or blocked | one status, one reason, one next action, limitation count, qualified download state | detailed limitation list, evidence link, review detail, operation proof | Primary because it is the first customer-safe consumption screen | follows review handoff workflow | removes interpretation work across multiple panels and labels |
|
||||
| Environment Review detail | Secondary Context | Operator verifies why the review output is blocked or limited before acting | review status, output readiness, publication/sharing state, next action | sections, evidence, review-pack proof, raw artifact truth | Secondary because it supports the first-screen decision | follows inspect-and-resolve workflow | keeps proof secondary and avoids another equal-weight warning wall |
|
||||
| Review Pack detail/download | Secondary Context | Operator verifies artifact truth and authorized download path | artifact availability and qualified download meaning | file metadata, operation proof, download audit | Secondary because it is artifact proof, not the first decision surface | supports export verification | prevents detail-only semantics from leaking into the primary screen |
|
||||
|
||||
## Audience-Aware Disclosure
|
||||
|
||||
| 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 |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| Customer Review Workspace | operator-MSP, customer-safe review consumer, support where authorized | output status, primary blocker, impact, next action, limitation count, qualified download label | limitation list, section summary, evidence basis state, PII/redaction notes | raw payloads, fingerprints, lower-level proof details | one primary resolution/download action | technical details collapsed or secondary | top card states the blocker once; lower sections add evidence only |
|
||||
| Environment Review detail | operator-MSP, support where authorized | review status, output readiness, publication/sharing state, next action | section counts, publish blockers, evidence basis, current export truth | raw metadata, fingerprints, support-only proof details | open the primary blocker destination | raw/support detail stays secondary and capability-scoped | detail view distinguishes status dimensions instead of restating them ambiguously |
|
||||
|
||||
## UI/UX Surface Classification
|
||||
|
||||
| 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 | Read-only strategic review hub | resolve the highest-priority blocker or download the qualified pack | explicit primary action in the decision card | forbidden | secondary links remain visually secondary | none in scope | `/admin/reviews/workspace` | existing review/evidence/pack routes only | workspace shell plus explicit `environment_id` filter | Customer Review Workspace | one output state, one blocker, one next action | none |
|
||||
| Environment Review detail | Detail / Governance Artifact Detail | Read-only review detail | inspect the output blocker and open the linked proof surface | existing detail route | current repo-real behavior only | contextual detail links and secondary proof stay inside sections | existing lifecycle actions remain outside customer-workspace mode | `/admin/workspaces/{workspace}/environments/{environment}/environment-reviews` | `/admin/workspaces/{workspace}/environments/{environment}/environment-reviews/{record}` | workspace + environment route scope | Review detail | review status, output readiness, sharing state | none |
|
||||
|
||||
## Operator Surface Contract
|
||||
|
||||
| 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 |
|
||||
|---|---|---|---|---|---|---|---|---|---|---|
|
||||
| Customer Review Workspace | MSP/workspace operator | decide whether current output can be shared and what to resolve first | workspace review hub | Can I use or share this package, and what do I do next? | output status, blocker, impact, next action, limitation count, qualified download state | limitation details, section/evidence proof, operation proof | review publication, review completeness, output readiness, customer-safe boundary | none by default | review/open/download qualified package | none in scope |
|
||||
| Environment Review detail | MSP/operator reviewer | inspect why a review is blocked or limited and open the right supporting context | read-only detail | Why is this review not customer-ready, and which proof surface explains it? | review status, output readiness, sharing/publication state, next action | sections, evidence, artifact proof, fingerprint and raw metadata | review publication, completeness, output readiness, artifact availability | existing lifecycle mutations remain unchanged and out of customer-workspace mode | open proof/evidence/review-pack links | existing publish/archive/export actions remain unchanged and already confirmation-gated where applicable |
|
||||
|
||||
## Proportionality Review *(mandatory when structural complexity is introduced)*
|
||||
|
||||
- **New source of truth?**: no
|
||||
- **New persisted entity/table/artifact?**: no
|
||||
- **New abstraction?**: maybe one bounded presentation adapter over current readiness truth
|
||||
- **New enum/state/reason family?**: no persisted family; any added `publication_blocked` or similar state must remain derived presentation only
|
||||
- **New cross-domain UI framework/taxonomy?**: no
|
||||
- **Current operator problem**: the runtime knows why output is limited, but the operator still has to assemble the right conclusion and next action manually.
|
||||
- **Existing structure is insufficient because**: current readiness truth is optimized for contract/data correctness, not for grouped operator guidance across workspace and detail surfaces.
|
||||
- **Narrowest correct implementation**: reuse the current readiness helper, derive one compact guidance object, and update only the current workspace/detail surfaces plus wording.
|
||||
- **Ownership cost**: one bounded helper or formatter, focused tests, browser smoke, and one page-report update.
|
||||
- **Alternative intentionally rejected**: new workflow engine, persisted resolution items, portal, or broad cross-domain guidance framework.
|
||||
- **Release truth**: current-release truth.
|
||||
|
||||
### Compatibility posture
|
||||
|
||||
This feature assumes a pre-production environment.
|
||||
|
||||
Backward compatibility shims, legacy route aliases, old audit-report renumbering, and compatibility-specific UI states are out of scope unless repo truth later proves they are required.
|
||||
|
||||
## Testing / Lane / Runtime Impact *(mandatory for runtime behavior changes)*
|
||||
|
||||
- **Test purpose / classification**: Feature, Browser
|
||||
- **Validation lane(s)**: confidence, browser
|
||||
- **Why this classification and these lanes are sufficient**: the change is a deterministic mapping and disclosure hardening over existing server-rendered surfaces. Focused Feature tests can prove grouped limitation behavior, one-primary-action rules, qualified download labels, and detail-surface separation. One bounded Browser smoke is required because this is a strategic customer-safe first-screen surface.
|
||||
- **New or expanded test families**:
|
||||
- `apps/platform/tests/Feature/ReviewPack/Spec349ReviewPackResolutionGuidanceTest.php`
|
||||
- `apps/platform/tests/Feature/Filament/Spec349CustomerReviewWorkspaceOutputGuidanceTest.php`
|
||||
- `apps/platform/tests/Feature/EnvironmentReview/Spec349EnvironmentReviewOutputGuidanceTest.php`
|
||||
- `apps/platform/tests/Browser/Spec349OutputResolutionGuidanceSmokeTest.php`
|
||||
- **Relevant existing regressions to rerun**:
|
||||
- `apps/platform/tests/Feature/ReviewPack/Spec347ReviewPackOutputContractTest.php`
|
||||
- `apps/platform/tests/Feature/ReviewPack/Spec347ReviewPackReadinessSemanticsTest.php`
|
||||
- `apps/platform/tests/Feature/Filament/Spec347CustomerReviewWorkspaceOutputReadinessTest.php`
|
||||
- `apps/platform/tests/Feature/Filament/Spec342CustomerReviewWorkspaceConsumptionTest.php`
|
||||
- `apps/platform/tests/Feature/Reviews/CustomerReviewWorkspacePageTest.php`
|
||||
- `apps/platform/tests/Feature/Reviews/CustomerReviewWorkspacePackAccessTest.php`
|
||||
- `apps/platform/tests/Feature/EnvironmentReview/EnvironmentReviewUiContractTest.php`
|
||||
- **Fixture / helper cost impact**: reuse current review/evidence/review-pack helpers and avoid widening default browser or workspace fixtures.
|
||||
- **Heavy-family visibility / justification**: one explicit browser smoke only
|
||||
- **Special surface test profile**: `global-context-shell` + `shared-detail-family` + strategic customer-safe review surface
|
||||
- **Standard-native relief or required special coverage**: special coverage required for no-false-share wording, one-primary-action discipline, grouped limitations, and detail-surface status separation
|
||||
- **Reviewer handoff**: reviewers must confirm no new persistence, no second readiness dialect, no weakened signed-download behavior, and no equal-weight warning wall on the workspace/detail surfaces
|
||||
- **Budget / baseline / trend impact**: none expected beyond one explicit browser smoke addition
|
||||
- **Escalation needed**: `document-in-feature` if a minor detail-surface contradiction must be recorded; `follow-up-spec` only if repo truth reveals a broader output-guidance framework need
|
||||
- **Active feature PR close-out entry**: Guardrail / Smoke Coverage
|
||||
- **Planned validation commands**:
|
||||
|
||||
```bash
|
||||
cd apps/platform
|
||||
./vendor/bin/sail artisan test tests/Feature/ReviewPack/Spec349ReviewPackResolutionGuidanceTest.php tests/Feature/Filament/Spec349CustomerReviewWorkspaceOutputGuidanceTest.php tests/Feature/EnvironmentReview/Spec349EnvironmentReviewOutputGuidanceTest.php --compact
|
||||
./vendor/bin/sail php vendor/bin/pest tests/Browser/Spec349OutputResolutionGuidanceSmokeTest.php --compact
|
||||
./vendor/bin/sail artisan test --compact --filter=Spec347
|
||||
./vendor/bin/sail artisan test --compact --filter=CustomerReviewWorkspace
|
||||
./vendor/bin/sail artisan test --compact --filter=ReviewPack
|
||||
./vendor/bin/sail pint --dirty
|
||||
git diff --check
|
||||
```
|
||||
|
||||
## Summary
|
||||
|
||||
Spec 347 already established the output-readiness contract. Spec 349 must not reopen that truth. The bounded job here is to productize it:
|
||||
|
||||
- one primary output state
|
||||
- one dominant next action
|
||||
- grouped limitations
|
||||
- qualified download wording
|
||||
- explicit internal-only / customer-safe boundary
|
||||
- collapsed technical details
|
||||
- distinct review status vs output readiness vs sharing/publication state on the detail surface
|
||||
|
||||
The implementation should extend existing readiness truth and current routes only.
|
||||
|
||||
## User Scenarios & Testing *(mandatory)*
|
||||
|
||||
### User Story 1 - See one clear blocker on the workspace (Priority: P1)
|
||||
|
||||
As an MSP operator, I need the Customer Review Workspace to translate current output-readiness limitations into one dominant blocker and one next action so I can decide quickly whether the package is ready, limited, internal-only, or blocked.
|
||||
|
||||
**Why this priority**: This is the first customer-safe consumption surface and the main sellability gap after Spec 347.
|
||||
|
||||
**Independent Test**: Can be fully tested by loading the workspace with ready, limited, blocked, and internal-only fixtures and asserting one state, one primary action, and honest download wording.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** the current review output has publish blockers or missing evidence, **When** the workspace loads, **Then** it shows one dominant blocker, one primary next action, and grouped supporting limitations instead of many equal-weight warnings.
|
||||
2. **Given** the output is customer-safe and export-ready, **When** the workspace loads, **Then** it shows a customer-safe state with a qualified download action.
|
||||
|
||||
---
|
||||
|
||||
### User Story 2 - Inspect limitations without a warning wall (Priority: P1)
|
||||
|
||||
As an operator, I need limitation details and technical metadata available on demand without dominating the default screen so I can verify the proof without losing the first decision.
|
||||
|
||||
**Why this priority**: Spec 347 increased truth density; the next step is progressive disclosure, not more visible warnings.
|
||||
|
||||
**Independent Test**: Can be tested by asserting grouped limitation disclosure, collapsed technical details, and visible PII/internal-only warnings where applicable.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** multiple output limitations exist, **When** the workspace or review detail renders, **Then** the limitations appear as a compact grouped list or disclosure and not as a flat wall of badges or alerts.
|
||||
2. **Given** the output includes PII or internal-only context, **When** the surface renders, **Then** the operator sees that warning before any customer-safe share wording.
|
||||
|
||||
---
|
||||
|
||||
### User Story 3 - Distinguish review status from output readiness on detail (Priority: P2)
|
||||
|
||||
As an operator opening a released review, I need the detail page to separate review publication/completeness from output-readiness/sharing state so I do not mistake "published" for "customer-safe".
|
||||
|
||||
**Why this priority**: This is the main semantic trap left after Spec 347.
|
||||
|
||||
**Independent Test**: Can be tested by opening a published-but-limited review and asserting distinct labels/rows for review status, output readiness, and sharing/publication state.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** a review is published but has output blockers, **When** the detail page renders, **Then** publication status remains visible but separate from output-readiness and sharing-state messaging.
|
||||
2. **Given** the detail page is opened from Customer Review Workspace context, **When** the page renders, **Then** it preserves existing scoped access and handoff behavior while showing the clearer output-guidance separation.
|
||||
|
||||
## Functional Requirements
|
||||
|
||||
- **FR-349-001**: The product MUST derive output resolution guidance from existing review/output-readiness truth; no new persistence is allowed.
|
||||
- **FR-349-002**: Customer Review Workspace MUST show one dominant output-guidance state and one primary next action.
|
||||
- **FR-349-003**: Multiple limitations MUST be grouped into a compact list or disclosure instead of many equal-weight warnings.
|
||||
- **FR-349-004**: The primary blocker MUST map to one plain-language reason and one supporting impact statement.
|
||||
- **FR-349-005**: Qualified download wording MUST reflect whether the package is customer-safe, internal-only, limited, or not ready.
|
||||
- **FR-349-006**: PII/internal-only state MUST be visible before any customer-safe/share wording is shown.
|
||||
- **FR-349-007**: Technical details, section counts, evidence state, and related diagnostics MUST remain available but secondary.
|
||||
- **FR-349-008**: Environment Review detail MUST distinguish review status, output readiness, and publication/sharing state.
|
||||
- **FR-349-009**: Existing authorization, signed-download safety, acknowledgement behavior, and scope semantics MUST remain intact.
|
||||
- **FR-349-010**: The workspace must remain workspace-scoped with visible `environment_id` filtering and no hidden topbar context behavior.
|
||||
|
||||
## Non-Functional Requirements
|
||||
|
||||
- **NFR-349-001**: The default UI must reduce attention load by showing one dominant output state and one dominant next action.
|
||||
- **NFR-349-002**: Wording must remain operator-first, customer-safe, and conservative; when uncertain, the UI must prefer "requires review" over "customer-safe".
|
||||
- **NFR-349-003**: The guidance layer must not add unbounded queries and should reuse already-loaded summary metadata where practical.
|
||||
- **NFR-349-004**: Status, warning, and disclosure cues must remain accessible via text, keyboard-reachable controls, and non-color-only signaling.
|
||||
- **NFR-349-005**: Any new display state must remain derived-only and must not become a persisted domain state or workflow state machine.
|
||||
|
||||
## Explicit Non-Goals
|
||||
|
||||
- No new table, persisted record, queue family, or workflow engine
|
||||
- No Customer Portal or customer-facing route family
|
||||
- No Review Pack PDF or HTML renderer
|
||||
- No Review Pack generation rewrite
|
||||
- No change to EvidenceSnapshot generation or OperationRun lifecycle semantics
|
||||
- No broad Customer Review Workspace redesign beyond output guidance
|
||||
- No Governance Inbox scope expansion
|
||||
- No broad localization pass beyond touched output-guidance copy
|
||||
- No billing, entitlement, PSA, or provider-onboarding work
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- Customer Review Workspace shows one clear output-guidance state
|
||||
- Highest-priority blocker becomes one clear next action
|
||||
- Limitations are grouped and honest
|
||||
- Download labels do not overpromise customer-safe sharing
|
||||
- Review detail clearly separates review status from output readiness
|
||||
- Technical details remain available without dominating the screen
|
||||
- No new workflow engine or persisted resolution truth is introduced
|
||||
|
||||
## Risks
|
||||
|
||||
- **Guidance drift**: workspace and detail could interpret limitation codes differently if the mapping is not centralized.
|
||||
- **Scope creep**: detail-surface hardening could spill into broader review resource redesign if not kept bounded.
|
||||
- **False reassurance**: customer-safe wording could still overpromise if findings or accepted-risk follow-up are not folded into the effective state honestly.
|
||||
- **Warning-wall regression**: a naive implementation could surface every limitation at equal weight and defeat the operator-guidance goal.
|
||||
|
||||
## Assumptions
|
||||
|
||||
- Spec 347 readiness truth remains the raw source of limitation codes and section/evidence signals.
|
||||
- Existing scoped routes, signed-download behavior, and review/evidence helpers remain authoritative.
|
||||
- `ui-006-customer-review-workspace.md` remains the durable page-report identity for this workspace surface unless repo truth later proves otherwise.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- None blocking prep. Final label phrasing and whether the guidance adapter extends `ReviewPackOutputReadiness` directly or wraps it should be decided during implementation based on the narrowest code change.
|
||||
@ -0,0 +1,119 @@
|
||||
# Tasks: Spec 349 - Customer Review Workspace Output Resolution Guidance
|
||||
|
||||
**Input**: `specs/349-customer-review-workspace-output-resolution-guidance/spec.md`, `plan.md`, `repo-truth-map.md`, and `checklists/requirements.md`
|
||||
|
||||
**Tests**: Required. This is a runtime guidance and trust-surface change on existing review-pack, workspace, and review-detail paths.
|
||||
|
||||
## Test Governance Checklist
|
||||
|
||||
- [x] Lane assignment is explicit and narrow: Feature for mapping and surface behavior, 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 in unrelated lane cost.
|
||||
- [x] The declared surface profiles (`global-context-shell` and `shared-detail-family`) are explicit.
|
||||
- [x] Any derived-state expansion remains presentation-only and does not create a hidden new domain state family.
|
||||
|
||||
## Phase 1: Preparation And Repo Truth
|
||||
|
||||
**Purpose**: Keep the implementation bounded to existing readiness truth and current workspace/detail surfaces.
|
||||
|
||||
- [x] T001 Re-read `spec.md`, `plan.md`, `repo-truth-map.md`, and `checklists/requirements.md` before runtime changes.
|
||||
- [x] T002 Re-read related historical context only: Specs 258, 308, 311, 326, 342, 343, 344, and 347. Do not modify their artifacts.
|
||||
- [x] T003 Re-verify current runtime truth in:
|
||||
- `apps/platform/app/Support/ReviewPacks/ReviewPackOutputReadiness.php`
|
||||
- `apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php`
|
||||
- `apps/platform/resources/views/filament/pages/reviews/customer-review-workspace.blade.php`
|
||||
- `apps/platform/app/Filament/Resources/EnvironmentReviewResource.php`
|
||||
- `apps/platform/app/Filament/Resources/EnvironmentReviewResource/Pages/ViewEnvironmentReview.php`
|
||||
- [x] T004 Keep `specs/349-customer-review-workspace-output-resolution-guidance/repo-truth-map.md` updated if implementation-time inspection reveals a narrower or broader bounded truth.
|
||||
- [x] T005 Confirm no migration, package, env var, queue family, scheduler change, storage-topology change, panel/provider change, or global-search change is required.
|
||||
- [x] T006 Confirm Filament v5 / Livewire v4.0+ compliance and avoid legacy Filament or Livewire APIs.
|
||||
- [x] T007 Confirm panel provider registration remains `apps/platform/bootstrap/providers.php`.
|
||||
|
||||
## Phase 2: Tests First
|
||||
|
||||
**Purpose**: Lock the operator-guidance behavior before runtime refactor.
|
||||
|
||||
- [x] T008 Add `apps/platform/tests/Feature/ReviewPack/Spec349ReviewPackResolutionGuidanceTest.php`.
|
||||
- [x] T009 Add `apps/platform/tests/Feature/Filament/Spec349CustomerReviewWorkspaceOutputGuidanceTest.php`.
|
||||
- [x] T010 Add `apps/platform/tests/Feature/EnvironmentReview/Spec349EnvironmentReviewOutputGuidanceTest.php`.
|
||||
- [x] T011 Add `apps/platform/tests/Browser/Spec349OutputResolutionGuidanceSmokeTest.php`.
|
||||
- [x] T012 Add assertions that the workspace shows exactly one dominant output state and exactly one primary next action.
|
||||
- [x] T013 Add assertions that grouped limitations appear compactly and technical details stay collapsed/secondary by default.
|
||||
- [x] T014 Add assertions that PII/internal-only output shows an explicit warning before customer-safe wording.
|
||||
- [x] T015 Add assertions that download labels are qualified honestly for customer-safe, internal-only, limited, and not-ready states.
|
||||
- [x] T016 Add assertions that Environment Review detail separates review status, output readiness, and publication/sharing state.
|
||||
- [x] T017 Reuse or extend current regressions such as:
|
||||
- `apps/platform/tests/Feature/ReviewPack/Spec347ReviewPackOutputContractTest.php`
|
||||
- `apps/platform/tests/Feature/ReviewPack/Spec347ReviewPackReadinessSemanticsTest.php`
|
||||
- `apps/platform/tests/Feature/Filament/Spec347CustomerReviewWorkspaceOutputReadinessTest.php`
|
||||
- `apps/platform/tests/Feature/Filament/Spec342CustomerReviewWorkspaceConsumptionTest.php`
|
||||
- `apps/platform/tests/Feature/Reviews/CustomerReviewWorkspacePageTest.php`
|
||||
- `apps/platform/tests/Feature/Reviews/CustomerReviewWorkspacePackAccessTest.php`
|
||||
- `apps/platform/tests/Feature/EnvironmentReview/EnvironmentReviewUiContractTest.php`
|
||||
|
||||
## Phase 3: Guidance Mapping
|
||||
|
||||
**Purpose**: Build one bounded derived mapping from readiness truth to operator guidance.
|
||||
|
||||
- [x] T018 Choose the narrowest implementation home:
|
||||
- extend `apps/platform/app/Support/ReviewPacks/ReviewPackOutputReadiness.php`, or
|
||||
- add `apps/platform/app/Support/ReviewPacks/ReviewPackOutputResolutionGuidance.php`
|
||||
- [x] T019 Derive display state, label, severity, primary reason, impact, primary action, grouped limitations, secondary actions, and technical details from existing readiness truth only.
|
||||
- [x] T020 Keep any added states such as `publication_blocked` or `internal_only` presentation-only; do not create new persisted enums or workflow states.
|
||||
- [x] T021 Map existing limitation codes to plain-language operator guidance and repo-real destinations using current scoped route helpers.
|
||||
- [x] T022 Keep the mapping shared enough to prevent workspace/detail wording drift, but bounded enough to avoid a generic workflow-resolution framework.
|
||||
|
||||
## Phase 4: Customer Review Workspace Update
|
||||
|
||||
**Purpose**: Turn the current readiness truth into one calm first-screen decision.
|
||||
|
||||
- [x] T023 Update `apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php` to consume the bounded guidance mapping instead of scattered reason/action logic.
|
||||
- [x] T024 Update `apps/platform/resources/views/filament/pages/reviews/customer-review-workspace.blade.php` so the decision card shows one output state, one reason, one impact statement, and one primary action.
|
||||
- [x] T025 Add a compact grouped limitations disclosure with supporting actions and collapsed technical details.
|
||||
- [x] T026 Qualify review-pack download labels and surrounding copy without weakening existing signed-download safety or authorization.
|
||||
- [x] T027 Preserve current acknowledgement, findings, accepted-risk, and proof sections unless a minimal hierarchy adjustment is required to support the one-blocker rule.
|
||||
- [x] T028 Preserve the visible `environment_id` workspace filter contract and avoid reintroducing hidden topbar or shell-context behavior.
|
||||
- [x] T029 Ensure grouped limitation disclosure, details toggles, and status text remain text-backed, keyboard-reachable, and not color-only.
|
||||
|
||||
## Phase 5: Environment Review Detail Update
|
||||
|
||||
**Purpose**: Separate review publication truth from output-readiness truth on the detail surface.
|
||||
|
||||
- [x] T030 Update `apps/platform/app/Filament/Resources/EnvironmentReviewResource.php` infolist presentation if needed so the detail summary exposes distinct status dimensions cleanly.
|
||||
- [x] T031 Update `apps/platform/app/Filament/Resources/EnvironmentReviewResource/Pages/ViewEnvironmentReview.php` or the supporting infolist state so the detail surface shows review status, output readiness, and publication/sharing state separately.
|
||||
- [x] T032 Keep fingerprint, raw proof metadata, and support-only detail secondary or hidden in customer-workspace mode.
|
||||
- [x] T033 Preserve existing customer-workspace-mode access, header-action narrowing, and lifecycle-action behavior outside customer-workspace mode.
|
||||
|
||||
## Phase 6: Copy, Audit, And Browser Proof
|
||||
|
||||
**Purpose**: Align wording and audit artifacts with the bounded guidance model.
|
||||
|
||||
- [x] T034 Update only the required output-guidance localization keys in:
|
||||
- `apps/platform/lang/en/localization.php`
|
||||
- `apps/platform/lang/de/localization.php`
|
||||
- [x] T035 Update `docs/ui-ux-enterprise-audit/page-reports/ui-006-customer-review-workspace.md` with the one-primary-action rule, grouped limitation disclosure, and repo-truth note about the user-draft `ui-009` conflict.
|
||||
- [x] T036 If implementation proves a second durable page report is required, create it under the next repo-real identity instead of reusing `ui-009`.
|
||||
- [x] T037 Capture browser screenshots under `specs/349-customer-review-workspace-output-resolution-guidance/artifacts/screenshots/`.
|
||||
|
||||
## Phase 7: Validation
|
||||
|
||||
**Purpose**: Prove the guidance mapping and preserve existing safety.
|
||||
|
||||
- [x] T038 Run `cd apps/platform && ./vendor/bin/sail artisan test tests/Feature/ReviewPack/Spec349ReviewPackResolutionGuidanceTest.php tests/Feature/Filament/Spec349CustomerReviewWorkspaceOutputGuidanceTest.php tests/Feature/EnvironmentReview/Spec349EnvironmentReviewOutputGuidanceTest.php --compact`.
|
||||
- [x] T039 Run `cd apps/platform && ./vendor/bin/sail php vendor/bin/pest tests/Browser/Spec349OutputResolutionGuidanceSmokeTest.php --compact`.
|
||||
- [x] T040 Run `cd apps/platform && ./vendor/bin/sail artisan test --compact --filter=Spec347`.
|
||||
- [x] T041 Run `cd apps/platform && ./vendor/bin/sail artisan test --compact --filter=CustomerReviewWorkspace`.
|
||||
- [x] T042 Run `cd apps/platform && ./vendor/bin/sail artisan test --compact --filter=ReviewPack`.
|
||||
- [x] T043 Run `cd apps/platform && ./vendor/bin/sail pint --dirty`.
|
||||
- [x] T044 Run `git diff --check`.
|
||||
- [x] T045 Report any unrelated broader-suite failures honestly if they remain out of scope.
|
||||
|
||||
## Non-Goals Checklist
|
||||
|
||||
- [x] NT001 Do not create a new persisted resolution entity, table, or status family.
|
||||
- [x] NT002 Do not add a workflow engine, approval engine, or queue family.
|
||||
- [x] NT003 Do not build a Customer Portal or Review Pack PDF/HTML renderer.
|
||||
- [x] NT004 Do not rewrite Review Pack generation or reopen Spec 347 contract truth broadly.
|
||||
- [x] NT005 Do not redesign Governance Inbox or broadly redesign Customer Review Workspace outside the output-guidance slice.
|
||||
- [x] NT006 Do not weaken existing workspace/environment scope, policies, or signed-download safety.
|
||||
Loading…
Reference in New Issue
Block a user