diff --git a/apps/platform/app/Filament/Pages/Governance/GovernanceInbox.php b/apps/platform/app/Filament/Pages/Governance/GovernanceInbox.php index b6132ee0..d5d992d4 100644 --- a/apps/platform/app/Filament/Pages/Governance/GovernanceInbox.php +++ b/apps/platform/app/Filament/Pages/Governance/GovernanceInbox.php @@ -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|null */ @@ -73,6 +78,11 @@ class GovernanceInbox extends Page */ private ?array $unfilteredInboxPayload = null; + /** + * @var array|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|null, + * summary_cards: list, + * 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 */ @@ -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> + */ + 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 $item + * @return array + */ + 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> $entries + * @return list + */ + 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(); diff --git a/apps/platform/app/Support/Filament/PanelThemeAsset.php b/apps/platform/app/Support/Filament/PanelThemeAsset.php index 1e7f265f..25921446 100644 --- a/apps/platform/app/Support/Filament/PanelThemeAsset.php +++ b/apps/platform/app/Support/Filament/PanelThemeAsset.php @@ -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); } } diff --git a/apps/platform/app/Support/GovernanceInbox/GovernanceInboxSectionBuilder.php b/apps/platform/app/Support/GovernanceInbox/GovernanceInboxSectionBuilder.php index 05994c78..40982c41 100644 --- a/apps/platform/app/Support/GovernanceInbox/GovernanceInboxSectionBuilder.php +++ b/apps/platform/app/Support/GovernanceInbox/GovernanceInboxSectionBuilder.php @@ -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; diff --git a/apps/platform/resources/views/filament/pages/governance/governance-inbox.blade.php b/apps/platform/resources/views/filament/pages/governance/governance-inbox.blade.php index 709a6b72..cc858dc1 100644 --- a/apps/platform/resources/views/filament/pages/governance/governance-inbox.blade.php +++ b/apps/platform/resources/views/filament/pages/governance/governance-inbox.blade.php @@ -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 - -
-
- - Governance inbox -
- -
-

- Governance inbox -

- -

- This workspace decision surface routes you into the existing findings, finding exceptions, operations, alerts, and review surfaces without introducing a second workflow state. -

-
- +
+
@if (filled($scope['workspace_label'] ?? null)) - + Workspace: {{ $scope['workspace_label'] }} @endif - + Scope: {{ $scope['family_label'] ?? 'All attention' }} - + Visible items: {{ $scope['total_count'] ?? 0 }} @if (filled($scope['tenant_label'] ?? null)) - + Environment: {{ $scope['tenant_label'] }} @endif @@ -51,112 +39,291 @@ ]) @endif - - - @if ($sections === []) - -
-
-

{{ $emptyState['title'] }}

-

{{ $emptyState['body'] }}

+
+
+
+
+

+ Decision workbench +

+

+ {{ $workbench['question'] }} +

+
+ + @if ($selectedItem !== null) + + {{ $selectedItem['status_label'] }} + + @endif
- @if (filled($emptyState['action_label'] ?? null) && filled($emptyState['action_url'] ?? null)) -
- - {{ $emptyState['action_label'] }} - + @if ($selectedItem === null) +
+

+ {{ $emptyState['title'] }} +

+

+ {{ $emptyState['body'] }} +

+ + @if (filled($emptyState['action_label'] ?? null) && filled($emptyState['action_url'] ?? null)) +
+ + {{ $emptyState['action_label'] }} + +
+ @endif
- @endif -
- - @else - @foreach ($sections as $section) - -
-
+ @else +
-

{{ $section['label'] }}

- - {{ $section['count'] }} + + {{ $selectedItem['section_label'] }} + + + Environment: {{ $selectedItem['environment_label'] }}
-

{{ $section['summary'] }}

+

+ {{ $selectedItem['title'] }} +

+ +

+ {{ $selectedItem['decision_label'] }} +

