feat: productize governance inbox decision-first workbench
Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 1m42s

This commit is contained in:
Ahmed Darrazi 2026-05-18 18:13:00 +02:00
parent c8224843b3
commit b1962ece80
17 changed files with 2200 additions and 98 deletions

View File

@ -42,12 +42,17 @@ class GovernanceInbox extends Page
protected static ?int $navigationSort = 5;
protected static ?string $title = 'Governance inbox';
protected static ?string $title = 'Governance Inbox';
protected static ?string $slug = 'governance/inbox';
protected string $view = 'filament.pages.governance.governance-inbox';
public function getSubheading(): ?string
{
return 'Prioritized governance decisions, owners, evidence, and follow-up actions across entitled environments.';
}
/**
* @var array<int, ManagedEnvironment>|null
*/
@ -73,6 +78,11 @@ class GovernanceInbox extends Page
*/
private ?array $unfilteredInboxPayload = null;
/**
* @var array<string, mixed>|null
*/
private ?array $decisionWorkbench = null;
private ?Workspace $workspace = null;
private ?bool $visibleAlertsFamily = null;
@ -140,6 +150,40 @@ public function sections(): array
return $this->inboxPayload()['sections'] ?? [];
}
/**
* @return array{
* question: string,
* selected_item: array<string, mixed>|null,
* summary_cards: list<array{label: string, value: string, description: string}>,
* diagnostics: array{label: string, state: string, body: string}
* }
*/
public function decisionWorkbench(): array
{
if (is_array($this->decisionWorkbench)) {
return $this->decisionWorkbench;
}
$entries = $this->workbenchEntries();
$selectedItem = $entries
->sortBy([
fn (array $entry): int => (int) ($entry['urgency_rank'] ?? 999),
fn (array $entry): string => (string) ($entry['headline'] ?? ''),
])
->first();
return $this->decisionWorkbench = [
'question' => 'What decision clears the highest-priority item?',
'selected_item' => is_array($selectedItem) ? $this->normalizeWorkbenchItem($selectedItem) : null,
'summary_cards' => $this->summaryCards($entries),
'diagnostics' => [
'label' => 'Diagnostics',
'state' => 'Collapsed',
'body' => 'Source diagnostics and raw support details stay on authorized source surfaces. This workbench shows decision, evidence, and proof state only.',
],
];
}
/**
* @return array<string, mixed>
*/
@ -155,8 +199,8 @@ public function calmEmptyState(): array
}
return [
'title' => 'No visible governance attention right now',
'body' => 'The current workspace scope is calm across the visible governance families.',
'title' => 'No governance decisions need attention',
'body' => 'The current workspace scope has no repo-backed governance decisions requiring action.',
'action_label' => null,
'action_url' => null,
];
@ -201,6 +245,101 @@ public function navigationContext(): CanonicalNavigationContext
);
}
/**
* @return \Illuminate\Support\Collection<int, array<string, mixed>>
*/
private function workbenchEntries(): \Illuminate\Support\Collection
{
return collect($this->sections())
->flatMap(function (array $section): array {
$entries = is_array($section['entries'] ?? null) ? $section['entries'] : [];
return array_map(function (array $entry) use ($section): array {
$entry['section_key'] = (string) ($section['key'] ?? $entry['family_key'] ?? 'governance');
$entry['section_label'] = (string) ($section['label'] ?? 'Governance item');
return $entry;
}, $entries);
})
->values();
}
/**
* @param array<string, mixed> $item
* @return array<string, mixed>
*/
private function normalizeWorkbenchItem(array $item): array
{
return [
'section_label' => (string) ($item['section_label'] ?? 'Governance item'),
'environment_label' => filled($item['tenant_label'] ?? null) ? (string) $item['tenant_label'] : 'Workspace-wide',
'title' => (string) ($item['headline'] ?? 'Governance item'),
'status_label' => (string) ($item['status_label'] ?? 'Needs attention'),
'decision_label' => (string) ($item['decision_label'] ?? 'Review governance item'),
'reason_label' => (string) ($item['reason_label'] ?? 'Reason unavailable'),
'impact_label' => (string) ($item['impact_label'] ?? 'Impact unavailable'),
'owner_label' => (string) ($item['owner_label'] ?? 'Owner unavailable'),
'owner_state' => (string) ($item['owner_state'] ?? 'unavailable'),
'due_label' => (string) ($item['due_label'] ?? 'Due date unavailable'),
'due_state' => (string) ($item['due_state'] ?? 'unavailable'),
'evidence_label' => (string) ($item['evidence_label'] ?? 'Evidence unavailable'),
'evidence_state' => (string) ($item['evidence_state'] ?? 'unavailable'),
'evidence_path_label' => (string) ($item['evidence_path_label'] ?? 'Proof path unavailable'),
'evidence_path_url' => filled($item['evidence_path_url'] ?? null) ? (string) $item['evidence_path_url'] : null,
'exception_label' => (string) ($item['exception_label'] ?? 'Accepted-risk state unavailable'),
'exception_state' => (string) ($item['exception_state'] ?? 'unavailable'),
'primary_action_label' => (string) ($item['primary_action_label'] ?? 'Open source'),
'primary_action_url' => filled($item['primary_action_url'] ?? null)
? (string) $item['primary_action_url']
: (filled($item['destination_url'] ?? null) ? (string) $item['destination_url'] : null),
'source_url' => filled($item['destination_url'] ?? null) ? (string) $item['destination_url'] : null,
];
}
/**
* @param \Illuminate\Support\Collection<int, array<string, mixed>> $entries
* @return list<array{label: string, value: string, description: string}>
*/
private function summaryCards(\Illuminate\Support\Collection $entries): array
{
$totalCount = (int) ($this->inboxPayload()['total_count'] ?? 0);
$selectedSection = $entries
->sortBy([
fn (array $entry): int => (int) ($entry['urgency_rank'] ?? 999),
fn (array $entry): string => (string) ($entry['headline'] ?? ''),
])
->first()['section_label'] ?? 'None';
$ownerGaps = $entries
->filter(fn (array $entry): bool => in_array((string) ($entry['owner_state'] ?? ''), ['missing', 'unavailable'], true))
->count();
$evidenceGaps = $entries
->filter(fn (array $entry): bool => in_array((string) ($entry['evidence_state'] ?? ''), ['missing', 'unavailable'], true))
->count();
return [
[
'label' => 'Visible decisions',
'value' => (string) $totalCount,
'description' => 'Repo-backed attention items in the current scope.',
],
[
'label' => 'Priority family',
'value' => (string) $selectedSection,
'description' => 'Highest-ranked visible preview item.',
],
[
'label' => 'Owner gaps in preview',
'value' => (string) $ownerGaps,
'description' => 'Preview items with missing or unavailable ownership.',
],
[
'label' => 'Evidence gaps in preview',
'value' => (string) $evidenceGaps,
'description' => 'Preview items without linked proof in the workbench.',
],
];
}
private function authorizeWorkspaceMembership(): void
{
$user = auth()->user();

View File

@ -110,6 +110,10 @@ private static function resolveFromManifest(string $entry): ?string
return null;
}
if (app()->runningUnitTests() || app()->environment('testing')) {
return url('/build/'.$file);
}
return asset('build/'.$file);
}
}

View File

