feat: customer review workspace output resolution guidance (spec 349)
Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 3m42s

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.
This commit is contained in:
Ahmed Darrazi 2026-06-03 03:31:29 +02:00
parent 12ea7f9924
commit acdb205e1b
29 changed files with 2756 additions and 118 deletions

View File

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

View File

@ -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(),
]);
}
}

View File

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

View File

@ -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'),
];
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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()];
}

View File

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

View File

@ -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');
});

View File

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

View File

@ -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');
});

View File

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

View File

@ -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,
);
}

View File

@ -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');
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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