-
+
+
+
Reason
+
{{ $selectedItem['reason_label'] }}
+
+
+
Impact
+
{{ $selectedItem['impact_label'] }}
+
+
+
Owner
+
{{ $selectedItem['owner_label'] }}
+
+
+
Due
+
{{ $selectedItem['due_label'] }}
+
+
+ +
+
+

Evidence

+

{{ $selectedItem['evidence_label'] }}

+
+
+

Accepted risk

+

{{ $selectedItem['exception_label'] }}

+
+
+ + @if (filled($selectedItem['primary_action_url'] ?? null)) +
+
+

Primary next action

+

{{ $selectedItem['primary_action_label'] }}

+
+ + + {{ $selectedItem['primary_action_label'] }} + +
+ @endif +
+ @endif +
+ + +
+ + @if ($sections !== []) +
+
+

Queue context

+

+ Secondary source-family context remains available after the priority decision. +

+
+ + @foreach ($sections as $section) +
+
+
+
+

{{ $section['label'] }}

+ + {{ $section['count'] }} + +
+ +

{{ $section['summary'] }}

+
+ {{ $section['dominant_action_label'] }}
-
- @if ($section['count'] === 0) -
- {{ $section['empty_state'] }} -
- @else -
    - @foreach ($section['entries'] as $entry) -
  • -
    -
    - @if (filled($entry['tenant_label'] ?? null)) -
    - {{ $entry['tenant_label'] }} + @if ($section['count'] === 0) +
    + {{ $section['empty_state'] }} +
    + @else +
      + @foreach ($section['entries'] as $entry) +
    • +
      +
      + @if (filled($entry['tenant_label'] ?? null)) +
      + {{ $entry['tenant_label'] }} +
      + @endif + +
      + + {{ $entry['headline'] }} + + + + {{ $entry['status_label'] }} +
      - @endif -
      - - {{ $entry['headline'] }} - - - - {{ $entry['status_label'] }} - + @if (filled($entry['subline'] ?? null)) +

      {{ $entry['subline'] }}

      + @endif
      - @if (filled($entry['subline'] ?? null)) -

      {{ $entry['subline'] }}

      - @endif -
      - -
      Open source
      -
      -
    • - @endforeach -
    - @endif -
    - - @endforeach - @endif +
  • + @endforeach +