@ -568,7 +568,7 @@ private function assignedFindingsQuery(User $user, array $visibleFindingTenants,
: array_keys($visibleFindingTenants);
return Finding::query()
->with(['tenant', 'ownerUser:id,name', 'assigneeUser:id,name'])
->with(['tenant', 'ownerUser:id,name', 'assigneeUser:id,name', 'findingException'])
->withSubjectDisplayName()
->whereIn('managed_environment_id', $tenantIds === [] ? [-1] : $tenantIds)
->where('assignee_user_id', (int) $user->getKey())
@ -597,7 +597,7 @@ private function intakeFindingsQuery(array $visibleFindingTenants, ?ManagedEnvir
: array_keys($visibleFindingTenants);
return Finding::query()
->with(['tenant', 'ownerUser:id,name', 'assigneeUser:id,name'])
->with(['tenant', 'ownerUser:id,name', 'assigneeUser:id,name', 'findingException'])
->withSubjectDisplayName()
->whereIn('managed_environment_id', $tenantIds === [] ? [-1] : $tenantIds)
->whereNull('assignee_user_id')
@ -700,6 +700,8 @@ private function findingExceptionsQuery(Workspace $workspace, array $authorizedT
'tenant',
'requester:id,name',
'owner:id,name',
'currentDecision',
'evidenceReferences',
'finding' => fn ($query) => $query->withSubjectDisplayName(),
])
->where('workspace_id', (int) $workspace->getKey())
@ -769,6 +771,27 @@ private function findingEntry(Finding $finding, string $familyKey, ?CanonicalNav
FindingResource::getUrl('view', ['record' => $finding], tenant: $finding->tenant),
$navigationContext?->toQuery() ?? [],
),
'decision_label' => $familyKey === 'assigned_findings' ? 'Review assigned finding' : 'Triage intake finding',
'reason_label' => $this->findingReasonLabel($finding, $familyKey),
'impact_label' => $this->findingImpactLabel($finding),
'owner_label' => $this->findingOwnerLabel($finding),
'owner_state' => $finding->owner_user_id !== null ? 'available' : 'missing',
'due_label' => $this->findingDueLabel($finding),
'due_state' => $finding->due_at === null ? 'unavailable' : ($finding->due_at->isPast() ? 'overdue' : 'available'),
'evidence_label' => $this->findingEvidenceLabel($finding),
'evidence_state' => $this->findingEvidenceState($finding),
'evidence_path_label' => $this->findingEvidenceState($finding) === 'linked' ? 'Open finding evidence' : 'Source record only',
'evidence_path_url' => $this->appendQuery(
FindingResource::getUrl('view', ['record' => $finding], tenant: $finding->tenant),
$navigationContext?->toQuery() ?? [],
),
'exception_label' => $this->findingExceptionLabel($finding),
'exception_state' => $finding->findingException instanceof FindingException ? 'available' : 'none',
'primary_action_label' => $familyKey === 'assigned_findings' ? 'Review finding' : 'Triage finding',
'primary_action_url' => $this->appendQuery(
FindingResource::getUrl('view', ['record' => $finding], tenant: $finding->tenant),
$navigationContext?->toQuery() ?? [],
),
'back_label' => $navigationContext?->backLinkLabel ?? 'Back to governance inbox',
];
}
@ -795,6 +818,29 @@ private function operationEntry(OperationRun $run, ?CanonicalNavigationContext $
? 'Terminal follow-up'
: 'Stale',
'destination_url' => OperationRunLinks::tenantlessView($run, $navigationContext),
'decision_label' => $problemClass === OperationRun::PROBLEM_CLASS_TERMINAL_FOLLOW_UP
? 'Review terminal operation follow-up'
: 'Review stale operation',
'reason_label' => $problemClass === OperationRun::PROBLEM_CLASS_TERMINAL_FOLLOW_UP
? 'The operation reached a terminal outcome that still needs monitoring follow-up.'
: 'The active operation appears stale and needs operator attention.',
'impact_label' => $problemClass === OperationRun::PROBLEM_CLASS_TERMINAL_FOLLOW_UP
? 'Terminal operation follow-up'
: 'Stale active operation',
'owner_label' => 'Owner unavailable',
'owner_state' => 'unavailable',
'due_label' => $run->completed_at instanceof \DateTimeInterface
? 'Completed '.FindingExceptionResource::relativeTimeDescription($run->completed_at)
: 'Due date unavailable',
'due_state' => 'unavailable',
'evidence_label' => 'Operation proof available',
'evidence_state' => 'linked',
'evidence_path_label' => OperationRunLinks::identifier($run),
'evidence_path_url' => OperationRunLinks::tenantlessView($run, $navigationContext),
'exception_label' => 'No accepted-risk state',
'exception_state' => 'not_required',
'primary_action_label' => 'Open operation proof',
'primary_action_url' => OperationRunLinks::tenantlessView($run, $navigationContext),
'back_label' => $navigationContext?->backLinkLabel ?? 'Back to governance inbox',
];
}
@ -809,9 +855,6 @@ private function alertEntry(AlertDelivery $delivery, ?CanonicalNavigationContext
? (string) $payload['title']
: 'Failed alert delivery';
$sublineParts = array_values(array_filter([
is_string($delivery->last_error_message) && $delivery->last_error_message !== ''
? $delivery->last_error_message
: null,
is_string($delivery->event_type) && $delivery->event_type !== ''
? $delivery->event_type
: null,
@ -831,6 +874,29 @@ private function alertEntry(AlertDelivery $delivery, ?CanonicalNavigationContext
AlertDeliveryResource::getUrl('view', ['record' => $delivery], panel: 'admin'),
$navigationContext?->toQuery() ?? [],
),
'decision_label' => 'Review failed alert delivery',
'reason_label' => 'An alert delivery attempt failed; source diagnostics remain on the alert delivery record.',
'impact_label' => is_string($delivery->event_type) && $delivery->event_type !== ''
? 'Alert event: '.$delivery->event_type
: 'Alert delivery follow-up',
'owner_label' => 'Owner unavailable',
'owner_state' => 'unavailable',
'due_label' => 'Due date unavailable',
'due_state' => 'unavailable',
'evidence_label' => 'Evidence not required',
'evidence_state' => 'not_required',
'evidence_path_label' => 'Alert delivery record',
'evidence_path_url' => $this->appendQuery(
AlertDeliveryResource::getUrl('view', ['record' => $delivery], panel: 'admin'),
$navigationContext?->toQuery() ?? [],
),
'exception_label' => 'No accepted-risk state',
'exception_state' => 'not_required',
'primary_action_label' => 'Open alert delivery',
'primary_action_url' => $this->appendQuery(
AlertDeliveryResource::getUrl('view', ['record' => $delivery], panel: 'admin'),
$navigationContext?->toQuery() ?? [],
),
'back_label' => $navigationContext?->backLinkLabel ?? 'Back to governance inbox',
];
}
@ -877,6 +943,45 @@ private function findingExceptionEntry(FindingException $exception, ?CanonicalNa
),
$navigationContext?->toQuery() ?? [],
),
'decision_label' => 'Review accepted-risk decision',
'reason_label' => is_string($exception->request_reason) && trim($exception->request_reason) !== ''
? trim($exception->request_reason)
: 'This accepted-risk or exception record needs review.',
'impact_label' => $this->findingExceptionImpactLabel($exception),
'owner_label' => $exception->owner?->name ?? 'Owner missing',
'owner_state' => $exception->owner?->name !== null ? 'available' : 'missing',
'due_label' => $this->findingExceptionDueLabel($exception),
'due_state' => $exception->review_due_at === null && $exception->expires_at === null
? 'unavailable'
: (($exception->review_due_at?->isPast() === true || $exception->expires_at?->isPast() === true) ? 'overdue' : 'available'),
'evidence_label' => $this->findingExceptionEvidenceLabel($exception),
'evidence_state' => $this->findingExceptionEvidenceState($exception),
'evidence_path_label' => $this->findingExceptionEvidenceState($exception) === 'linked'
? 'Open exception proof'
: 'Source record only',
'evidence_path_url' => $this->appendQuery(
FindingExceptionsQueue::getUrl(
panel: 'admin',
parameters: array_filter([
'tenant' => $exception->tenant?->external_id,
'exception' => (int) $exception->getKey(),
], static fn (mixed $value): bool => $value !== null && $value !== ''),
),
$navigationContext?->toQuery() ?? [],
),
'exception_label' => $this->findingExceptionDecisionStateLabel($exception),
'exception_state' => 'available',
'primary_action_label' => 'Review accepted risk',
'primary_action_url' => $this->appendQuery(
FindingExceptionsQueue::getUrl(
panel: 'admin',
parameters: array_filter([
'tenant' => $exception->tenant?->external_id,
'exception' => (int) $exception->getKey(),
], static fn (mixed $value): bool => $value !== null && $value !== ''),
),
$navigationContext?->toQuery() ?? [],
),
'back_label' => $navigationContext?->backLinkLabel ?? 'Back to governance inbox',
];
}
@ -924,6 +1029,27 @@ private function reviewEntry(
? 'Follow-up needed'
: 'Changed since review',
'destination_url' => $this->appendQuery($destinationUrl, $navigationContext?->toQuery() ?? []),
'decision_label' => 'Review customer workspace follow-up',
'reason_label' => $state === ManagedEnvironmentTriageReview::STATE_FOLLOW_UP_NEEDED
? 'The latest review state asks for follow-up before this governance concern is closed.'
: 'This review concern changed since the latest reviewed state.',
'impact_label' => $familyLabel.' review follow-up',
'owner_label' => is_string($row['reviewed_by_user_name'] ?? null) && $row['reviewed_by_user_name'] !== ''
? 'Last reviewer: '.$row['reviewed_by_user_name']
: 'Owner unavailable',
'owner_state' => is_string($row['reviewed_by_user_name'] ?? null) && $row['reviewed_by_user_name'] !== ''
? 'available'
: 'unavailable',
'due_label' => 'Due date unavailable',
'due_state' => 'unavailable',
'evidence_label' => 'Review evidence path available',
'evidence_state' => 'linked',
'evidence_path_label' => 'Open review context',
'evidence_path_url' => $this->appendQuery($destinationUrl, $navigationContext?->toQuery() ?? []),
'exception_label' => 'Accepted-risk state unavailable',
'exception_state' => 'unavailable',
'primary_action_label' => 'Open review context',
'primary_action_url' => $this->appendQuery($destinationUrl, $navigationContext?->toQuery() ?? []),
'back_label' => $navigationContext?->backLinkLabel ?? 'Back to governance inbox',
];
}
@ -1042,6 +1168,148 @@ private function findingExceptionStatusLabel(FindingException $exception): strin
return Str::of((string) $exception->status)->replace('_', ' ')->title()->value();
}
private function findingReasonLabel(Finding $finding, string $familyKey): string
{
if ($finding->due_at?->isPast() === true) {
return 'The finding is overdue and still open in the current governance scope.';
}
if ($finding->reopened_at !== null) {
return 'The finding reopened after a previous resolution path.';
}
if ($familyKey === 'intake_findings') {
return 'The finding is unassigned and still needs first triage.';
}
return 'The finding remains assigned and needs follow-up before it can be cleared.';
}
private function findingImpactLabel(Finding $finding): string
{
$severity = is_string($finding->severity) && $finding->severity !== ''
? Str::of($finding->severity)->replace('_', ' ')->title()->value()
: 'Severity unavailable';
$type = is_string($finding->finding_type) && $finding->finding_type !== ''
? Str::of($finding->finding_type)->replace('_', ' ')->lower()->value()
: 'finding';
return $severity.' '.$type;
}
private function findingOwnerLabel(Finding $finding): string
{
if ($finding->ownerUser?->name !== null) {
return $finding->ownerUser->name;
}
if ($finding->assigneeUser?->name !== null) {
return 'Active assignee: '.$finding->assigneeUser->name;
}
return 'Owner missing';
}
private function findingDueLabel(Finding $finding): string
{
if (! $finding->due_at instanceof \DateTimeInterface) {
return 'Due date unavailable';
}
$relative = FindingExceptionResource::relativeTimeDescription($finding->due_at);
if ($finding->due_at->isPast()) {
return 'Overdue'.($relative !== null ? ': '.$relative : '');
}
return $relative ?? $finding->due_at->format('Y-m-d');
}
private function findingEvidenceState(Finding $finding): string
{
return is_array($finding->evidence_jsonb) && $finding->evidence_jsonb !== []
? 'linked'
: 'missing';
}
private function findingEvidenceLabel(Finding $finding): string
{
return $this->findingEvidenceState($finding) === 'linked'
? 'Evidence captured on finding'
: 'Evidence missing';
}
private function findingExceptionLabel(Finding $finding): string
{
$exception = $finding->relationLoaded('findingException') ? $finding->findingException : null;
if ($exception instanceof FindingException) {
return $this->findingExceptionDecisionStateLabel($exception);
}
return 'No accepted risk';
}
private function findingExceptionImpactLabel(FindingException $exception): string
{
return match ((string) $exception->current_validity_state) {
FindingException::VALIDITY_EXPIRING => 'Accepted risk expiring',
FindingException::VALIDITY_EXPIRED => 'Accepted risk expired',
FindingException::VALIDITY_MISSING_SUPPORT => 'Accepted risk missing support',
FindingException::VALIDITY_VALID => 'Accepted risk active',
default => 'Exception review required',
};
}
private function findingExceptionDueLabel(FindingException $exception): string
{
$date = $exception->review_due_at ?? $exception->expires_at;
if (! $date instanceof \DateTimeInterface) {
return 'Due date unavailable';
}
$relative = FindingExceptionResource::relativeTimeDescription($date);
if ($date->isPast()) {
return 'Overdue'.($relative !== null ? ': '.$relative : '');
}
return $relative ?? $date->format('Y-m-d');
}
private function findingExceptionEvidenceState(FindingException $exception): string
{
$summaryCount = data_get($exception->evidence_summary, 'reference_count');
$referenceCount = is_numeric($summaryCount)
? (int) $summaryCount
: ($exception->relationLoaded('evidenceReferences') ? $exception->evidenceReferences->count() : 0);
return $referenceCount > 0 ? 'linked' : 'missing';
}
private function findingExceptionEvidenceLabel(FindingException $exception): string
{
return $this->findingExceptionEvidenceState($exception) === 'linked'
? 'Evidence references linked'
: 'Evidence missing';
}
private function findingExceptionDecisionStateLabel(FindingException $exception): string
{
if ((string) $exception->status === FindingException::STATUS_PENDING) {
return 'Pending exception';
}
return match ((string) $exception->current_validity_state) {
FindingException::VALIDITY_VALID => 'Accepted risk active',
FindingException::VALIDITY_EXPIRING => 'Accepted risk expiring',
FindingException::VALIDITY_EXPIRED => 'Accepted risk expired',
FindingException::VALIDITY_MISSING_SUPPORT => 'Accepted risk missing support',
default => Str::of((string) $exception->status)->replace('_', ' ')->title()->value(),
};
}
private function reviewSummary(int $followUpCount, int $changedCount): string
{
$total = $followUpCount + $changedCount;

View File

@ -3,42 +3,30 @@
$scope = $this->appliedScope();
$sections = $this->sections();
$emptyState = $this->calmEmptyState();
$workbench = $this->decisionWorkbench();
$selectedItem = $workbench['selected_item'] ?? null;
$diagnostics = $workbench['diagnostics'] ?? [];
@endphp
<x-filament::section>
<div class="flex flex-col gap-3">
<div class="inline-flex w-fit items-center gap-2 rounded-full border border-primary-200 bg-primary-50 px-3 py-1 text-xs font-medium text-primary-700 dark:border-primary-700/60 dark:bg-primary-950/40 dark:text-primary-300">
<x-filament::icon icon="heroicon-o-inbox-stack" class="h-3.5 w-3.5" />
Governance inbox
</div>
<div class="space-y-1">
<h1 class="text-2xl font-semibold tracking-tight text-gray-950 dark:text-white">
Governance inbox
</h1>
<p class="max-w-3xl text-sm leading-6 text-gray-600 dark:text-gray-300">
This workspace decision surface routes you into the existing findings, finding exceptions, operations, alerts, and review surfaces without introducing a second workflow state.
</p>
</div>
<div class="space-y-6">
<div class="space-y-3" data-testid="governance-inbox-secondary-filters">
<div class="flex flex-wrap gap-2 text-sm text-gray-600 dark:text-gray-300">
@if (filled($scope['workspace_label'] ?? null))
<span class="inline-flex items-center rounded-full bg-gray-100 px-3 py-1 text-xs font-medium text-gray-700 dark:bg-gray-800 dark:text-gray-200">
<span class="inline-flex items-center rounded-md bg-gray-100 px-2.5 py-1 text-xs font-medium text-gray-700 dark:bg-gray-800 dark:text-gray-200">
Workspace: {{ $scope['workspace_label'] }}
</span>
@endif
<span class="inline-flex items-center rounded-full bg-gray-100 px-3 py-1 text-xs font-medium text-gray-700 dark:bg-gray-800 dark:text-gray-200">
<span class="inline-flex items-center rounded-md bg-gray-100 px-2.5 py-1 text-xs font-medium text-gray-700 dark:bg-gray-800 dark:text-gray-200">
Scope: {{ $scope['family_label'] ?? 'All attention' }}
</span>
<span class="inline-flex items-center rounded-full bg-gray-100 px-3 py-1 text-xs font-medium text-gray-700 dark:bg-gray-800 dark:text-gray-200">
<span class="inline-flex items-center rounded-md bg-gray-100 px-2.5 py-1 text-xs font-medium text-gray-700 dark:bg-gray-800 dark:text-gray-200">
Visible items: {{ $scope['total_count'] ?? 0 }}
</span>
@if (filled($scope['tenant_label'] ?? null))
<span class="inline-flex items-center rounded-full bg-warning-50 px-3 py-1 text-xs font-medium text-warning-700 dark:bg-warning-500/10 dark:text-warning-300">
<span class="inline-flex items-center rounded-md bg-warning-50 px-2.5 py-1 text-xs font-medium text-warning-700 dark:bg-warning-500/10 dark:text-warning-300">
Environment: {{ $scope['tenant_label'] }}
</span>
@endif
@ -51,112 +39,291 @@
])
@endif
<div class="flex flex-wrap gap-2">
<div class="flex flex-wrap gap-2" data-testid="governance-inbox-family-filters">
<a
href="{{ $this->pageUrl(['family' => null]) }}"
class="inline-flex items-center gap-2 rounded-full border px-3 py-1.5 text-sm font-medium transition {{ $this->family === null ? 'border-primary-300 bg-primary-50 text-primary-700 dark:border-primary-700/60 dark:bg-primary-950/40 dark:text-primary-300' : 'border-gray-200 text-gray-700 hover:border-gray-300 dark:border-gray-700 dark:text-gray-200 dark:hover:border-gray-600' }}"
class="inline-flex items-center gap-2 rounded-md border px-2.5 py-1 text-xs font-medium transition {{ $this->family === null ? 'border-primary-300 bg-primary-50 text-primary-700 dark:border-primary-700/60 dark:bg-primary-950/40 dark:text-primary-300' : 'border-gray-200 text-gray-700 hover:border-gray-300 dark:border-gray-700 dark:text-gray-200 dark:hover:border-gray-600' }}"
>
All attention
<span class="rounded-full bg-black/5 px-2 py-0.5 text-xs dark:bg-white/10">{{ $scope['total_count'] ?? 0 }}</span>
<span class="rounded-md bg-black/5 px-2 py-0.5 text-xs dark:bg-white/10">{{ $scope['total_count'] ?? 0 }}</span>
</a>
@foreach ($this->availableFamilies() as $family)
<a
href="{{ $this->pageUrl(['family' => $family['key']]) }}"
class="inline-flex items-center gap-2 rounded-full border px-3 py-1.5 text-sm font-medium transition {{ $this->isActiveFamily($family['key']) ? 'border-primary-300 bg-primary-50 text-primary-700 dark:border-primary-700/60 dark:bg-primary-950/40 dark:text-primary-300' : 'border-gray-200 text-gray-700 hover:border-gray-300 dark:border-gray-700 dark:text-gray-200 dark:hover:border-gray-600' }}"
class="inline-flex items-center gap-2 rounded-md border px-2.5 py-1 text-xs font-medium transition {{ $this->isActiveFamily($family['key']) ? 'border-primary-300 bg-primary-50 text-primary-700 dark:border-primary-700/60 dark:bg-primary-950/40 dark:text-primary-300' : 'border-gray-200 text-gray-700 hover:border-gray-300 dark:border-gray-700 dark:text-gray-200 dark:hover:border-gray-600' }}"
>
{{ $family['label'] }}
<span class="rounded-full bg-black/5 px-2 py-0.5 text-xs dark:bg-white/10">{{ $family['count'] }}</span>
<span class="rounded-md bg-black/5 px-2 py-0.5 text-xs dark:bg-white/10">{{ $family['count'] }}</span>
</a>
@endforeach
</div>
</div>
</x-filament::section>
@if ($sections === [])
<x-filament::section>
<div class="flex flex-col gap-4 rounded-2xl border border-dashed border-gray-300 bg-gray-50/60 p-6 dark:border-gray-700 dark:bg-gray-900/40">
<div class="space-y-1">
<h2 class="text-base font-semibold text-gray-950 dark:text-white">{{ $emptyState['title'] }}</h2>
<p class="max-w-2xl text-sm leading-6 text-gray-600 dark:text-gray-300">{{ $emptyState['body'] }}</p>
<div class="grid grid-cols-1 gap-6 lg:grid-cols-[minmax(0,1fr)_22rem]" data-testid="governance-inbox-decision-workbench">
<section class="rounded-lg border border-gray-200 bg-white p-5 shadow-sm dark:border-gray-800 dark:bg-gray-900" data-testid="governance-inbox-priority-card">
<div class="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
<div class="space-y-2">
<p class="text-xs font-medium uppercase text-gray-500 dark:text-gray-400">
Decision workbench
</p>
<h2 class="text-xl font-semibold text-gray-950 dark:text-white">
{{ $workbench['question'] }}
</h2>
</div>
@if ($selectedItem !== null)
<span class="inline-flex w-fit items-center rounded-lg bg-warning-50 px-2.5 py-1 text-xs font-medium text-warning-700 dark:bg-warning-500/10 dark:text-warning-300">
{{ $selectedItem['status_label'] }}
</span>
@endif
</div>
@if (filled($emptyState['action_label'] ?? null) && filled($emptyState['action_url'] ?? null))
<div>
<x-filament::button tag="a" color="gray" href="{{ $emptyState['action_url'] }}">
{{ $emptyState['action_label'] }}
</x-filament::button>
@if ($selectedItem === null)
<div class="mt-5 rounded-lg border border-dashed border-gray-300 bg-gray-50 p-5 dark:border-gray-700 dark:bg-gray-950/40" data-testid="governance-inbox-empty-decision-state">
<h3 class="text-base font-semibold text-gray-950 dark:text-white">
{{ $emptyState['title'] }}
</h3>
<p class="mt-1 text-sm leading-6 text-gray-600 dark:text-gray-300">
{{ $emptyState['body'] }}
</p>
@if (filled($emptyState['action_label'] ?? null) && filled($emptyState['action_url'] ?? null))
<div class="mt-4">
<x-filament::button tag="a" color="gray" href="{{ $emptyState['action_url'] }}">
{{ $emptyState['action_label'] }}
</x-filament::button>
</div>
@endif
</div>
@endif
</div>
</x-filament::section>
@else
@foreach ($sections as $section)
<x-filament::section>
<div class="flex flex-col gap-4">
<div class="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
@else
<div class="mt-5 space-y-5">
<div class="space-y-2">
<div class="flex flex-wrap items-center gap-2">
<h2 class="text-base font-semibold text-gray-950 dark:text-white">{{ $section['label'] }}</h2>
<span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-1 text-xs font-medium text-gray-700 dark:bg-gray-800 dark:text-gray-200">
{{ $section['count'] }}
<span class="inline-flex items-center rounded-lg bg-gray-100 px-2.5 py-1 text-xs font-medium text-gray-700 dark:bg-gray-800 dark:text-gray-200">
{{ $selectedItem['section_label'] }}
</span>
<span class="inline-flex items-center rounded-lg bg-gray-100 px-2.5 py-1 text-xs font-medium text-gray-700 dark:bg-gray-800 dark:text-gray-200">
Environment: {{ $selectedItem['environment_label'] }}
</span>
</div>
<p class="max-w-3xl text-sm leading-6 text-gray-600 dark:text-gray-300">{{ $section['summary'] }}</p>
<h3 class="text-lg font-semibold text-gray-950 dark:text-white">
{{ $selectedItem['title'] }}
</h3>
<p class="text-sm leading-6 text-gray-600 dark:text-gray-300">
{{ $selectedItem['decision_label'] }}
</p>
</div>
<div>
<dl class="grid grid-cols-1 gap-3 md:grid-cols-2">
<div class="rounded-lg border border-gray-200 p-3 dark:border-gray-800">
<dt class="text-xs font-medium text-gray-500 dark:text-gray-400">Reason</dt>
<dd class="mt-1 text-sm leading-6 text-gray-800 dark:text-gray-100">{{ $selectedItem['reason_label'] }}</dd>
</div>
<div class="rounded-lg border border-gray-200 p-3 dark:border-gray-800">
<dt class="text-xs font-medium text-gray-500 dark:text-gray-400">Impact</dt>
<dd class="mt-1 text-sm leading-6 text-gray-800 dark:text-gray-100">{{ $selectedItem['impact_label'] }}</dd>
</div>
<div class="rounded-lg border border-gray-200 p-3 dark:border-gray-800">
<dt class="text-xs font-medium text-gray-500 dark:text-gray-400">Owner</dt>
<dd class="mt-1 text-sm leading-6 text-gray-800 dark:text-gray-100">{{ $selectedItem['owner_label'] }}</dd>
</div>
<div class="rounded-lg border border-gray-200 p-3 dark:border-gray-800">
<dt class="text-xs font-medium text-gray-500 dark:text-gray-400">Due</dt>
<dd class="mt-1 text-sm leading-6 text-gray-800 dark:text-gray-100">{{ $selectedItem['due_label'] }}</dd>
</div>
</dl>
<div class="grid grid-cols-1 gap-3 md:grid-cols-2">
<div class="rounded-lg border border-gray-200 p-3 dark:border-gray-800">
<p class="text-xs font-medium text-gray-500 dark:text-gray-400">Evidence</p>
<p class="mt-1 text-sm leading-6 text-gray-800 dark:text-gray-100">{{ $selectedItem['evidence_label'] }}</p>
</div>
<div class="rounded-lg border border-gray-200 p-3 dark:border-gray-800">
<p class="text-xs font-medium text-gray-500 dark:text-gray-400">Accepted risk</p>
<p class="mt-1 text-sm leading-6 text-gray-800 dark:text-gray-100">{{ $selectedItem['exception_label'] }}</p>
</div>
</div>
@if (filled($selectedItem['primary_action_url'] ?? null))
<div class="flex flex-col gap-2 rounded-lg border border-gray-200 p-3 dark:border-gray-800 sm:flex-row sm:items-center sm:justify-between">
<div>
<p class="text-xs font-medium text-gray-500 dark:text-gray-400">Primary next action</p>
<p class="mt-1 text-sm leading-6 text-gray-800 dark:text-gray-100">{{ $selectedItem['primary_action_label'] }}</p>
</div>
<x-filament::button
tag="a"
href="{{ $selectedItem['primary_action_url'] }}"
icon="heroicon-o-arrow-top-right-on-square"
>
{{ $selectedItem['primary_action_label'] }}
</x-filament::button>
</div>
@endif
</div>
@endif
</section>
<aside class="rounded-lg border border-gray-200 bg-white p-5 shadow-sm dark:border-gray-800 dark:bg-gray-900" data-testid="governance-inbox-decision-detail">
<div class="space-y-4">
<div class="space-y-1">
<p class="text-xs font-medium uppercase text-gray-500 dark:text-gray-400">Decision summary</p>
<h2 class="text-base font-semibold text-gray-950 dark:text-white">
{{ $selectedItem['title'] ?? 'No selected decision' }}
</h2>
</div>
@if ($selectedItem !== null)
<div class="space-y-3 text-sm">
<div>
<p class="text-xs font-medium text-gray-500 dark:text-gray-400">Impact</p>
<p class="mt-1 text-gray-800 dark:text-gray-100">{{ $selectedItem['impact_label'] }}</p>
</div>
<div>
<p class="text-xs font-medium text-gray-500 dark:text-gray-400">Evidence path</p>
@if (filled($selectedItem['evidence_path_url'] ?? null))
<a href="{{ $selectedItem['evidence_path_url'] }}" class="mt-1 inline-flex items-center gap-1 text-sm font-medium text-primary-600 hover:underline dark:text-primary-300">
{{ $selectedItem['evidence_path_label'] }}
<x-filament::icon icon="heroicon-m-arrow-top-right-on-square" class="h-4 w-4" />
</a>
@else
<p class="mt-1 text-gray-800 dark:text-gray-100">{{ $selectedItem['evidence_path_label'] }}</p>
@endif
</div>
<div>
<p class="text-xs font-medium text-gray-500 dark:text-gray-400">Accepted risk</p>
<p class="mt-1 text-gray-800 dark:text-gray-100">{{ $selectedItem['exception_label'] }}</p>
</div>
<div>
<p class="text-xs font-medium text-gray-500 dark:text-gray-400">Owner / due</p>
<p class="mt-1 text-gray-800 dark:text-gray-100">{{ $selectedItem['owner_label'] }} · {{ $selectedItem['due_label'] }}</p>
</div>
</div>
@if (filled($selectedItem['primary_action_url'] ?? null))
<div class="space-y-2">
<p class="text-xs font-medium text-gray-500 dark:text-gray-400">Primary next action</p>
<x-filament::button
tag="a"
color="gray"
href="{{ $selectedItem['primary_action_url'] }}"
icon="heroicon-o-arrow-top-right-on-square"
class="w-full"
>
{{ $selectedItem['primary_action_label'] }}
</x-filament::button>
</div>
@endif
@else
<div class="space-y-3 text-sm">
<div>
<p class="text-xs font-medium text-gray-500 dark:text-gray-400">Impact</p>
<p class="mt-1 text-gray-800 dark:text-gray-100">Unavailable</p>
</div>
<div>
<p class="text-xs font-medium text-gray-500 dark:text-gray-400">Owner / due</p>
<p class="mt-1 text-gray-800 dark:text-gray-100">Unavailable</p>
</div>
<div>
<p class="text-xs font-medium text-gray-500 dark:text-gray-400">Evidence path</p>
<p class="mt-1 text-gray-800 dark:text-gray-100">Unavailable</p>
</div>
<div>
<p class="text-xs font-medium text-gray-500 dark:text-gray-400">Accepted risk</p>
<p class="mt-1 text-gray-800 dark:text-gray-100">Unavailable</p>
</div>
<div>
<p class="text-xs font-medium text-gray-500 dark:text-gray-400">Primary next action</p>
<p class="mt-1 text-gray-800 dark:text-gray-100">No action available</p>
</div>
</div>
@endif
<details class="rounded-lg border border-gray-200 p-3 dark:border-gray-800" data-testid="governance-inbox-diagnostics">
<summary class="cursor-pointer text-sm font-medium text-gray-700 dark:text-gray-200">
{{ $diagnostics['label'] ?? 'Diagnostics' }} · {{ $diagnostics['state'] ?? 'Collapsed' }}
</summary>
<p class="mt-3 text-sm leading-6 text-gray-600 dark:text-gray-300">
{{ $diagnostics['body'] ?? 'Diagnostics are not default-visible.' }}
</p>
</details>
</div>
</aside>
</div>
@if ($sections !== [])
<div class="space-y-4" data-testid="governance-inbox-queue-context">
<div class="flex flex-col gap-1">
<h2 class="text-lg font-semibold text-gray-950 dark:text-white">Queue context</h2>
<p class="text-sm leading-6 text-gray-600 dark:text-gray-300">
Secondary source-family context remains available after the priority decision.
</p>
</div>
@foreach ($sections as $section)
<div class="rounded-lg border border-gray-200 bg-white p-5 dark:border-gray-800 dark:bg-gray-900">
<div class="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
<div class="space-y-2">
<div class="flex flex-wrap items-center gap-2">
<h3 class="text-base font-semibold text-gray-950 dark:text-white">{{ $section['label'] }}</h3>
<span class="inline-flex items-center rounded-lg bg-gray-100 px-2.5 py-1 text-xs font-medium text-gray-700 dark:bg-gray-800 dark:text-gray-200">
{{ $section['count'] }}
</span>
</div>
<p class="max-w-3xl text-sm leading-6 text-gray-600 dark:text-gray-300">{{ $section['summary'] }}</p>
</div>
<x-filament::button tag="a" color="gray" href="{{ $section['dominant_action_url'] }}">
{{ $section['dominant_action_label'] }}
</x-filament::button>
</div>
</div>
@if ($section['count'] === 0)
<div class="rounded-2xl border border-dashed border-gray-300 bg-gray-50/60 p-5 text-sm leading-6 text-gray-600 dark:border-gray-700 dark:bg-gray-900/40 dark:text-gray-300">
{{ $section['empty_state'] }}
</div>
@else
<ul class="grid gap-3">
@foreach ($section['entries'] as $entry)
<li class="rounded-2xl border border-gray-200 bg-white p-4 shadow-sm dark:border-gray-800 dark:bg-gray-900/60">
<div class="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
<div class="space-y-1.5">
@if (filled($entry['tenant_label'] ?? null))
<div class="text-xs font-medium uppercase tracking-[0.16em] text-gray-500 dark:text-gray-400">
{{ $entry['tenant_label'] }}
@if ($section['count'] === 0)
<div class="mt-4 rounded-lg border border-dashed border-gray-300 bg-gray-50 p-4 text-sm leading-6 text-gray-600 dark:border-gray-700 dark:bg-gray-950/50 dark:text-gray-300">
{{ $section['empty_state'] }}
</div>
@else
<ul class="mt-4 grid gap-3">
@foreach ($section['entries'] as $entry)
<li class="rounded-lg border border-gray-200 p-4 dark:border-gray-800">
<div class="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
<div class="space-y-1.5">
@if (filled($entry['tenant_label'] ?? null))
<div class="text-xs font-medium uppercase text-gray-500 dark:text-gray-400">
{{ $entry['tenant_label'] }}
</div>
@endif
<div class="flex flex-wrap items-center gap-2">
<a href="{{ $entry['destination_url'] }}" class="text-sm font-semibold text-gray-950 hover:text-primary-600 dark:text-white dark:hover:text-primary-300">
{{ $entry['headline'] }}
</a>
<span class="inline-flex items-center rounded-lg bg-gray-100 px-2.5 py-1 text-xs font-medium text-gray-700 dark:bg-gray-800 dark:text-gray-200">
{{ $entry['status_label'] }}
</span>
</div>
@endif
<div class="flex flex-wrap items-center gap-2">
<a href="{{ $entry['destination_url'] }}" class="text-sm font-semibold text-gray-950 hover:text-primary-600 dark:text-white dark:hover:text-primary-300">
{{ $entry['headline'] }}
</a>
<span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-1 text-xs font-medium text-gray-700 dark:bg-gray-800 dark:text-gray-200">
{{ $entry['status_label'] }}
</span>
@if (filled($entry['subline'] ?? null))
<p class="text-sm leading-6 text-gray-600 dark:text-gray-300">{{ $entry['subline'] }}</p>
@endif
</div>
@if (filled($entry['subline'] ?? null))
<p class="text-sm leading-6 text-gray-600 dark:text-gray-300">{{ $entry['subline'] }}</p>
@endif
</div>
<div>
<x-filament::button tag="a" color="gray" size="sm" href="{{ $entry['destination_url'] }}">
Open source
</x-filament::button>
</div>
</div>
</li>
@endforeach
</ul>
@endif
</div>
</x-filament::section>
@endforeach
@endif
</li>
@endforeach
</ul>
@endif
</div>
@endforeach
</div>
@endif
</div>
</x-filament-panels::page>

View File

@ -0,0 +1,264 @@
<?php
declare(strict_types=1);
use App\Filament\Pages\Governance\GovernanceInbox;
use App\Models\Finding;
use App\Models\ManagedEnvironment;
use App\Models\User;
use App\Support\Workspaces\WorkspaceContext;
pest()->browser()->timeout(60_000);
it('Spec327 smokes non-empty governance inbox decision workbench entry', function (): void {
[$user, $environmentA, $environmentB] = spec327GovernanceInboxFixture();
spec327AuthenticateGovernanceInboxBrowser($this, $user, $environmentA);
visit(GovernanceInbox::getUrl(panel: 'admin'))
->resize(1440, 1100)
->waitForText('Governance Inbox')
->assertSee('Prioritized governance decisions, owners, evidence, and follow-up actions across entitled environments.')
->assertSee(__('localization.shell.no_environment_selected'))
->assertDontSee('Environment filter:')
->assertSee('What decision clears the highest-priority item?')
->assertSee('Decision workbench')
->assertSee('Decision summary')
->assertSee('Finding #2')
->assertSee('Reason')
->assertSee('The finding reopened after a previous resolution path.')
->assertSee('Impact')
->assertSee('Medium drift')
->assertSee('Owner')
->assertSee('Owner missing')
->assertSee('Due')
->assertSee('In 14 days')
->assertSee('Evidence missing')
->assertSee('Evidence path')
->assertSee('Source record only')
->assertSee('Accepted risk')
->assertSee('Primary next action')
->assertSee('Triage finding')
->assertSee('No accepted risk')
->assertSee('Queue context')
->assertSee($environmentA->name)
->assertSee($environmentB->name)
->assertDontSee('No governance decisions need attention')
->assertDontSee('tenant filter')
->assertDontSee('current tenant')
->assertDontSee('entitled tenant')
->assertDontSee('all tenants')
->assertDontSee('raw payload should stay hidden')
->assertDontSee('stack trace should stay hidden')
->assertDontSee('provider secret should stay hidden')
->assertDontSee('debug metadata should stay hidden')
->assertScript('document.querySelector("[data-testid=\"governance-inbox-diagnostics\"]")?.open === false', true)
->assertScript('(() => {
const grid = document.querySelector("[data-testid=\"governance-inbox-decision-workbench\"]");
const workbench = document.querySelector("[data-testid=\"governance-inbox-priority-card\"]");
const detail = document.querySelector("[data-testid=\"governance-inbox-decision-detail\"]");
if (! grid || ! workbench || ! detail) {
return false;
}
const children = Array.from(grid.children);
const workbenchBox = workbench.getBoundingClientRect();
const detailBox = detail.getBoundingClientRect();
return window.innerWidth >= 1024
&& grid.classList.contains("lg:grid-cols-[minmax(0,1fr)_22rem]")
&& detail.tagName === "ASIDE"
&& children.indexOf(workbench) !== -1
&& children.indexOf(detail) > children.indexOf(workbench)
&& detailBox.left > workbenchBox.right
&& Math.abs(detailBox.top - workbenchBox.top) <= 8;
})()', true)
->assertNoJavaScriptErrors()
->assertNoConsoleLogs()
->screenshot(true, spec327GovernanceInboxScreenshot('governance-inbox--clean'));
spec327CopyBrowserScreenshot('governance-inbox--clean');
spec327CopyBrowserScreenshot('governance-inbox--clean', 'governance-inbox-decision-workbench.png');
});
it('Spec327 smokes filtered governance inbox clear and reload behavior', function (): void {
[$user, $environmentA, $environmentB] = spec327GovernanceInboxFixture();
$cleanPath = json_encode((string) parse_url(GovernanceInbox::getUrl(panel: 'admin'), PHP_URL_PATH), JSON_THROW_ON_ERROR);
spec327AuthenticateGovernanceInboxBrowser($this, $user, $environmentA);
$page = visit(GovernanceInbox::getUrl(panel: 'admin', parameters: [
'environment_id' => (int) $environmentA->getKey(),
]))
->waitForText('Environment filter:')
->assertSee('Environment filter: '.$environmentA->name)
->assertSee('What decision clears the highest-priority item?')
->assertSee($environmentA->name)
->assertDontSee($environmentB->name)
->assertScript('document.querySelector("[data-testid=\"governance-inbox-diagnostics\"]")?.open === false', true)
->assertNoJavaScriptErrors()
->assertNoConsoleLogs()
->screenshot(true, spec327GovernanceInboxScreenshot('governance-inbox--filtered'));
spec327CopyBrowserScreenshot('governance-inbox--filtered');
$page
->click('[data-testid="workspace-hub-environment-filter-clear"]')
->waitForText(__('localization.shell.no_environment_selected'))
->assertDontSee('Environment filter:')
->assertSee($environmentB->name)
->assertScript("window.location.pathname === {$cleanPath}", true)
->assertScript('! window.location.search.includes("environment_id=")', true)
->assertNoJavaScriptErrors()
->assertNoConsoleLogs()
->screenshot(true, spec327GovernanceInboxScreenshot('governance-inbox--after-clear'));
spec327CopyBrowserScreenshot('governance-inbox--after-clear');
$page->script('window.location.reload();');
$page
->waitForText(__('localization.shell.no_environment_selected'))
->assertDontSee('Environment filter:')
->assertSee($environmentB->name)
->assertScript("window.location.pathname === {$cleanPath}", true)
->assertScript('! window.location.search.includes("environment_id=")', true)
->assertNoJavaScriptErrors()
->assertNoConsoleLogs()
->screenshot(true, spec327GovernanceInboxScreenshot('governance-inbox--after-reload'));
spec327CopyBrowserScreenshot('governance-inbox--after-reload');
});
it('Spec327 smokes governance inbox diagnostics disclosure and secondary queue', function (): void {
[$user, $environmentA] = spec327GovernanceInboxFixture();
spec327AuthenticateGovernanceInboxBrowser($this, $user, $environmentA);
visit(GovernanceInbox::getUrl(panel: 'admin'))
->waitForText('Queue context')
->assertSee('Assigned findings')
->assertScript('document.querySelector("[data-testid=\"governance-inbox-diagnostics\"]")?.open === false', true)
->click('[data-testid="governance-inbox-diagnostics"] summary')
->assertScript('document.querySelector("[data-testid=\"governance-inbox-diagnostics\"]")?.open === true', true)
->assertSee('Source diagnostics and raw support details stay on authorized source surfaces')
->assertDontSee('raw payload should stay hidden')
->assertDontSee('internal exception should stay hidden')
->assertNoJavaScriptErrors()
->assertNoConsoleLogs()
->screenshot(true, spec327GovernanceInboxScreenshot('governance-inbox--diagnostics'));
spec327CopyBrowserScreenshot('governance-inbox--diagnostics');
});
/**
* @return array{0: User, 1: ManagedEnvironment, 2: ManagedEnvironment}
*/
function spec327GovernanceInboxFixture(): array
{
$environmentA = ManagedEnvironment::factory()->active()->create([
'name' => 'Spec327 Browser Environment A',
'external_id' => 'spec327-browser-environment-a',
]);
[$user, $environmentA] = createUserWithTenant(
tenant: $environmentA,
role: 'owner',
workspaceRole: 'owner',
);
$environmentB = ManagedEnvironment::factory()->active()->create([
'workspace_id' => (int) $environmentA->workspace_id,
'name' => 'Spec327 Browser Environment B',
'external_id' => 'spec327-browser-environment-b',
]);
createUserWithTenant(
tenant: $environmentB,
user: $user,
role: 'owner',
workspaceRole: 'owner',
);
Finding::factory()
->for($environmentA)
->assignedTo((int) $user->getKey())
->ownedBy((int) $user->getKey())
->overdueByHours()
->create([
'workspace_id' => (int) $environmentA->workspace_id,
'subject_external_id' => 'spec327-browser-priority-a',
'severity' => Finding::SEVERITY_HIGH,
'evidence_jsonb' => [
'summary' => [
'kind' => 'policy_snapshot',
'raw_payload' => 'raw payload should stay hidden',
'stack_trace' => 'stack trace should stay hidden',
'provider_secret' => 'provider secret should stay hidden',
'debug_metadata' => 'debug metadata should stay hidden',
'internal_exception' => 'internal exception should stay hidden',
],
],
]);
Finding::factory()
->for($environmentB)
->reopened()
->create([
'workspace_id' => (int) $environmentB->workspace_id,
'subject_external_id' => 'spec327-browser-secondary-b',
'severity' => Finding::SEVERITY_MEDIUM,
'owner_user_id' => null,
'assignee_user_id' => null,
'due_at' => now()->addDays(14),
'evidence_jsonb' => [],
]);
return [$user, $environmentA, $environmentB];
}
function spec327AuthenticateGovernanceInboxBrowser(
mixed $test,
User $user,
ManagedEnvironment $rememberedEnvironment,
): void {
$workspaceId = (int) $rememberedEnvironment->workspace_id;
$session = [
WorkspaceContext::SESSION_KEY => $workspaceId,
WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => [
(string) $workspaceId => (int) $rememberedEnvironment->getKey(),
],
];
$test->actingAs($user)->withSession($session);
foreach ($session as $key => $value) {
session()->put($key, $value);
}
setAdminPanelContext($rememberedEnvironment);
}
function spec327GovernanceInboxScreenshot(string $name): string
{
return 'spec327-'.$name;
}
function spec327CopyBrowserScreenshot(string $name, ?string $targetFilename = null): void
{
$filename = spec327GovernanceInboxScreenshot($name).'.png';
$source = \Pest\Browser\Support\Screenshot::path($filename);
$targetDirectory = repo_path('specs/327-governance-inbox-decision-first-workbench-productization/artifacts/screenshots');
$targetFilename ??= $filename;
if (! is_dir($targetDirectory)) {
@mkdir($targetDirectory, 0755, true);
}
if (! is_dir($targetDirectory) || ! is_writable($targetDirectory)) {
return;
}
if (is_file($source)) {
@copy($source, $targetDirectory.DIRECTORY_SEPARATOR.$targetFilename);
}
}