+ @endif +
+ @endforeach +
+ @endif +
diff --git a/apps/platform/tests/Browser/Spec327GovernanceInboxProductizationSmokeTest.php b/apps/platform/tests/Browser/Spec327GovernanceInboxProductizationSmokeTest.php new file mode 100644 index 00000000..86399cf0 --- /dev/null +++ b/apps/platform/tests/Browser/Spec327GovernanceInboxProductizationSmokeTest.php @@ -0,0 +1,264 @@ +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); + } +} diff --git a/apps/platform/tests/Feature/Governance/GovernanceInboxPageTest.php b/apps/platform/tests/Feature/Governance/GovernanceInboxPageTest.php index b030e119..e9ab6d9d 100644 --- a/apps/platform/tests/Feature/Governance/GovernanceInboxPageTest.php +++ b/apps/platform/tests/Feature/Governance/GovernanceInboxPageTest.php @@ -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', diff --git a/specs/327-governance-inbox-decision-first-workbench-productization/artifacts/screenshots/governance-inbox-decision-workbench.png b/specs/327-governance-inbox-decision-first-workbench-productization/artifacts/screenshots/governance-inbox-decision-workbench.png new file mode 100644 index 00000000..9a25b685 Binary files /dev/null and b/specs/327-governance-inbox-decision-first-workbench-productization/artifacts/screenshots/governance-inbox-decision-workbench.png differ diff --git a/specs/327-governance-inbox-decision-first-workbench-productization/artifacts/screenshots/spec327-governance-inbox--after-clear.png b/specs/327-governance-inbox-decision-first-workbench-productization/artifacts/screenshots/spec327-governance-inbox--after-clear.png new file mode 100644 index 00000000..a0200a9c Binary files /dev/null and b/specs/327-governance-inbox-decision-first-workbench-productization/artifacts/screenshots/spec327-governance-inbox--after-clear.png differ diff --git a/specs/327-governance-inbox-decision-first-workbench-productization/artifacts/screenshots/spec327-governance-inbox--after-reload.png b/specs/327-governance-inbox-decision-first-workbench-productization/artifacts/screenshots/spec327-governance-inbox--after-reload.png new file mode 100644 index 00000000..a0200a9c Binary files /dev/null and b/specs/327-governance-inbox-decision-first-workbench-productization/artifacts/screenshots/spec327-governance-inbox--after-reload.png differ diff --git a/specs/327-governance-inbox-decision-first-workbench-productization/artifacts/screenshots/spec327-governance-inbox--clean.png b/specs/327-governance-inbox-decision-first-workbench-productization/artifacts/screenshots/spec327-governance-inbox--clean.png new file mode 100644 index 00000000..9a25b685 Binary files /dev/null and b/specs/327-governance-inbox-decision-first-workbench-productization/artifacts/screenshots/spec327-governance-inbox--clean.png differ diff --git a/specs/327-governance-inbox-decision-first-workbench-productization/artifacts/screenshots/spec327-governance-inbox--diagnostics.png b/specs/327-governance-inbox-decision-first-workbench-productization/artifacts/screenshots/spec327-governance-inbox--diagnostics.png new file mode 100644 index 00000000..d2f0e979 Binary files /dev/null and b/specs/327-governance-inbox-decision-first-workbench-productization/artifacts/screenshots/spec327-governance-inbox--diagnostics.png differ diff --git a/specs/327-governance-inbox-decision-first-workbench-productization/artifacts/screenshots/spec327-governance-inbox--filtered.png b/specs/327-governance-inbox-decision-first-workbench-productization/artifacts/screenshots/spec327-governance-inbox--filtered.png new file mode 100644 index 00000000..03ac3fbf Binary files /dev/null and b/specs/327-governance-inbox-decision-first-workbench-productization/artifacts/screenshots/spec327-governance-inbox--filtered.png differ diff --git a/specs/327-governance-inbox-decision-first-workbench-productization/checklists/requirements.md b/specs/327-governance-inbox-decision-first-workbench-productization/checklists/requirements.md new file mode 100644 index 00000000..978b2c65 --- /dev/null +++ b/specs/327-governance-inbox-decision-first-workbench-productization/checklists/requirements.md @@ -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. diff --git a/specs/327-governance-inbox-decision-first-workbench-productization/plan.md b/specs/327-governance-inbox-decision-first-workbench-productization/plan.md new file mode 100644 index 00000000..373c42d6 --- /dev/null +++ b/specs/327-governance-inbox-decision-first-workbench-productization/plan.md @@ -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. diff --git a/specs/327-governance-inbox-decision-first-workbench-productization/repo-truth-map.md b/specs/327-governance-inbox-decision-first-workbench-productization/repo-truth-map.md new file mode 100644 index 00000000..31849c50 --- /dev/null +++ b/specs/327-governance-inbox-decision-first-workbench-productization/repo-truth-map.md @@ -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`. diff --git a/specs/327-governance-inbox-decision-first-workbench-productization/spec.md b/specs/327-governance-inbox-decision-first-workbench-productization/spec.md new file mode 100644 index 00000000..a1b53db7 --- /dev/null +++ b/specs/327-governance-inbox-decision-first-workbench-productization/spec.md @@ -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. diff --git a/specs/327-governance-inbox-decision-first-workbench-productization/tasks.md b/specs/327-governance-inbox-decision-first-workbench-productization/tasks.md new file mode 100644 index 00000000..e94bfe07 --- /dev/null +++ b/specs/327-governance-inbox-decision-first-workbench-productization/tasks.md @@ -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 `