View File

@ -16,6 +16,186 @@
use App\Support\PortfolioTriage\PortfolioArrivalContextToken;
use App\Support\Workspaces\WorkspaceContext;
it('documents the Spec 327 governance inbox repo truth map', function (): void {
$path = repo_path('specs/327-governance-inbox-decision-first-workbench-productization/repo-truth-map.md');
expect($path)->toBeFile();
$contents = (string) file_get_contents($path);
expect($contents)
->toContain('Findings')
->toContain('Finding Exceptions / Accepted Risks')
->toContain('OperationRun links')
->toContain('Workspace / Environment filter state')
->toContain('Diagnostics');
});
it('renders the Spec 327 decision-first workbench for the highest-priority finding', function (): void {
$tenant = ManagedEnvironment::factory()->create([
'status' => 'active',
'name' => 'Spec327 Environment Alpha',
]);
[$user, $tenant] = createUserWithTenant($tenant, role: 'owner', workspaceRole: 'owner');
Finding::factory()
->for($tenant)
->assignedTo((int) $user->getKey())
->ownedBy((int) $user->getKey())
->overdueByHours()
->create([
'workspace_id' => (int) $tenant->workspace_id,
'subject_external_id' => 'spec327-priority-finding',
'severity' => Finding::SEVERITY_HIGH,
'status' => Finding::STATUS_IN_PROGRESS,
'evidence_jsonb' => [
'summary' => [
'kind' => 'policy_snapshot',
'raw_payload' => 'raw payload should stay hidden',
'debug_metadata' => 'debug metadata should stay hidden',
],
],
]);
$this->actingAs($user)
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
->get(GovernanceInbox::getUrl(panel: 'admin'))
->assertOk()
->assertSee('Governance Inbox')
->assertSee('Prioritized governance decisions, owners, evidence, and follow-up actions across entitled environments.')
->assertSee('What decision clears the highest-priority item?')
->assertSee('Decision workbench')
->assertSee('Reason')
->assertSee('Impact')
->assertSee('Owner')
->assertSee('Due')
->assertSee('Evidence')
->assertSee('Accepted risk')
->assertSee('Review finding')
->assertSee('Evidence captured on finding')
->assertSee('No accepted risk')
->assertSee('Decision summary')
->assertSee('Owner / due')
->assertSee('Evidence path')
->assertSee('Primary next action')
->assertSee('Queue context')
->assertSee('Assigned findings')
->assertDontSee('No governance decisions need attention')
->assertDontSee('tenant filter')
->assertDontSee('current tenant')
->assertDontSee('entitled tenant')
->assertDontSee('all tenants')
->assertDontSee('raw payload should stay hidden')
->assertDontSee('debug metadata should stay hidden');
});
it('renders a compact empty decision state without primary zero metric cards', function (): void {
$tenant = ManagedEnvironment::factory()->create([
'status' => 'active',
'name' => 'Spec327 Empty Environment',
]);
[$user, $tenant] = createUserWithTenant($tenant, role: 'owner', workspaceRole: 'owner');
$response = $this->actingAs($user)
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
->get(GovernanceInbox::getUrl(panel: 'admin'));
$response
->assertOk()
->assertSee('What decision clears the highest-priority item?')
->assertSee('No governance decisions need attention')
->assertSee('The current workspace scope has no repo-backed governance decisions requiring action.')
->assertSee('Decision summary')
->assertSee('Owner / due')
->assertSee('Evidence path')
->assertSee('Accepted risk')
->assertSee('Primary next action')
->assertSee('Diagnostics')
->assertSee('Collapsed')
->assertDontSee('Visible decisions')
->assertDontSee('Priority family')
->assertDontSee('raw payload')
->assertDontSee('stack trace')
->assertDontSee('debug metadata')
->assertDontSee('provider secret');
});
it('renders honest missing owner due evidence and accepted-risk states', function (): void {
$tenant = ManagedEnvironment::factory()->create([
'status' => 'active',
'name' => 'Spec327 Environment Missing State',
]);
[$user, $tenant] = createUserWithTenant($tenant, role: 'owner', workspaceRole: 'owner');
Finding::factory()
->for($tenant)
->create([
'workspace_id' => (int) $tenant->workspace_id,
'subject_external_id' => 'spec327-missing-state-finding',
'owner_user_id' => null,
'assignee_user_id' => null,
'due_at' => null,
'evidence_jsonb' => [],
]);
$this->actingAs($user)
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
->get(GovernanceInbox::getUrl(panel: 'admin'))
->assertOk()
->assertSee('Owner missing')
->assertSee('Due date unavailable')
->assertSee('Evidence missing')
->assertSee('No accepted risk')
->assertSee('Triage finding');
});
it('renders accepted-risk and exception state without exposing raw diagnostics by default', function (): void {
$tenant = ManagedEnvironment::factory()->create([
'status' => 'active',
'name' => 'Spec327 Environment Exception',
]);
[$user, $tenant] = createUserWithTenant($tenant, role: 'owner', workspaceRole: 'owner');
$finding = Finding::factory()
->for($tenant)
->riskAccepted()
->create([
'workspace_id' => (int) $tenant->workspace_id,
'subject_external_id' => 'spec327-exception-finding',
]);
FindingException::query()->create([
'workspace_id' => (int) $tenant->workspace_id,
'managed_environment_id' => (int) $tenant->getKey(),
'finding_id' => (int) $finding->getKey(),
'requested_by_user_id' => (int) $user->getKey(),
'owner_user_id' => null,
'status' => FindingException::STATUS_PENDING,
'current_validity_state' => FindingException::VALIDITY_MISSING_SUPPORT,
'request_reason' => 'Exception needs governance evidence',
'requested_at' => now()->subDay(),
'review_due_at' => now()->addDay(),
'evidence_summary' => ['reference_count' => 0],
]);
$this->actingAs($user)
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
->get(GovernanceInbox::getUrl(panel: 'admin').'?family=finding_exceptions')
->assertOk()
->assertSee('Review accepted-risk decision')
->assertSee('Exception needs governance evidence')
->assertSee('Pending exception')
->assertSee('Evidence missing')
->assertSee('Review accepted risk')
->assertSee('Diagnostics')
->assertSee('Collapsed')
->assertDontSee('raw payload')
->assertDontSee('stack trace')
->assertDontSee('provider secret')
->assertDontSee('internal exception')
->assertDontSee('debug metadata');
});
it('renders visible governance attention sections on the governance inbox page', function (): void {
$alphaTenant = ManagedEnvironment::factory()->create([
'status' => 'active',

View File

@ -0,0 +1,50 @@
# Requirements Checklist: Spec 327 - Governance Inbox Decision-First Workbench Productization
**Purpose**: Validate preparation artifact quality before implementation.
**Created**: 2026-05-18
**Feature**: `specs/327-governance-inbox-decision-first-workbench-productization/spec.md`
## Content Quality
- [x] No implementation details leak into product requirements beyond required repo constraints.
- [x] User value and operator workflow are clear.
- [x] Scope is bounded to one existing runtime surface.
- [x] Non-goals explicitly prevent backend/workflow overbuild.
- [x] Dependencies and historical specs are listed.
## Repo Truth And Safety
- [x] Existing route/class/view/builder are named.
- [x] Repo truth map exists and uses required classifications.
- [x] No new persisted truth is proposed.
- [x] No migrations/packages/env/queues/scheduler/storage changes are expected.
- [x] No legacy tenant query alias support is allowed.
## Workspace / Environment Contract
- [x] Clean workspace-wide entry is specified.
- [x] Canonical `environment_id` filter is specified.
- [x] Visible chip and clear filter are specified.
- [x] Legacy aliases are rejected.
- [x] Cross-workspace environment guard is specified.
## RBAC / Audit / Diagnostics
- [x] Existing capabilities and policies remain authoritative.
- [x] Unauthorized action behavior is specified.
- [x] Diagnostics are collapsed/hidden by default.
- [x] Dangerous actions are out of scope unless spec/plan are updated.
- [x] No raw payloads/provider secrets/debug traces are default-visible.
## Testability
- [x] Feature tests are listed.
- [x] Browser smoke flows are listed.
- [x] Navigation/scope guard tests are listed.
- [x] `pint --dirty` and `git diff --check` are listed.
- [x] Full-suite status must be reported honestly.
## Readiness Decision
- [x] Spec is ready for implementation planning.
- [x] No open question blocks a bounded implementation loop.

View File

@ -0,0 +1,305 @@
# Implementation Plan: Spec 327 - Governance Inbox Decision-First Workbench Productization
**Branch**: `327-governance-inbox-decision-first-workbench-productization` | **Date**: 2026-05-18 | **Spec**: `specs/327-governance-inbox-decision-first-workbench-productization/spec.md`
**Input**: User-provided Spec 327 and repo inspection.
## Summary
Productize the existing workspace-scoped Governance Inbox into a decision-first operator workbench. The implementation must keep the current route and source-family truth, introduce no backend foundation, and make the first viewport answer:
```text
What decision clears the highest-priority item?
```
The workbench will elevate selected/highest-priority item detail, reason, impact, owner/due, evidence state, accepted-risk/exception state, and a single next action, while keeping the existing queue/source sections as secondary context and diagnostics collapsed.
## Technical Context
**Language/Version**: PHP 8.4.15, Laravel 12.52.0.
**Primary Dependencies**: Filament 5.2.1, Livewire 4.1.4, Pest 4.3.1, Tailwind CSS 4.2.2.
**Storage**: PostgreSQL; no schema change expected.
**Testing**: Pest 4 Feature/Livewire/Browser tests.
**Validation Lanes**: confidence and browser; targeted navigation guard tests.
**Target Platform**: Laravel Sail locally; Dokploy/container deployment posture unchanged.
**Project Type**: Laravel monolith under `apps/platform`.
**Performance Goals**: DB-only page render; no Graph calls during render; no extra heavy query family beyond existing inbox source queries unless bounded and eager-loaded.
**Constraints**: No new persisted truth, no migration, no packages, no queue/scheduler/storage/env changes, no legacy alias support.
**Scale/Scope**: One existing Filament page and its feature-local builder/view/tests.
## UI / Surface Guardrail Plan
- **Guardrail scope**: changed existing operator-facing strategic surface.
- **Affected routes/pages/actions/states/navigation/panel/provider surfaces**:
- `/admin/governance/inbox`
- `apps/platform/app/Filament/Pages/Governance/GovernanceInbox.php`
- `apps/platform/resources/views/filament/pages/governance/governance-inbox.blade.php`
- `apps/platform/app/Support/GovernanceInbox/GovernanceInboxSectionBuilder.php`
- **No-impact class, if applicable**: N/A.
- **Native vs custom classification summary**: Native Filament page plus existing Blade composition; no new UI framework.
- **Shared-family relevance**: status messaging, action links, evidence/proof links, OperationRun links, workspace/environment filter chip, navigation context, diagnostics disclosure.
- **State layers in scope**: page payload, URL query (`environment_id`, `family`), selected/highest-priority item state, diagnostics disclosure.
- **Audience modes in scope**: operator-MSP, manager, auditor/support reviewer where authorized.
- **Decision/diagnostic/raw hierarchy plan**: decision-first, proof/evidence second, diagnostics/support raw third.
- **Raw/support gating plan**: collapsed by default and capability-gated where existing capabilities support access.
- **One-primary-action / duplicate-truth control**: selected/highest-priority item owns one primary next action; queue rows/source links remain secondary.
- **Handling modes by drift class or surface**: review-mandatory for UI-028 strategic surface; document-in-feature for any UI coverage registry no-change decision.
- **Repository-signal treatment**: Spec 325 target image is visual direction only; runtime claims must be repo-verified or unavailable.
## Constitution Check
- **Inventory-first, snapshots-second**: N/A, no Graph or snapshot changes.
- **Read/write separation by default**: Page remains read-first. Any unexpected mutation requires spec/plan update, confirmation, authorization, audit, notification, and tests.
- **Single Contract Path to Graph**: No Graph calls may be added.
- **Deterministic Capabilities**: Reuse existing `Capabilities`, `CapabilityResolver`, `WorkspaceCapabilityResolver`, policies, and resource authorization.
- **Proportionality / anti-bloat**: No new source of truth, persisted entity, enum/status family, public abstraction, or cross-domain UI framework.
- **Workspace isolation**: Clean workspace URL stays workspace-wide; `environment_id` is resolved through current workspace and actor entitlement.
- **Tenant/environment language**: Runtime copy must avoid tenant as platform context; provider-specific tenant wording only where explicitly provider-bound.
- **OperationRun UX**: Deep links only through `OperationRunLinks`; no operation start or lifecycle changes.
- **UI-COV-001**: Existing strategic surface UI-028 changes; active spec package must carry repo-truth map, tests, and browser screenshots, and implementation close-out must decide whether route inventory/coverage matrix updates are needed.
- **TEST-GOV-001**: Targeted Feature and Browser tests are explicit; no broad heavy-governance lane unless implementation reveals structural risk.
## Current Repo Truth Summary
Existing verified surfaces:
- `GovernanceInbox` is a Filament `Page` with slug `governance/inbox`.
- `governance-inbox.blade.php` currently renders header/scope chips, family filter pills, and section cards/entries.
- `GovernanceInboxSectionBuilder` already assembles:
- `assigned_findings`
- `intake_findings`
- `finding_exceptions`
- `stale_operations`
- `alert_delivery_failures`
- `review_follow_up`
- `WorkspaceHubEnvironmentFilter::fromRequest()` accepts canonical `environment_id`, scopes to current workspace, and rejects inaccessible/cross-workspace IDs.
- Navigation tests already cover canonical environment filter, clear filter, legacy alias rejection, and workspace hub no-drift behavior.
- Governance Inbox already uses `CanonicalNavigationContext` and `OperationRunLinks` for source handoff.
Known current productization gap:
- The page is still section/queue-first. It does not yet consistently promote a selected/highest-priority item with decision, reason, impact, owner/due, evidence path, exception state, and a single next action ahead of the queue.
## Existing Repository Surfaces Likely Affected
Runtime files, only during later implementation:
- `apps/platform/app/Filament/Pages/Governance/GovernanceInbox.php`
- `apps/platform/resources/views/filament/pages/governance/governance-inbox.blade.php`
- `apps/platform/app/Support/GovernanceInbox/GovernanceInboxSectionBuilder.php`
- `apps/platform/resources/lang/en/*` and `apps/platform/resources/lang/de/*` only if current project pattern requires localized strings for new stable copy.
Tests, only during later implementation:
- `apps/platform/tests/Feature/Governance/GovernanceInboxPageTest.php`
- `apps/platform/tests/Feature/Governance/GovernanceInboxAuthorizationTest.php`
- `apps/platform/tests/Feature/Governance/GovernanceInboxNavigationContextTest.php`
- `apps/platform/tests/Feature/Governance/GovernanceInboxNavigationContextConvergenceTest.php`
- `apps/platform/tests/Feature/Navigation/WorkspaceHubEnvironmentFilterContractTest.php`
- `apps/platform/tests/Feature/Navigation/WorkspaceHubClearFilterContractTest.php`
- `apps/platform/tests/Browser/Spec327GovernanceInboxProductizationSmokeTest.php`
Spec/UI artifacts:
- `specs/327-governance-inbox-decision-first-workbench-productization/repo-truth-map.md`
- screenshot artifacts under `specs/327-governance-inbox-decision-first-workbench-productization/artifacts/screenshots/`
- optional UI coverage registry updates only if implementation materially changes route/archetype/coverage state.
## Domain / Model Implications
- No new model, table, migration, enum, status family, source of truth, or persisted display state.
- Workbench item state must derive from existing source payloads:
- `Finding`: title/subject, status, severity, owner/assignee, due date, operation/evidence JSON where available.
- `FindingException`: status, current validity, owner, review due, request reason, current decision, evidence references.
- `OperationRun`: problem class, status/outcome, run identifier, managed environment, canonical link.
- `AlertDelivery`: failed status, event type/title/error text, managed environment/workspace.
- `ManagedEnvironmentTriageReview` / `EnvironmentReview`: review follow-up state and customer review destination.
- `EvidenceSnapshot` / `FindingExceptionEvidenceReference`: proof path only where linked and authorized.
- If exact evidence or owner/due is missing for a family, render explicit unavailable/missing states.
## UI / Filament Implications
- Filament v5 and Livewire v4.0+ compliance must be preserved.
- Panel providers remain in `apps/platform/bootstrap/providers.php`; no panel provider changes expected.
- No globally searchable resource is added or changed. Related resources must remain either disabled for global search or backed by safe View/Edit pages.
- The layout should use Filament sections/cards/badges/buttons and Tailwind utility classes consistent with existing pages; no heavy one-off CSS.
- Header must stay short:
- `Governance Inbox`
- workspace label
- scope label: workspace-wide or filtered environment
- visible filter chip and clear action when filtered
- Main workbench must render before queue sections.
- Summary cards should be compact and action-relevant.
- Right-side detail panel should be desktop aside and mobile stack.
- Diagnostics must be collapsed by default.
## Livewire / Page State Implications
- Existing `tenantId` internal property may remain as implementation detail only; runtime URL/filter language must be `environment_id`.
- Existing `family` query filter can remain if it does not conflict with `environment_id` contract.
- Selected/highest-priority state should be deterministic on page load. If interactive selection is implemented, it must not introduce persisted state or break reload/back/forward behavior.
- Clear filter must remove `environment_id` and any environment-like table/session filter state through existing helpers.
## OperationRun / Monitoring Implications
- No new `OperationRun` creation or lifecycle transition.
- No queued/running/terminal DB notification changes.
- Any operation proof link must use existing `OperationRunLinks` and authorization.
- Raw `OperationRun.context`, `failure_summary`, or payload-like data must not be default-visible.
## RBAC / Policy Implications
Reuse existing authorization:
- Workspace page access through `WorkspaceCapabilityResolver::isMember()`.
- Environment access through `User::canAccessTenant()` and current accessible environments.
- Findings through `Capabilities::TENANT_FINDINGS_VIEW` and `FindingPolicy`.
- Assignment actions, if shown, through `Capabilities::TENANT_FINDINGS_ASSIGN` / policy.
- Finding exceptions/accepted risks through `Capabilities::FINDING_EXCEPTION_VIEW`, `FINDING_EXCEPTION_MANAGE`, and `FINDING_EXCEPTION_APPROVE` as appropriate.
- Evidence through `Capabilities::EVIDENCE_VIEW` and `EvidenceSnapshotPolicy`.
- Alerts through `Capabilities::ALERTS_VIEW`.
- Diagnostics through `Capabilities::SUPPORT_DIAGNOSTICS_VIEW` where any diagnostics UI is exposed.
No new permission semantics should be added unless implementation proves existing capabilities cannot express the action and spec/plan are updated first.
## Audit / Evidence / Disclosure Implications
- No new audit event is required for read-only page rendering unless current page-open audit conventions are extended repo-wide.
- Evidence should appear as proof path/state:
- linked
- missing
- stale
- unavailable
- not required
- Do not show raw provider payloads, debug metadata, internal exception traces, provider secrets, raw OperationRun payloads, or stack traces by default.
- If diagnostics disclosure is present, it must be collapsed and capability-aware.
## Data / Migration Implications
Expected outcome:
- No migrations.
- No seeders.
- No data backfills.
- No packages.
- No env vars.
- No queues/scheduler/storage changes.
- No deployment asset changes.
- No backwards compatibility layer.
- No legacy tenant query alias support.
If implementation discovers an actual schema need, stop and update spec/plan/tasks/repo-truth-map first. Default decision remains no migration.
## Localization / Copy Implications
- Runtime copy must be concise and operator-safe.
- Avoid platform-context `tenant` wording. Use `Workspace` and `Environment` for shell/filter/product context.
- Provider-bound tenant wording may remain only when describing an external Microsoft/Entra tenant identifier or provider payload outside the default decision view.
- Add EN/DE localization only if the surrounding files already route stable page copy through language files; otherwise keep localized scope as implementation-local and document the decision.
## Implementation Phases
### Phase 1 - Repo Truth And Current UI Audit
- Re-read this spec, plan, tasks, and `repo-truth-map.md`.
- Inspect current `GovernanceInbox`, view, builder, related models, policies, and tests.
- Update `repo-truth-map.md` before runtime changes if implementation discovers new source truth or gaps.
- Confirm no migration/package/env/queue/storage need.
### Phase 2 - Tests First
- Add tests for repo truth map existence.
- Add Feature/Livewire tests for decision-first workbench, right detail panel, queue context, diagnostics hidden, evidence state, accepted-risk/exception state, RBAC action visibility, environment filter, legacy aliases, cross-workspace guard, and tenant-copy guard.
### Phase 3 - Layout Productization
- Refactor the existing page into:
- header/scope
- main decision workbench
- summary cards
- queue/table context
- right detail/decision panel
- collapsed diagnostics disclosure
- Keep existing family/queue context and source links.
### Phase 4 - Data Binding
- Bind workbench and panel to repo-verified sources.
- Render unavailable states for missing owner/due/evidence/decision data.
- Do not create synthetic success, risk, evidence, or decision claims.
### Phase 5 - Action Hierarchy And RBAC
- Show one primary next action per selected/highest-priority item.
- Link only to existing, authorized source routes/actions.
- Keep bulk/source actions secondary.
- Do not introduce destructive actions.
### Phase 6 - Scope / Filter Integration
- Preserve clean workspace-wide entry.
- Preserve `?environment_id=` filter, visible chip, clear filter, reload/back/forward behavior.
- Preserve legacy alias rejection and cross-workspace guard.
### Phase 7 - Browser Smoke And Screenshots
- Add targeted Browser smoke for clean, filtered, clear/reload, selected item/detail panel, diagnostics hidden, table secondary, and no platform-context tenant wording.
- Save screenshots under the spec artifacts path when generated.
### Phase 8 - Validation And Close-Out
- Run targeted Feature/navigation tests, Browser smoke, filtered guard tests, `pint --dirty`, and `git diff --check`.
- Report full suite status honestly if not run.
- Record no migrations/seeders/packages/env/queues/scheduler/storage/deployment asset/backcompat/legacy alias support.
## Testing Strategy
Required tests:
- `it('documents_governance_inbox_repo_truth_map')`
- `it('renders_governance_inbox_decision_first_workbench')`
- `it('renders_governance_inbox_decision_detail_panel')`
- `it('keeps_governance_inbox_queue_table_available_as_secondary_context')`
- `it('governance_inbox_hides_raw_diagnostics_by_default')`
- `it('governance_inbox_shows_accepted_risk_or_exception_state')`
- `it('governance_inbox_shows_evidence_state_without_raw_payload')`
- `it('governance_inbox_respects_capabilities_for_primary_actions')`
- `it('governance_inbox_supports_canonical_environment_filter')`
- `it('governance_inbox_rejects_legacy_environment_aliases')`
- `it('governance_inbox_rejects_cross_workspace_environment_filter')`
- `it('governance_inbox_does_not_use_tenant_as_platform_context_copy')`
Required Browser smoke:
- `tests/Browser/Spec327GovernanceInboxProductizationSmokeTest.php`
Browser flows:
1. Clean workspace Governance Inbox.
2. Environment-filtered Governance Inbox.
3. Clear filter and reload.
4. Decision workbench and right detail panel visible.
5. Diagnostics hidden by default.
6. Queue/table context remains visible lower/secondary.
7. No platform-context tenant wording.
## Rollout Considerations
- Runtime page-only productization; no deployment asset changes expected.
- No `filament:assets` deployment change expected because no registered Filament assets are planned.
- If implementation adds registered Filament assets unexpectedly, stop and update spec/plan first, then include `cd apps/platform && php artisan filament:assets` in deployment notes.
- Staging validation should include targeted Browser smoke for light mode and workspace/environment filter behavior before production promotion.
## Risk Controls
- **False green risk**: Use only repo-backed success/ready labels; otherwise render unavailable or attention states.
- **RBAC leakage risk**: Derive counts after capability/environment scoping; hidden families must not leak counts.
- **Scope drift risk**: Reuse existing `WorkspaceHubEnvironmentFilter` and clear-filter helpers; preserve Spec 314-322 tests.
- **Diagnostics leakage risk**: Assert raw payload/debug/provider-secret/internal exception text absent by default.
- **UI overbuild risk**: Keep composition page-local and Filament-native; no new framework.
- **Test cost risk**: Use targeted Feature and Browser tests; avoid heavy fixture defaults.
## Gate Review
Candidate Selection Gate: expected pass. Direct user-supplied Spec 327, roadmap-aligned, not previously specced as `327`, related completed specs preserved, scope is one existing page.
Spec Readiness Gate: expected pass when `spec.md`, `plan.md`, `tasks.md`, `repo-truth-map.md`, and `checklists/requirements.md` exist and preparation analysis has no blocking issues.

View File

@ -0,0 +1,99 @@
# Spec 327 Repo Truth Map
Status: implemented
Created: 2026-05-18
Purpose: classify each Governance Inbox decision-first workbench element before runtime implementation. This map is based on repository inspection only; implementation must update it before runtime changes if new source gaps appear.
## Classification Legend
- `repo-verified`: exact runtime source exists and was inspected.
- `foundation-real`: backend model/service/policy exists, but exact page binding still needs implementation verification.
- `derived from existing model`: display value can be derived from existing persisted/domain truth.
- `empty state / unavailable`: no safe source/action exists for v1; show explicit unavailable state or omit.
- `deferred future capability`: outside Spec 327 and must not be shown as live runtime truth.
## Required Data Areas
| Data area | Repo source | Preparation finding |
|---|---|---|
| Findings | `Finding`, `FindingResource`, `GovernanceInboxSectionBuilder::assignedFindingsSection()` and `intakeFindingsSection()` | repo-real for subject/title, status, owner/assignee, due, environment, source links, severity/family fields |
| Finding Exceptions / Accepted Risks | `FindingException`, `FindingExceptionDecision`, `FindingExceptionEvidenceReference`, `FindingExceptionsQueue`, `FindingExceptionResource` | repo-real for status, validity, owner, request reason, due/expiry, current decision, evidence refs |
| Governance Inbox current state | `GovernanceInbox`, `governance-inbox.blade.php`, `GovernanceInboxSectionBuilder` | route/page/view/builder exist; current layout is section/queue-first |
| Decision Register / decisions | `DecisionRegister`, `FindingExceptionDecision`, Specs 265/306/307/308 | foundation-real for decision-related links/states; only show if existing source route/action is repo-real and authorized |
| Evidence links | `EvidenceSnapshot`, `EvidenceSnapshotItem`, `FindingExceptionEvidenceReference`, `EvidenceSnapshotPolicy`, `Capabilities::EVIDENCE_VIEW` | foundation-real; exact inbox binding must be verified during implementation |
| OperationRun links | `OperationRun`, `OperationRunLinks`, `RelatedNavigationResolver` | repo-real for stale/terminal follow-up and proof deep links |
| Workspace / Environment filter state | `WorkspaceContext`, `WorkspaceHubEnvironmentFilter`, `WorkspaceHubFilterStateResetter`, `WorkspaceHubRegistry`, filter chip partial | repo-real canonical `environment_id`, clear filter, alias rejection, cross-workspace guard |
| Owners / assignments | `Finding.owner_user_id`, `Finding.assignee_user_id`, `FindingException.owner_user_id`, resource display helpers | repo-real where fields are present; unavailable otherwise |
| Due dates | `Finding.due_at`, `FindingException.review_due_at`, `FindingException.expires_at` | repo-real where fields are present; unavailable otherwise |
| Statuses / priorities / severities | source model status/severity fields and builder ordering | derived; no new persisted status family |
| Alerts | `AlertDelivery`, `AlertDeliveryResource`, `Capabilities::ALERTS_VIEW` | repo-real for failed delivery attention family |
| Reviews / review follow-up | `ManagedEnvironmentTriageReview`, `EnvironmentReview`, `EnvironmentReviewRegisterService`, `CustomerReviewWorkspace` | repo-real for follow-up and review workspace handoff |
## UI Element Map
| UI element | Source model/service/page | Status source | Authorization/capability | Workspace/Environment scope | Evidence/OperationRun/Audit link | Fallback/empty state | Classification |
|---|---|---|---|---|---|---|---|
| Governance Inbox route | `GovernanceInbox` page, slug `governance/inbox` | Filament page registration | workspace membership via `WorkspaceCapabilityResolver::isMember()` | `WorkspaceContext` session; optional `environment_id` | no direct audit found during prep | redirect workspace chooser, 404, or 403 per existing behavior | repo-verified |
| Header title | `governance-inbox.blade.php`, page title | static page copy | page access | workspace-wide or filtered | none | static title | repo-verified |
| Workspace chip | `appliedScope()['workspace_label']` | current `Workspace` name | page access | workspace session | none | omit if unavailable | repo-verified |
| Environment filter chip | `workspace-hub-environment-filter-chip` partial | `WorkspaceHubEnvironmentFilter::fromRequest()` | actor must access environment | `?environment_id={id}` only | none | no chip on clean URL | repo-verified |
| Clear filter action | `pageUrl(['environment_id' => null])`, shared chip | clean URL generated by page/helper | page access | removes canonical filter | none | omit when unfiltered | repo-verified |
| Legacy alias rejection | `WorkspaceHubRegistry`, `WorkspaceHubFilterStateResetter`, navigation tests | forbidden query keys | page/source access | aliases do not set filter | none | workspace-wide view or safe 404 | repo-verified |
| Cross-workspace environment guard | `WorkspaceHubEnvironmentFilter::fromRequest()` | environment scoped by workspace and `canAccessTenant()` | workspace and environment entitlement | current workspace only | none | `NotFoundHttpException` | repo-verified |
| Main decision question | new page/view copy | static stable copy | page access | current scope | none | visible even empty with honest state | empty state / unavailable until implemented |
| Highest-priority/selected item | existing section entries plus builder ordering | entry `urgency_rank`, source model status/due | source-family capabilities | current workspace/filter | source links | no selected item / no visible decisions | derived from existing model |
| Decision title | `Finding` subject, `FindingException` finding label, `OperationRun` problem class, alert title, review family label | existing entry `headline` | source-family capability | current workspace/filter | source route | fallback source identifier | repo-verified |
| Reason | `FindingException.request_reason`, operation problem class, alert error/event, review row state, finding status/reopened/due | source fields and builder summaries | source-family capability | current workspace/filter | source route/proof links | `Reason unavailable` if absent | derived from existing model |
| Impact | source severity/status/validity/problem class/review follow-up | `Finding.severity`, `FindingException.current_validity_state`, `OperationRun::problemClass()`, review state | source-family capability | current workspace/filter | source route/proof links | `Impact unavailable` if no safe mapping | derived from existing model |
| Owner | `Finding.ownerUser`, `Finding.assigneeUser`, `FindingException.owner` | owner/assignee user relationships | source-family capability | environment/workspace scoped | source route | `Owner missing` or `Owner unavailable` | repo-verified |
| Due date | `Finding.due_at`, `FindingException.review_due_at`, `FindingException.expires_at` | model casts and resource relative-time helpers | source-family capability | environment/workspace scoped | source route | `Due date unavailable` | repo-verified |
| Evidence state | `Finding.evidence_jsonb`, `FindingException.evidence_summary`, `FindingExceptionEvidenceReference`, `EvidenceSnapshot` | existing JSON/relations where linked | `Capabilities::EVIDENCE_VIEW` for evidence links | environment/workspace scoped | evidence snapshot/reference links if authorized | `Evidence missing`, `Unavailable`, or omit | foundation-real |
| Evidence path | evidence refs, snapshots, review context, source routes | relation presence and policy checks | `EVIDENCE_VIEW` plus source policies | current workspace/filter | evidence/operation/review links only | unavailable rows | foundation-real |
| Operation proof | `OperationRun`, `OperationRunLinks` | run relation or operation attention entry | existing operation entitlement checks | workspace and environment entitlement | canonical operation links | `Operation proof unavailable` | repo-verified |
| Accepted-risk / exception state | `FindingException.status`, `current_validity_state`, `Finding.status = risk_accepted` | existing constants and validity fields | `FINDING_EXCEPTION_VIEW/MANAGE/APPROVE`, findings view | environment/workspace scoped | current decision/evidence references if authorized | `No exception` or unavailable | repo-verified |
| Decision Register link/action | `DecisionRegister`, `FindingExceptionDecision`, existing decision routes/tests | current decision/link presence | existing decision/finding exception capabilities | workspace hub with optional filter | decision proof/run links where existing | omit / `No active decision record` | foundation-real |
| Primary next action | existing source routes/actions from builder entries | destination URL/action label per family | source route/action authorization | current workspace/filter | source route/proof links | `Next action unavailable` | repo-verified for open-source links; derived for selected action label |
| Assign owner action | `FindingPolicy::assign`, `Capabilities::TENANT_FINDINGS_ASSIGN` | existing capability/policy | tenant/environment capability | source environment | audit on source action if existing | hide/unavailable unless source action exists | foundation-real |
| Create/update exception action | `FindingExceptionPolicy`, `Capabilities::FINDING_EXCEPTION_MANAGE/APPROVE`, existing source surfaces | existing policy and source action | workspace/environment capability | source environment | source audit if action exists | hide/unavailable unless source action exists | foundation-real |
| Open evidence action | `EvidenceSnapshotResource`, `EvidenceSnapshotPolicy` | linked evidence and capability | `EVIDENCE_VIEW` | source environment/workspace | evidence detail route | hide/unavailable | foundation-real |
| Open diagnostics action | `Capabilities::SUPPORT_DIAGNOSTICS_VIEW` if exposed | no current default inbox raw diagnostics | support diagnostics capability | current scope | source diagnostics only if repo-real | collapsed/hidden | empty state / unavailable |
| Summary card: open decisions | current selected/available entries count | builder `total_count` or filtered item count | source-family capabilities | current scope | source links | `0 visible decisions` | repo-verified as visible attention count; label must avoid unsupported claim |
| Summary card: critical/high priority | `Finding.severity`, exception validity/status, operation problem class | model fields | source-family capabilities | current scope | source links | unavailable if no safe cross-family mapping | derived from existing model |
| Summary card: overdue | `Finding.due_at`, `FindingException.review_due_at/expires_at` | datetime fields | source-family capabilities | current scope | source links | `No due dates available` | derived from existing model |
| Summary card: owner missing | finding/exception owner fields | nullable owner/assignee IDs | source-family capabilities | current scope | source links | unavailable for families without owner source | derived from existing model |
| Summary card: evidence missing | evidence refs/summary/snapshot presence | evidence fields/relations | source-family and evidence capabilities | current scope | evidence links | unavailable where not supported | foundation-real |
| Queue/table context | existing sections/entries rendered in Blade | builder sections and entries | source-family capabilities | current scope and family filter | source routes | existing calm empty states | repo-verified |
| Right decision/detail panel | new layout over selected/highest entry | selected entry and source-specific derived fields | source-family capabilities | current scope | source/evidence/operation links | panel shows unavailable states | empty state / unavailable until implemented |
| Diagnostics disclosure | new collapsed section if needed | safe copy only unless authorized diagnostics source exists | `SUPPORT_DIAGNOSTICS_VIEW` if raw detail exists | current scope | source routes only | collapsed/hidden | empty state / unavailable |
| Raw provider payloads | raw Graph/provider payloads | not safe default | support-only future | N/A | N/A | never default-visible | deferred future capability |
| Platform-context tenant copy guard | runtime copy/tests | string assertions | N/A | page copy | N/A | use Workspace/Environment | repo-verified need; implementation test required |
## Required Runtime Element Decisions
| Element | v1 decision |
|---|---|
| New persisted inbox item | deferred future capability; do not build |
| New Decision Register workflow | deferred future capability; do not rebuild |
| AI prioritization | deferred future capability; do not show |
| Owner/due/evidence where absent | explicit unavailable/missing state; do not invent |
| Green success state | allowed only for exact repo-backed proof; otherwise avoid |
| Diagnostics | collapsed/hidden by default and capability-aware if exposed |
| Dangerous/mutating actions | do not add unless spec/plan updated first |
| Legacy query aliases | rejected/neutralized; do not support |
## Implementation Update Rule
If implementation discovers that a planned UI element has no safe source, no authorization path, or would require new persisted truth, the element must become `empty state / unavailable` or `deferred future capability`. Do not create backend foundation inside Spec 327 without updating `spec.md`, `plan.md`, and this map first.
## Implementation Close-Out
Implemented on 2026-05-18 against the existing `/admin/governance/inbox` Filament page.
- The workbench now derives selected/highest-priority state from existing `GovernanceInboxSectionBuilder` entries; no new persisted inbox item, status family, queue, migration, or backend source of truth was added.
- Findings and finding exceptions now expose repo-backed decision labels, reasons, impact, owner/due, evidence, accepted-risk/exception, and primary source-action labels. Unsupported fields render explicit missing or unavailable states.
- Alert delivery failure copy no longer exposes raw provider/error diagnostics in the default inbox entry. Diagnostics remain collapsed and point operators back to authorized source surfaces.
- The page remains workspace-owned with optional canonical `environment_id` filtering. Legacy tenant/environment aliases were not added or supported.
- Primary actions are read-first source navigation links. No destructive, provider-changing, approve/reject, restore, delete, or remediation action was added.
- Browser proof and screenshots live under `specs/327-governance-inbox-decision-first-workbench-productization/artifacts/screenshots/`. The route inventory/design coverage registry were not changed because the route and UI-028 strategic-surface classification are unchanged.
- Follow-up refinement removed the duplicate in-view page heading and moved the first dominant experience to the decision/evidence workbench. Empty workbench state now renders one compact `No governance decisions need attention` decision state instead of primary zero-metric cards.
- Final browser proof keeps the empty state separate and adds a non-empty repo-backed finding fixture that visibly renders the decision title, reason, impact, missing owner, due date, missing evidence, accepted-risk state, and primary next action in the decision workbench plus right-side evidence panel. The required screenshot artifact is `artifacts/screenshots/governance-inbox-decision-workbench.png`.

View File

@ -0,0 +1,458 @@
# Feature Specification: Spec 327 - Governance Inbox Decision-First Workbench Productization
**Feature Branch**: `327-governance-inbox-decision-first-workbench-productization`
**Created**: 2026-05-18
**Status**: Draft
**Type**: Runtime UI productization / decision workbench / governance triage surface
**Runtime posture**: Narrow runtime UI implementation. Repo-based. No invented backend foundation.
**Input**: User-provided full Spec 327 draft.
## Dependencies And Historical Context
Depends on:
- Spec 250 - Decision-Based Governance Inbox v1, treated as historical inbox foundation.
- Spec 257 - Governance Decision Surface Convergence, treated as historical decision-home convergence foundation.
- Spec 314 - Workspace Hub Navigation Context Contract.
- Spec 315 - Environment CTA Explicit Filter Contract.
- Spec 316 - Workspace Hub Clear Filter Contract.
- Spec 317 - Legacy Tenant / Environment Context Cleanup.
- Spec 318 - Admin Surface Scope & Shell Context Audit.
- Spec 319 - Environment-Owned Surface Routing & Shell Context Contract.
- Spec 320 - Workspace-Owned Analysis Surface Registration & Shell Cutover.
- Spec 321 - Alerts / Audit Log Environment Filter Contract Decision.
- Spec 322 - Browser No-Drift Regression Guard.
- Spec 325 - Screenshot-Anchored Strategic Target Images.
- Spec 326 - Customer Review Workspace v1 Productization.
Repo truth adjustment: the user draft intentionally starts from Spec 325 target direction and the Spec 326 premium-layout lesson, but the repository already has a runtime `GovernanceInbox` page and a `GovernanceInboxSectionBuilder`. Spec 327 must productize that existing route and builder instead of creating a new workflow engine, ticket queue, Decision Register rebuild, persisted inbox item, or broad status taxonomy. Spec 325 premium references are visual calibration only; they are not runtime truth for counts, states, actions, RBAC, audit, evidence, or OperationRun behavior.
## Spec Candidate Check *(mandatory - SPEC-GATE-001)*
- **Problem**: Governance Inbox is repo-real and already aggregates findings, finding exceptions, stale or failed operations, alert delivery failures, and review follow-up, but the first-read experience still behaves like a sectioned queue rather than a decision-first workbench that answers which governance decision clears the highest-priority item.
- **Today's failure**: Operators can see attention families and source links, but they still need to infer reason, impact, owner, due date, evidence state, accepted-risk state, and one safest next action from section summaries and row sublines. Diagnostics/source routing can compete with the decision hierarchy.
- **User-visible improvement**: Governance operators get a polished workbench where the highest-priority or selected item leads with decision, reason, impact, owner/due, evidence, exception/accepted-risk state, and a single primary next action while raw diagnostics remain collapsed.
- **Smallest enterprise-capable version**: Productize only the existing `/admin/governance/inbox` page and its current builder/view payloads. Add a repo-truth map first, derive any display states from existing models/services, keep the current section/table context available, and add targeted Feature/Browser coverage for layout, scope, RBAC, diagnostics, and no tenant-context copy.
- **Explicit non-goals**: No backend workflow rebuild, no Decision Register rebuild, no helpdesk/PSA system, no AI prioritization, no new remediation automation, no new persisted inbox entity, no new decision taxonomy, no migrations, no packages, no env vars, no queues/scheduler/storage changes, no broad redesign of Customer Review Workspace, Operations Hub, Evidence Overview, Environment Dashboard, Baseline Compare, or Restore Safety Workflow.
- **Permanent complexity imported**: Feature-local page composition, feature-local display payloads if needed, targeted Feature tests, a Browser smoke test, screenshot artifacts, and `repo-truth-map.md`. No new persisted truth, no public abstraction, no enum/status family, no cross-domain UI framework.
- **Why now**: Spec 326 completed the customer review productization lane and explicitly deferred Governance Inbox as Spec 327. `docs/product/spec-candidates.md`, `docs/product/roadmap.md`, and Spec 325 all identify Governance Inbox decision experience as a P1/P0 strategic surface and the largest remaining operator workflow productization gap.
- **Why not local**: A copy-only patch or another section link would leave the primary operator question unresolved. A new task engine would overbuild. The narrow correct slice is a decision-first productization pass on the existing workspace-scoped Governance Inbox.
- **Approval class**: Workflow Compression.
- **Red flags triggered**: Cross-surface decision composition, evidence/action display, and strategic UI productization. Defense: scope is bounded to one existing page, existing truth sources, existing authorization, existing routes, existing workspace/environment filter contract, and explicit no-new-backend constraints.
- **Score**: Nutzen: 2 | Dringlichkeit: 2 | Scope: 2 | Komplexitaet: 2 | Produktnaehe: 2 | Wiederverwendung: 2 | **Gesamt: 12/12**
- **Decision**: approve.
## Candidate Source And Completed-Spec Guardrail
- **Candidate source**: Direct user-provided manual promotion for Spec 327, aligned with `decision-based-governance-inbox-v1` in `docs/product/spec-candidates.md`, the roadmap's Decision-Based Governance Inbox lane, and the Governance Inbox target brief from Spec 325.
- **Completed-spec check**: No `specs/327-*` package existed before generation. Related Specs 250, 257, and 314-326 contain historical/completed foundation or productization signals and must remain unchanged by this preparation.
- **Close alternatives deferred**: Operations Hub Decision-First Workbench, Evidence/Audit disclosure, Environment Dashboard/Baseline Compare, and Restore Safety Workflow productization are follow-up specs 328-331, not hidden scope.
- **Smallest viable implementation slice**: Existing Governance Inbox only, including header/scope clarity, main decision workbench, summary cards, queue/table context, right-side decision panel, diagnostics disclosure, RBAC-aware actions, canonical `environment_id` filter behavior, and tests/browser smoke.
## Spec Scope Fields *(mandatory)*
- **Scope**: workspace canonical-view governance decision workbench, optionally filtered by canonical `environment_id`.
- **Primary Routes**:
- Existing route: `/admin/governance/inbox`.
- Existing page class: `apps/platform/app/Filament/Pages/Governance/GovernanceInbox.php`.
- Existing view: `apps/platform/resources/views/filament/pages/governance/governance-inbox.blade.php`.
- Existing builder: `apps/platform/app/Support/GovernanceInbox/GovernanceInboxSectionBuilder.php`.
- **Data Ownership**:
- Findings truth: `Finding`, including owner, assignee, due date, severity, status, subject display, and linked operation runs.
- Finding exception / accepted-risk truth: `FindingException`, `FindingExceptionDecision`, `FindingExceptionEvidenceReference`.
- Operation proof truth: `OperationRun` and existing `OperationRunLinks`.
- Evidence proof truth: `EvidenceSnapshot`, `EvidenceSnapshotItem`, and existing evidence policies/resources where linked.
- Review follow-up truth: `ManagedEnvironmentTriageReview`, `EnvironmentReview`, `EnvironmentReviewRegisterService`, Customer Review Workspace links.
- Alert delivery truth: `AlertDelivery`, alert resource routes, and workspace alert capabilities.
- Workspace / environment filter truth: `WorkspaceContext`, `WorkspaceHubEnvironmentFilter`, `WorkspaceHubFilterStateResetter`, `WorkspaceHubRegistry`, and existing filter chip partial.
- Audit truth: existing source surfaces and policies only; this spec introduces no new mutation/audit action unless spec/plan are updated first.
- **RBAC**:
- Workspace membership required.
- Managed-environment entitlement required when an `environment_id` filter is applied or environment-bound data is rendered.
- Existing capabilities and policies remain authoritative for findings, finding exceptions/accepted risks, evidence, operation runs, reviews, alerts, diagnostics, assignment, and source actions.
- Non-member or cross-workspace environment access remains deny-as-not-found.
- Member with missing source capability must not see unauthorized actions or sensitive existence hints.
For canonical-view specs:
- **Default filter behavior when tenant-context is active**: Clean `/admin/governance/inbox` remains workspace-wide and must not inherit remembered environment context, Filament tenant context, session table filters, or legacy query aliases.
- **Explicit entitlement checks preventing cross-tenant leakage**: `?environment_id=` must resolve through the current workspace and actor entitlement; cross-workspace or inaccessible IDs return 404/no-access.
## 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
- [ ] 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**: `/admin/governance/inbox`, `GovernanceInbox`, `governance-inbox.blade.php`, `GovernanceInboxSectionBuilder`.
- **Current or new page archetype**: Strategic Surface / Findings-Inbox Workbench, matching `docs/ui-ux-enterprise-audit/page-reports/ui-004-governance-inbox.md`.
- **Design depth**: Strategic Surface.
- **Repo-truth level**: repo-verified page and foundation; individual runtime elements must be classified in `repo-truth-map.md`.
- **Existing pattern reused**: Filament Page, Filament sections, existing builder, existing source-surface links, existing workspace hub environment chip, existing capability resolvers, existing OperationRun links, existing resources/policies.
- **New pattern required**: no new runtime framework; page-local workbench composition only.
- **Screenshot required**: yes, Browser smoke screenshots under `specs/327-governance-inbox-decision-first-workbench-productization/artifacts/screenshots/`.
- **Page audit required**: no new full audit unless implementation materially changes route inventory; active spec artifacts and screenshots are sufficient if route/archetype remains UI-028.
- **Customer-safe review required**: operator-facing, but copy may feed customer review summaries. Keep customer/operator-safe labels and hide raw diagnostics.
- **Dangerous-action review required**: no new dangerous action expected. If implementation unexpectedly adds approve/reject/accept-risk/assign/close style mutations, spec/plan must be updated first and actions must use confirmation, server authorization, audit, notifications, and tests.
- **Coverage files updated or explicitly not needed**:
- [ ] `docs/ui-ux-enterprise-audit/route-inventory.md`
- [ ] `docs/ui-ux-enterprise-audit/design-coverage-matrix.md`
- [ ] `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`
- **Coverage decision for implementation**: implementation must either update UI coverage registry artifacts or document in this spec's close-out why UI-004 and Spec 325 target artifacts remain sufficient for the unchanged route/archetype.
## Cross-Cutting / Shared Pattern Reuse *(mandatory)*
- **Cross-cutting feature?**: yes.
- **Interaction class(es)**: status messaging, action links, governance queue summaries, evidence/proof links, OperationRun links, workspace/environment filter chip, diagnostics disclosure, navigation/back context.
- **Systems touched**: `GovernanceInbox`, `GovernanceInboxSectionBuilder`, `FindingResource`, `FindingExceptionsQueue`, `FindingExceptionResource`, `AlertDeliveryResource`, `CustomerReviewWorkspace`, `EnvironmentReviewResource`, `OperationRunLinks`, `CanonicalNavigationContext`, `WorkspaceHubEnvironmentFilter`, capability resolvers, and policies.
- **Existing pattern(s) to extend**: existing Governance Inbox section payloads, existing workspace hub filter chip, existing navigation context, source-surface routes, existing policy/capability checks.
- **Shared contract / presenter / builder / renderer to reuse**: `GovernanceInboxSectionBuilder`, `CanonicalNavigationContext`, `OperationRunLinks`, `WorkspaceHubEnvironmentFilter`, `WorkspaceHubFilterStateResetter`, `CapabilityResolver`, `WorkspaceCapabilityResolver`, existing Filament resources/routes.
- **Why the existing shared path is sufficient or insufficient**: Existing paths are sufficient for truth, authorization, and source routing. They are insufficient only in first-read page hierarchy; Spec 327 may add page-local payload shaping but must not create a new generic workflow or status framework.
- **Allowed deviation and why**: bounded page-local layout/view helper changes are allowed. New public cross-domain presenters, status taxonomies, or persisted item stores are not allowed.
- **Consistency impact**: Labels, badges, actions, and links must stay aligned with existing findings, exception, evidence, review, alert, and operation vocabulary.
- **Review focus**: Verify no fake metrics, no false green state, no raw diagnostics by default, no unauthorized action, no shell-scope regression, and no local duplicate of existing source-surface mutation semantics.
## OperationRun UX Impact *(mandatory)*
- **Touches OperationRun start/completion/link UX?**: secondary/deep-link semantics only; no new OperationRun creation, queueing, dedupe, completion, or lifecycle behavior.
- **Shared OperationRun UX contract/layer reused**: existing `OperationRunLinks` and existing operation resource routes only.
- **Delegated start/completion UX behaviors**: N/A - no operation start.
- **Local surface-owned behavior that remains**: show operation proof availability/unavailability and source links only when existing run records and authorization support it.
- **Queued DB-notification policy**: N/A.
- **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 governance workbench over existing environment-bound artifacts.
- **Seams affected**: display and routing over findings, finding exceptions, evidence, review, alert, and operation proof.
- **Neutral platform terms preserved or introduced**: workspace, environment filter, governance decision, evidence path, accepted risk, operation proof, diagnostics.
- **Provider-specific semantics retained and why**: existing Microsoft/Intune terms may appear only where the underlying finding/review/resource already uses them. Do not surface raw provider IDs, Graph payloads, tenant aliases, or provider diagnostics by default.
- **Why this does not deepen provider coupling accidentally**: no Graph calls, no provider contracts, no provider connection changes, and no new provider-shaped persistence.
- **Follow-up path**: provider readiness, environment dashboard, baseline compare, restore safety, and evidence/audit productization remain separate specs.
## 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 |
|---|---:|---|---|---|---:|---|
| Governance Inbox page | yes | Native Filament page plus existing Blade composition | decision queue, evidence/proof links, source routing | page, URL-query, derived payload | no | Existing route only |
| Header / scope area | yes | Filament section / Blade | workspace/environment context presentation | page, URL-query | no | Must keep Workspace shell ownership |
| Main decision workbench | yes | Filament section/cards/badges | status/readiness/action hierarchy | page payload | no | Derived labels only, no new persisted state |
| Summary cards | yes | Filament cards/badges | decision posture and count summaries | builder/page payload | no | Only repo-backed counts or unavailable states |
| Queue/table context | yes | existing sections/list; table-like queue may be refactored | specialist source surfaces | builder/page payload | no | Current table/queue remains secondary context |
| Right detail / decision panel | yes | native layout / Blade | evidence, accepted risk, decision summary, action links | page payload | no | Desktop aside, stacked on small screens |
| Diagnostics disclosure | yes | collapsed/progressive disclosure only | support/raw detail | page payload/action visibility | no | Authorized and collapsed by default |
## 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 |
|---|---|---|---|---|---|---|---|
| Governance Inbox | Primary Decision Surface | Operator decides what governance item should be cleared first and which source action is safest | highest-priority/selected item, decision, reason, impact, owner/due, evidence state, exception state, next action | source record, operation proof, evidence snapshot, audit/review context, diagnostics | Primary because it is the canonical workspace governance decision workbench | Follows pending governance decisions, not storage objects | Removes cross-page reconstruction before first action |
| Queue/list sections | Secondary Context | Operator scans remaining items after priority decision is visible | decision/finding label, environment, severity/priority, owner/due where available, evidence/exception state, next action | source page detail and diagnostics after open | Secondary because workbench leads with decision summary | Keeps source families visible without making table the only experience | Reduces equal-priority section overload |
| Diagnostics disclosure | Tertiary Evidence / Diagnostics | Support/operator confirms technical detail after the decision path | collapsed availability only | raw payloads/debug/support detail only if authorized and explicitly opened | Not primary; proof and diagnostics support decisions | Preserves audit depth without diagnostic dominance | Prevents default raw-console experience |
## 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 |
|---|---|---|---|---|---|---|---|
| Governance Inbox | operator-MSP, manager, auditor/support reviewer | decision title, reason, impact, owner/due, evidence state, accepted-risk state, environment scope, primary next action | collapsed source diagnostics and source-page detail | raw payloads, stack traces, provider secrets, internal exception traces, raw OperationRun payloads | context-aware source action such as review decision, open finding, review accepted risk, open evidence, or open operation proof | raw/support detail and unauthorized actions | main workbench states the blocker once; queue and panel add evidence/source proof |
| Right detail panel | operator-MSP, support reviewer where authorized | decision summary, proof path, accepted-risk state, owner/due, next action | operation/evidence/review context behind links/disclosure | raw/support details remain hidden or routed to existing authorized surfaces | same primary next action as selected item | raw diagnostics and unsupported links | panel expands the same selected item instead of creating another decision |
## 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 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Governance Inbox | Workbench / Workspace Decision | Decision-first governance queue | Review/open the highest-priority item | explicit primary action plus selected/row source link | optional row/select only if repo-real | right/detail panel and source links | none introduced; any future mutation must be confirmed/audited | `/admin/governance/inbox` | existing source routes only | active workspace, optional `environment_id` chip | Governance Inbox | decision, reason, impact, owner/due, evidence, exception, next action | none |
| Queue context | List / Table / Read-only decision queue | Secondary workbench context | Select/open source item | existing source route | allowed only if clear and accessible | row action or panel action | existing source surfaces only | `/admin/governance/inbox` | existing finding/exception/operation/alert/review routes | workspace + environment filter | Governance item | priority/severity/status, evidence/exception, owner/due | none |
| Diagnostics disclosure | Diagnostics / Support Raw | Collapsed diagnostic context | Expand proof detail if authorized | disclosure component | N/A | below/inside detail panel | none | same page | existing source diagnostics routes if any | authorized-only label | Diagnostics | collapsed status only | 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 |
|---|---|---|---|---|---|---|---|---|---|---|
| Governance Inbox | Workspace operator / MSP operator | Decide which governance item clears first and open the safest existing action path | Workspace decision workbench | What decision clears the highest-priority governance item? | decision, reason, impact, owner, due, evidence, exception, environment, next action | raw payloads, provider debug, stack traces, raw OperationRun payload, internal exception/debug metadata | decision urgency, evidence availability, risk/exception state, owner/due, source family, operation follow-up | none on the page by default; existing source surfaces own mutations | review decision/open finding/review accepted risk/open evidence/open operation proof as supported | none introduced |
## Proportionality Review *(mandatory when structural complexity is introduced)*
- **New source of truth?**: no.
- **New persisted entity/table/artifact?**: no. `repo-truth-map.md` is a preparation artifact, not runtime truth.
- **New abstraction?**: no public abstraction. Page-local private helpers are allowed only when they reduce Blade complexity and stay feature-local.
- **New enum/state/reason family?**: no domain state. Display states must be derived from existing `Finding`, `FindingException`, `EvidenceSnapshot`, `OperationRun`, `AlertDelivery`, `EnvironmentReview`, and current builder truth.
- **New cross-domain UI framework/taxonomy?**: no.
- **Current operator problem**: Governance Inbox needs a decision-first runtime surface that consumes existing truth without making unsupported decision, owner, evidence, accepted-risk, or success claims.
- **Existing structure is insufficient because**: Current page foundations show attention families but do not consistently elevate the selected/highest-priority decision, proof path, owner/due state, accepted risk, and one primary next action above queue diagnostics.
- **Narrowest correct implementation**: Refactor the existing page layout and derived payloads, bind to existing sources, hide diagnostics, and add targeted tests/browser smoke.
- **Ownership cost**: Feature-local layout/payload tests, one Browser smoke, and screenshot artifacts. No durable backend model or new framework cost.
- **Alternative intentionally rejected**: new ticket/helpdesk system, new Decision Register foundation, new workflow engine, AI prioritization, broad design system work, and persisted inbox items.
- **Release truth**: current-release runtime UI productization over existing governance inbox foundations.
### Compatibility posture
This feature assumes pre-production runtime posture. Backward compatibility, historical aliases, migration shims, dual-write logic, and legacy tenant query aliases are out of scope. Existing legacy query aliases (`tenant`, `tenant_id`, `managed_environment_id`, `environment`, `tenant_scope`, `tableFilters`) must not be supported for Governance Inbox filtering.
## Testing / Lane / Runtime Impact *(mandatory for runtime behavior changes)*
- **Test purpose / classification**: Feature, Filament/Livewire/HTTP, Browser.
- **Validation lane(s)**: confidence plus browser for critical workspace/environment UI/scope smoke.
- **Why this classification and these lanes are sufficient**: The change is a user-facing Filament page productization with RBAC, source truth, scope, and disclosure behavior. Feature tests prove data/scope/action rules; Browser smoke proves shell/filter/reload/disclosure/selected-panel behavior.
- **New or expanded test families**: additions under `tests/Feature/Governance/*` and `tests/Browser/Spec327GovernanceInboxProductizationSmokeTest.php`.
- **Fixture / helper cost impact**: reuse existing helpers and factories in `tests/Pest.php`; do not widen expensive defaults.
- **Heavy-family visibility / justification**: browser addition is explicit and named for Spec 327.
- **Special surface test profile**: `global-context-shell` plus decision-first disclosure.
- **Standard-native relief or required special coverage**: special coverage required for environment filter, clear/reload, highest-priority/selected decision, right detail panel, diagnostics default-hidden, and no platform-context tenant copy.
- **Reviewer handoff**: confirm diagnostics are collapsed, RBAC actions hide/disable correctly, no false green, clean workspace entry, canonical filter, clear filter, cross-workspace guard, and table/queue remains available as secondary context.
- **Budget / baseline / trend impact**: no expected material lane-cost shift beyond one targeted browser smoke.
- **Escalation needed**: document-in-feature if browser coverage becomes too expensive or requires fixture broadening.
- **Active feature PR close-out entry**: Smoke Coverage.
- **Planned validation commands**:
- `cd apps/platform && ./vendor/bin/sail artisan test tests/Feature/Governance tests/Feature/Navigation/WorkspaceHubEnvironmentFilterContractTest.php tests/Feature/Navigation/WorkspaceHubClearFilterContractTest.php --compact`
- `cd apps/platform && ./vendor/bin/sail artisan test tests/Browser/Spec327GovernanceInboxProductizationSmokeTest.php --compact`
- `cd apps/platform && ./vendor/bin/sail artisan test --filter='GovernanceInbox|WorkspaceHub|EnvironmentFilter|ClearFilter|LegacyTenant|Spec322' --compact`
- `cd apps/platform && ./vendor/bin/sail pint --dirty`
- `git diff --check`
## User Scenarios & Testing *(mandatory)*
### User Story 1 - Decide the highest-priority governance item (Priority: P1)
As a governance operator, I want the inbox to show the highest-priority or selected item with decision, reason, impact, owner, due, evidence, accepted-risk state, and one next action so I can clear work without reconstructing context across sections.
**Why this priority**: This is the core productization promise and delivers value even before optional detail refinements.
**Independent Test**: Seed findings, finding exceptions, evidence/operation links, and review follow-up; open Governance Inbox; verify the workbench question, selected/highest-priority item, reason, impact, evidence, exception state, owner/due or unavailable state, and primary action.
**Acceptance Scenarios**:
1. **Given** visible governance attention exists, **When** the operator opens clean Governance Inbox, **Then** the page asks `What decision clears the highest-priority item?` and shows the top decision with reason, impact, evidence, exception, owner/due, and next action.
2. **Given** owner, due date, or evidence data is absent, **When** the item is rendered, **Then** the page shows an honest unavailable/missing state rather than invented data.
3. **Given** no visible attention exists, **When** the operator opens the page, **Then** the empty state says no governance decisions need action in the current scope without false green success claims.
### User Story 2 - Keep workspace/environment scope explicit (Priority: P1)
As a workspace operator, I want Governance Inbox to stay workspace-owned while accepting a canonical environment filter so clean, filtered, clear, reload, and cross-workspace behavior remain predictable.
**Why this priority**: Specs 314-322 made this a hard product contract for workspace hub surfaces.
**Independent Test**: Open clean `/admin/governance/inbox`, filtered `?environment_id=`, clear filter, reload, legacy aliases, and cross-workspace IDs; verify workspace shell and chip behavior.
**Acceptance Scenarios**:
1. **Given** clean Governance Inbox URL, **When** the page loads, **Then** no Environment chip or remembered Environment filter appears.
2. **Given** `?environment_id={id}` for an entitled environment, **When** the page loads, **Then** Workspace shell remains active and a visible Environment filter chip appears.
3. **Given** a cross-workspace environment ID or legacy query alias, **When** the page loads, **Then** the cross-workspace ID is denied as not found and aliases do not create filter state.
### User Story 3 - Inspect proof without raw diagnostics by default (Priority: P1)
As an operator or support reviewer, I want evidence path and operation proof to be visible as links/states while raw diagnostics remain secondary and capability-gated.
**Why this priority**: Governance decisions need proof, but default raw details create support-console drift and leakage risk.
**Independent Test**: Render items with linked evidence/operation proof and assert evidence state/path is visible while raw payload, stack trace, provider secret, debug metadata, and internal exception text are absent by default.
**Acceptance Scenarios**:
1. **Given** an item has linked evidence or operation proof, **When** the detail panel renders, **Then** the evidence path or operation proof state/link appears only if authorized.
2. **Given** diagnostics are available, **When** the page first renders, **Then** raw diagnostics are collapsed or absent by default.
3. **Given** the actor lacks diagnostics/evidence capability, **When** the page renders, **Then** protected links/actions are hidden/disabled without leaking sensitive details.
### User Story 4 - Preserve queue context as secondary workbench content (Priority: P2)
As a governance operator, I want the existing queue/table context to remain available below or beside the workbench so I can scan other items after the top decision is clear.
**Why this priority**: The workbench must not regress existing routing and filter functionality.
**Independent Test**: Render the existing Governance Inbox fixtures and assert source families/rows still appear as secondary context with filters/search/source links intact.
**Acceptance Scenarios**:
1. **Given** multiple visible source families exist, **When** the workbench renders, **Then** existing family/queue context remains available and not replaced by an empty dashboard.
2. **Given** a family filter is selected, **When** the page renders, **Then** the workbench and secondary queue align to the filtered family without losing clear-filter behavior.
## Functional Requirements
- **FR-001**: Governance Inbox MUST remain on the existing `/admin/governance/inbox` route.
- **FR-002**: The page MUST render a decision-first workbench before the secondary queue/table context.
- **FR-003**: The workbench MUST include the question `What decision clears the highest-priority item?` or stable equivalent final copy.
- **FR-004**: The selected/highest-priority item MUST show decision/title, reason, impact, owner/due state, evidence state, accepted-risk/exception state, and primary next action.
- **FR-005**: Missing owner, due, evidence, decision, or accepted-risk data MUST render honest unavailable/missing states or be omitted; it MUST NOT be invented.
- **FR-006**: Summary cards MUST use only repo-supported counts/states, such as open decisions, overdue, awaiting review, owner missing, evidence missing, or accepted-risk follow-up.
- **FR-007**: Existing source-family/queue context MUST remain available as secondary workbench content.
- **FR-008**: A right-side decision/detail panel MUST be visible on desktop and stack on smaller screens.
- **FR-009**: Evidence/proof must be shown as proof path/state, not raw payload.
- **FR-010**: Diagnostics/raw details MUST be collapsed, hidden, or capability-gated by default.
- **FR-011**: Primary action MUST be singular and repo-real for the selected/highest-priority item.
- **FR-012**: Unauthorized actions MUST be hidden, disabled with existing helper text convention, or replaced by unavailable state; UI visibility is not security.
- **FR-013**: No new destructive or governance-impacting action may be added without updating spec/plan first and applying confirmation, server authorization, audit, notification, and tests.
- **FR-014**: Clean entry MUST remain workspace-wide with no active Environment shell and no remembered Environment fallback.
- **FR-015**: Filtered entry MUST use only `?environment_id={id}` with a visible chip and clear action.
- **FR-016**: Legacy query aliases (`tenant`, `tenant_id`, `managed_environment_id`, `environment`, `tenant_scope`, `tableFilters`) MUST NOT create filter state.
- **FR-017**: Cross-workspace or unauthorized `environment_id` MUST be rejected as safe not-found/no-access.
- **FR-018**: Runtime copy MUST avoid `tenant` as platform context copy. Provider-specific use remains allowed only when explicitly provider-bound.
- **FR-019**: Implementation MUST update `repo-truth-map.md` before runtime changes if new source gaps are discovered.
- **FR-020**: No migrations, seeders, packages, env vars, queues, scheduler, storage, deployment asset changes, backwards compatibility layer, or legacy alias support are expected.
## Non-Functional Requirements
- **NFR-001**: Page render MUST remain DB-only and MUST NOT perform Microsoft Graph or external provider calls.
- **NFR-002**: The layout MUST remain Filament v5 / Livewire v4 compatible and use native Filament/Tailwind primitives before custom UI.
- **NFR-003**: The workbench MUST remain responsive enough for the Filament shell; desktop gets a right-side detail panel and smaller viewports stack it below.
- **NFR-004**: Decision priority MUST NOT rely on color alone; status/action meaning must also be expressed in text.
- **NFR-005**: Raw diagnostics, provider payloads, stack traces, provider secrets, raw OperationRun payloads, and internal exception/debug metadata MUST NOT be default-visible.
- **NFR-006**: Query and section counts MUST be scoped by workspace, environment entitlement, and capability before rendering.
- **NFR-007**: Implementation MUST avoid broad fixture/helper defaults or hidden heavy-governance test cost.
## Auditability / Observability Requirements
- **AOR-001**: No new audit event is required for the read-first page productization unless implementation introduces a new mutation or extends an existing repo audit convention.
- **AOR-002**: Any unexpected mutating action MUST be added only after spec/plan update and must include server-side authorization, confirmation when destructive/high-impact, audit logging, user feedback, and tests.
- **AOR-003**: Operation proof links must use existing OperationRun truth and link helpers; this spec must not create or transition OperationRuns.
- **AOR-004**: Evidence, decision, and operation proof states must distinguish available proof from unavailable/missing proof without implying audit success.
## Data / Truth Requirements
Each visible runtime element must be classified as one of:
- `repo-verified`
- `foundation-real`
- `derived from existing model`
- `empty state / unavailable`
- `deferred future capability`
The authoritative map is `repo-truth-map.md` in this spec directory. If implementation discovers that a planned element has no safe source, no authorization path, or would require new persisted truth, the element must become `empty state / unavailable` or `deferred future capability`.
## RBAC / Capability Requirements
At minimum, implementation must verify existing capabilities/policies for:
- view Governance Inbox / workspace membership
- view findings
- assign owner where supported
- view/manage/approve finding exceptions or accepted risks
- view evidence
- view operation runs/proof
- open review/customer review context
- open decision record if supported
- view diagnostics/support detail
Do not introduce new permission semantics unless repo analysis proves an existing capability cannot express the action and spec/plan are updated first.
## Assumptions
- The existing `GovernanceInboxSectionBuilder` can provide enough item-level source context for a first implementation without becoming a generic task engine.
- Existing source routes for findings, finding exceptions, operations, alerts, reviews, and Customer Review Workspace remain the owning surfaces for mutations and deep diagnostics.
- Existing capability resolvers and policies are sufficient for v1 action visibility and source-link access.
- The internal `tenantId` property may remain as implementation detail only; runtime URL/copy/filter language must stay `environment_id` and `Environment`.
- Spec 325 target images remain visual calibration and must not be treated as runtime data truth.
## Risks
- Implementation could overreach into a new workflow engine or duplicate Decision Register state instead of deriving a page-local workbench.
- Cross-family priority mapping could imply a new decision taxonomy if it is not kept as derived display logic.
- Evidence or accepted-risk labels could become false green claims if unavailable proof is not rendered honestly.
- Capability-limited actors could learn hidden family existence through counts, empty states, or disabled action labels if scoping is applied too late.
- Browser coverage could become expensive if fixtures grow beyond the targeted smoke path.
## Open Questions
- None blocking preparation. Implementation must update `repo-truth-map.md` and spec/plan before runtime edits if it discovers a selected UI element requires new persisted truth, a new capability, or an unsupported source route.
## Non-Goals
- No backend workflow rebuild.
- No Decision Register rebuild.
- No new ticket/helpdesk/PSA system.
- No AI prioritization or recommendation engine.
- No new remediation automation.
- No broad redesign of findings pages, Customer Review Workspace, Operations Hub, Evidence Overview, Environment Dashboard, Baseline Compare, or Restore Safety Workflow.
- No migrations by default.
- No packages, env vars, queues, scheduler, storage, or deployment asset changes.
- No legacy tenant query alias support.
## Acceptance Criteria
### Productization
- [ ] Governance Inbox has a decision-first layout.
- [ ] Main decision question is visible.
- [ ] Highest-priority/selected item is visible.
- [ ] Reason and impact are visible.
- [ ] Owner/due state is visible or honestly unavailable.
- [ ] Evidence state is visible.
- [ ] Accepted-risk/exception state is visible.
- [ ] Primary next action is clear.
- [ ] Diagnostics are secondary/collapsed.
- [ ] Table/queue remains available as secondary workbench context.
### Customer / Operator Safety
- [ ] Raw diagnostics hidden by default.
- [ ] Provider secrets not visible.
- [ ] Internal exception/debug text not visible.
- [ ] No false green success state.
- [ ] Copy avoids tenant as platform context.
- [ ] Empty/unavailable states are honest.
### Scope
- [ ] Clean URL is workspace-wide.
- [ ] Shell is Workspace-only.
- [ ] Environment filter uses `environment_id`.
- [ ] Visible Environment chip appears when filtered.
- [ ] Clear filter works.
- [ ] Reload after clear is safe.
- [ ] Legacy aliases do not create filter state.
- [ ] Cross-workspace Environment is rejected.
### RBAC
- [ ] Unauthorized user cannot access protected data.
- [ ] Unauthorized actions are hidden/disabled.
- [ ] Assign owner action respects capability if shown.
- [ ] Exception/accepted-risk action respects capability if shown.
- [ ] Evidence access respects capability.
- [ ] Diagnostics access respects capability.
### UI / Visual
- [ ] Layout uses premium direction from Spec 325 without treating target images as runtime truth.
- [ ] Filament light mode remains readable.
- [ ] No heavy one-off CSS.
- [ ] Right-side detail panel exists on desktop.
- [ ] Workbench table/queue is not the only default experience.
- [ ] Page remains responsive enough for Filament shell.
### Tests / Validation
- [ ] Repo truth map exists.
- [ ] Required Feature tests pass.
- [ ] Required Browser smoke passes.
- [ ] Relevant Spec 314-322 guards still pass.
- [ ] `pint --dirty` passes.
- [ ] `git diff --check` passes.
- [ ] Full suite status is honestly reported if run/not run.
## Follow-Up Spec Candidates
- Spec 328 - Operations Hub Decision-First Workbench Productization.
- Spec 329 - Evidence / Audit Log Disclosure Productization.
- Spec 330 - Environment Dashboard / Baseline Compare Productization.
- Spec 331 - Restore Safety Workflow Productization.
Do not start these inside Spec 327.

View File

@ -0,0 +1,168 @@
# Tasks: Spec 327 - Governance Inbox Decision-First Workbench Productization
**Input**: Design documents from `/specs/327-governance-inbox-decision-first-workbench-productization/`
**Prerequisites**: `spec.md`, `plan.md`, `repo-truth-map.md`
**Tests**: Required. This is a runtime UI/operator workbench Filament page productization with browser smoke.
## Test Governance Checklist
- [x] Lane assignment is named and is the narrowest sufficient proof for the changed behavior.
- [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 test profile (`global-context-shell` plus decision-first disclosure) is explicit.
- [x] Any material budget, baseline, trend, or escalation note is recorded in the active spec or PR.
## Phase 1: Preparation And Repo Truth
**Purpose**: Confirm runtime truth and prevent invented claims before page edits.
- [x] T001 Re-read `specs/327-governance-inbox-decision-first-workbench-productization/spec.md`, `plan.md`, `tasks.md`, and `repo-truth-map.md`.
- [x] T002 Re-read related completed context only: Specs 250, 257, and 314-326. Do not modify their artifacts.
- [x] T003 Verify current `GovernanceInbox` route/class/view/builder and existing tests before editing.
- [x] T004 Update `repo-truth-map.md` with any newly discovered source, capability, fallback, or classification before runtime changes.
- [x] T005 Confirm no migration/package/env/queue/storage/deployment asset change is required; if one appears necessary, stop and update spec/plan first.
- [x] T006 Confirm Filament v5 / Livewire v4.0+ compliance and no Livewire v3/Filament legacy API use.
- [x] T007 Confirm panel provider registration remains `apps/platform/bootstrap/providers.php`.
- [x] T008 Confirm related globally searchable resources stay disabled or have safe View/Edit pages; no global search change is expected.
## Phase 2: Feature Tests First
**Purpose**: Lock decision-first layout, scope, RBAC, evidence, accepted-risk, and diagnostics behavior before the UI refactor.
- [x] T009 Add or update a feature test asserting `repo-truth-map.md` exists and lists required data areas.
- [x] T010 Add or update a Feature/Livewire/HTTP test for the decision-first layout text: `Governance Inbox`, `What decision clears the highest-priority item?`, `Reason`, `Impact`, `Evidence`, and `Next action`.
- [x] T011 Add or update a Feature/Livewire/HTTP test asserting a highest-priority or selected item shows owner or owner unavailable, due date or due unavailable, evidence state, accepted-risk/exception state, and primary next action.
- [x] T012 Add or update a Feature/Livewire/HTTP test asserting the right decision detail panel contains `Decision summary`, `Impact`, `Evidence path`, `Accepted risk` or exception state, owner/due state, and primary next action.
- [x] T013 Add or update a test asserting existing Governance Inbox queue/sections remain available as secondary workbench context.
- [x] T014 Add or update a test that raw diagnostics are hidden by default: `raw payload`, `stack trace`, `debug metadata`, `provider secret`, `internal exception`, and raw OperationRun payload text must not appear.
- [x] T015 Add or update accepted-risk/exception state tests for repo-supported states such as no exception, pending exception, accepted risk active, expiring, expired, or follow-up required.
- [x] T016 Add or update evidence state tests proving linked/missing/unavailable evidence appears without raw evidence payload.
- [x] T017 Add or update RBAC tests covering primary action visibility/unavailability for assign owner, create/update exception or accepted risk, open evidence, open operation proof, and open diagnostics where supported.
- [x] T018 Add or update canonical environment filter tests for `?environment_id=`, visible chip, workspace shell only, clear filter, and provable filtered data.
- [x] T019 Add or update legacy alias rejection tests for `tenant`, `tenant_id`, `managed_environment_id`, `environment`, `tenant_scope`, and `tableFilters`.
- [x] T020 Add or update cross-workspace environment filter guard test returning safe 404/no-access.
- [x] T021 Add or update tenant-copy guard asserting platform-context copy such as `current tenant`, `tenant filter`, `entitled tenant`, and `all tenants` is not visible on Governance Inbox.
## Phase 3: Page Skeleton Productization
**Purpose**: Refactor existing page layout without new backend foundation.
- [x] T022 Update `apps/platform/app/Filament/Pages/Governance/GovernanceInbox.php` to expose a repo-truth-bounded payload for header/scope, selected/highest-priority item, summary cards, queue context, detail panel, actions, evidence path, exception state, and diagnostics disclosure.
- [x] T023 Update `apps/platform/resources/views/filament/pages/governance/governance-inbox.blade.php` to render the decision-first workbench before the secondary queue context.
- [x] T024 Ensure the header/scope area shows workspace-wide vs environment-filtered context, visible environment chip when filtered, and concise queue purpose copy.
- [x] T025 Ensure the main decision workbench shows the stable question, status badge, title, reason, impact, owner, due, evidence state, accepted-risk/exception state, and one primary next action.
- [x] T026 Ensure summary cards show only repo-backed posture such as visible decisions, overdue, owner missing, evidence missing, or accepted-risk follow-up; show unavailable or omit unsupported cards.
- [x] T027 Ensure the right-side decision/detail panel shows decision summary, impact, owner/due, evidence path, accepted-risk/exception state, linked review/operation proof where available, primary next action, and diagnostics disclosure.
- [x] T028 Ensure the right-side detail panel is visible on desktop and stacks below on smaller screens.
- [x] T029 Keep the existing queue/table/source-family context as secondary content; it must not be the only default experience.
- [x] T030 Ensure diagnostics/internal details are collapsed, hidden, or capability-gated by default.
## Phase 4: Data Binding And Honest States
**Purpose**: Bind to repo-verified sources and avoid false claims.
- [x] T031 Map selected/highest-priority item state from existing `GovernanceInboxSectionBuilder` entries and source models without creating persisted state.
- [x] T032 Bind owner and due display to `Finding` and `FindingException` fields where present; show `Owner missing`, `Owner unavailable`, or `Due date unavailable` when absent.
- [x] T033 Bind evidence display to existing evidence fields/relations only; show `Evidence missing`, `Unavailable`, or omit unsupported proof paths.
- [x] T034 Bind accepted-risk/exception display to existing `FindingException` and `Finding` truth; do not introduce new status families.
- [x] T035 Bind operation proof links only through existing `OperationRunLinks` or authorized source routes.
- [x] T036 Bind decision/review links only where existing source routes and authorization are repo-real.
- [x] T037 Ensure no generic green success state appears without exact repo-backed proof.
## Phase 5: Actions, RBAC, And Safety
**Purpose**: Show only real, authorized actions and preserve read-first default behavior.
- [x] T038 Keep primary action singular and context-aware for the selected/highest-priority item.
- [x] T039 Show open finding, review accepted risk, open evidence, open operation proof, open review context, or open decision record only when route and authorization are repo-real.
- [x] T040 Ensure unauthorized actions are hidden or unavailable without leaking sensitive details.
- [x] T041 Verify no default action approves, rejects, accepts risk, closes, deletes, restores, remediates, or mutates provider state.
- [x] T042 If any high-impact action is unexpectedly required, update spec/plan first, then implement it with `Action::make(...)->action(...)`, `->requiresConfirmation()`, server-side authorization, audit, notification, and tests.
## Phase 6: Workspace / Environment Scope Contract
**Purpose**: Preserve Specs 314-322.
- [x] T043 Verify clean `/admin/governance/inbox` does not read remembered environment shell state or persisted table filters.
- [x] T044 Verify `/admin/governance/inbox?environment_id={id}` filters only page data, shows visible chip, and keeps Workspace shell ownership.
- [x] T045 Verify clear filter redirects to clean workspace URL and remains safe after reload.
- [x] T046 Verify legacy aliases are removed/neutralized and do not set filter state.
- [x] T047 Verify cross-workspace or unauthorized `environment_id` returns safe no-access/404.
- [x] T048 Verify back/forward/reload behavior does not resurrect cleared environment filter state.
## Phase 7: Browser Smoke And Screenshots
**Purpose**: Prove the user-facing contract in the integrated browser lane.
- [x] T049 Create `apps/platform/tests/Browser/Spec327GovernanceInboxProductizationSmokeTest.php` using existing Pest Browser conventions.
- [x] T050 Browser Flow A: clean workspace entry; assert Workspace shell only, no Environment chip, main decision question, right detail panel, diagnostics collapsed, screenshot.
- [x] T051 Browser Flow B: filtered environment entry; assert Workspace shell only, visible chip, clear filter action, filtered scope copy, screenshot.
- [x] T052 Browser Flow C: clear filter and reload; assert clean URL, chip does not return, no active Environment shell.
- [x] T053 Browser Flow D: selected/highest-priority item detail; assert detail panel and primary action visible and raw diagnostics absent.
- [x] T054 Browser Flow E: table/queue remains visible lower/secondary and no platform-context tenant wording appears.
- [x] T055 Browser Flow F: light mode readability check if supported; capture optional screenshot.
- [x] T056 Save screenshots under `specs/327-governance-inbox-decision-first-workbench-productization/artifacts/screenshots/` when generated and ensure they contain no secrets.
## Phase 8: UI Coverage And Documentation Artifacts
**Purpose**: Satisfy UI-COV without unrelated docs churn.
- [x] T057 Decide after runtime diff whether `docs/ui-ux-enterprise-audit/route-inventory.md` or `design-coverage-matrix.md` needs an update.
- [x] T058 If coverage docs are not changed, add a close-out note explaining why existing UI-004 report plus Spec 325 target artifacts remain sufficient for the unchanged route/archetype.
- [x] T059 Update `repo-truth-map.md` final classifications for implemented/empty/deferred elements.
- [x] T060 Do not create general documentation files outside required Spec Kit/UI coverage artifacts.
## Phase 9: Validation
**Purpose**: Run narrow proof and report honestly.
- [x] T061 Run `cd apps/platform && ./vendor/bin/sail artisan test tests/Feature/Governance tests/Feature/Navigation/WorkspaceHubEnvironmentFilterContractTest.php tests/Feature/Navigation/WorkspaceHubClearFilterContractTest.php --compact`.
- [x] T062 Run `cd apps/platform && ./vendor/bin/sail artisan test tests/Browser/Spec327GovernanceInboxProductizationSmokeTest.php --compact`.
- [x] T063 Run `cd apps/platform && ./vendor/bin/sail artisan test --filter='GovernanceInbox|WorkspaceHub|EnvironmentFilter|ClearFilter|LegacyTenant|Spec322' --compact`.
- [x] T064 Run `cd apps/platform && ./vendor/bin/sail pint --dirty`.
- [x] T065 Run `git diff --check`.
- [x] T066 Report full-suite status honestly if not run.
- [x] T067 Confirm no migrations, seeders, packages, env vars, queues, scheduler, storage, deployment assets, backwards compatibility layer, or legacy tenant alias support were added.
## Non-Goals Checklist
- [x] NT001 Do not rebuild Governance Inbox backend.
- [x] NT002 Do not rebuild Decision Register.
- [x] NT003 Do not build a ticketing/helpdesk/PSA system.
- [x] NT004 Do not add AI prioritization or remediation automation.
- [x] NT005 Do not redesign Customer Review Workspace, Operations Hub, Evidence Overview, Environment Dashboard, Baseline Compare, or Restore Safety Workflow.
- [x] NT006 Do not add migrations unless spec/plan are updated first with proof.
- [x] NT007 Do not rewrite completed Specs 250, 257, or 314-326.
- [x] NT008 Do not add legacy tenant query alias support.
## Implementation Close-Out Notes
- Spec 327 implementation stayed bounded to the existing Governance Inbox page, section builder, Blade view, feature tests, browser smoke, and spec artifacts.
- Follow-up refinement kept the same Spec 327 package and tightened the page hierarchy: the duplicate in-view heading was removed, Filament owns the single page title/subtitle, filter chips are secondary, zero metric cards no longer dominate the empty primary experience, and the decision/evidence workbench is the first dominant surface.
- Browser screenshot artifacts were generated by Pest Browser and copied host-side into `specs/327-governance-inbox-decision-first-workbench-productization/artifacts/screenshots/`. The Sail container sees the repository spec path as read-only, so the browser helper keeps artifact copying best-effort while retaining Pest screenshots under `apps/platform/tests/Browser/Screenshots/`.
- The UI coverage registry and route inventory were not changed because `/admin/governance/inbox` remains the existing UI-028 strategic surface and route/archetype classification did not change; this spec package carries the productization proof, repo truth map, tests, and screenshots.
- No new mutating governance action was added. Primary actions remain repo-real navigation/source handoff links; destructive or provider-changing actions remain out of scope.
- The broad Sail filter was rerun after the final asset fix. Spec327 passed inside that run. The remaining failures were outside Spec327 in `Spec316WorkspaceHubClearFilterSmokeTest`, where the Operations page screenshot already showed clean all-environment state at the click timeout. The same Spec316 browser file passed when rerun by itself, so this is recorded as browser-suite timing/interference residual rather than an in-scope Spec327 regression.
- Requested refinement validation passed on Sail: `tests/Feature/Governance`, `WorkspaceHubEnvironmentFilterContractTest`, `WorkspaceHubClearFilterContractTest`, `Spec327GovernanceInboxProductizationSmokeTest.php`, `pint --dirty`, and `git diff --check`.
- Final non-empty workbench proof uses a repo-backed finding fixture, asserts visible decision title/reason/impact/owner/due/evidence/accepted-risk/primary action fields, verifies the desktop right-side `<aside>` geometry, and writes `artifacts/screenshots/governance-inbox-decision-workbench.png`.
## Required Final Report Content
When implementation later completes, report:
- Changed behavior.
- Decision-first workbench details.
- Evidence / Accepted Risk / Owner / Due coverage.
- Files changed.
- Repo truth map status.
- Tests run and results.
- Browser verification and screenshots path.
- Known gaps.
- Remaining follow-ups.
- Diagnostics default state.
- RBAC-visible/hidden actions.
- Repo-verified vs unavailable states.
- Full suite run/not run.
- Explicit no migrations/seeders/packages/env/queues/scheduler/storage/deployment assets/backcompat/legacy aliases statement.