feat: cut over workspace-owned analysis shell context (#375)
## Summary - cut over workspace-owned analysis and library surfaces to workspace shell ownership instead of inheriting remembered environment shell context - update the affected findings pages, scope resolution, navigation helpers, and related Blade views to keep environment focus explicit instead of implicit - add and update Spec 320 artifacts plus focused regression coverage for findings navigation context, workspace hub registration, and admin surface scope behavior ## Guardrails - Filament remains on v5 with Livewire v4 compliance unchanged - provider registration remains in apps/platform/bootstrap/providers.php - no new globally searchable resources were introduced or changed - no new destructive actions were introduced or changed - no Filament assets were added or changed, so the deploy requirement for filament:assets is unchanged ## Testing - `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Findings/FindingsAssignmentHygieneReportTest.php tests/Feature/Findings/FindingsIntakeQueueNavigationContextTest.php tests/Feature/Findings/FindingsIntakeQueueTest.php tests/Feature/Findings/MyFindingsInboxNavigationContextTest.php tests/Feature/Findings/MyWorkInboxTest.php tests/Feature/Navigation/WorkspaceHubRegistryTest.php tests/Unit/Support/OperateHub/OperateHubShellResolutionTest.php tests/Unit/Tenants/AdminSurfaceScopeTest.php` - `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent` Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #375
@ -15,6 +15,7 @@
|
||||
use App\Support\Filament\CanonicalAdminEnvironmentFilterState;
|
||||
use App\Support\Filament\TablePaginationProfiles;
|
||||
use App\Support\Navigation\CanonicalNavigationContext;
|
||||
use App\Support\Navigation\WorkspaceHubEnvironmentFilter;
|
||||
use App\Support\OperateHub\OperateHubShell;
|
||||
use App\Support\Ui\ActionSurface\ActionSurfaceDeclaration;
|
||||
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceInspectAffordance;
|
||||
@ -182,7 +183,7 @@ public function availableFilters(): array
|
||||
'options' => [],
|
||||
],
|
||||
[
|
||||
'key' => 'tenant',
|
||||
'key' => 'environment_id',
|
||||
'label' => 'ManagedEnvironment',
|
||||
'fixed' => false,
|
||||
'options' => collect($this->visibleTenants())
|
||||
@ -402,14 +403,22 @@ private function tenantFilterOptions(): array
|
||||
|
||||
private function applyRequestedTenantPrefilter(): void
|
||||
{
|
||||
$requestedTenant = request()->query('tenant');
|
||||
$workspace = $this->workspace();
|
||||
|
||||
if (! is_string($requestedTenant) && ! is_numeric($requestedTenant)) {
|
||||
if (! $workspace instanceof Workspace) {
|
||||
return;
|
||||
}
|
||||
|
||||
$filter = WorkspaceHubEnvironmentFilter::fromRequest(request(), $workspace);
|
||||
|
||||
if (! $filter instanceof WorkspaceHubEnvironmentFilter) {
|
||||
return;
|
||||
}
|
||||
|
||||
$environmentId = $filter->environmentId();
|
||||
|
||||
foreach ($this->visibleTenants() as $tenant) {
|
||||
if ((string) $tenant->getKey() !== (string) $requestedTenant && (string) $tenant->external_id !== (string) $requestedTenant) {
|
||||
if ((int) $tenant->getKey() !== $environmentId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -418,6 +427,8 @@ private function applyRequestedTenantPrefilter(): void
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
throw new NotFoundHttpException;
|
||||
}
|
||||
|
||||
private function normalizeTenantFilterState(): void
|
||||
@ -583,9 +594,9 @@ private function navigationContext(): CanonicalNavigationContext
|
||||
|
||||
private function reportUrl(array $overrides = []): string
|
||||
{
|
||||
$resolvedTenant = array_key_exists('tenant', $overrides)
|
||||
? $overrides['tenant']
|
||||
: $this->filteredTenant()?->external_id;
|
||||
$resolvedEnvironment = array_key_exists('environment_id', $overrides)
|
||||
? $overrides['environment_id']
|
||||
: $this->filteredTenant()?->getKey();
|
||||
$resolvedReason = array_key_exists('reason', $overrides)
|
||||
? $overrides['reason']
|
||||
: $this->currentReasonFilter();
|
||||
@ -593,7 +604,7 @@ private function reportUrl(array $overrides = []): string
|
||||
return static::getUrl(
|
||||
panel: 'admin',
|
||||
parameters: array_filter([
|
||||
'tenant' => is_string($resolvedTenant) && $resolvedTenant !== '' ? $resolvedTenant : null,
|
||||
'environment_id' => is_numeric($resolvedEnvironment) ? (int) $resolvedEnvironment : null,
|
||||
'reason' => is_string($resolvedReason) && $resolvedReason !== FindingAssignmentHygieneService::FILTER_ALL
|
||||
? $resolvedReason
|
||||
: null,
|
||||
|
||||
@ -19,6 +19,7 @@
|
||||
use App\Support\Filament\CanonicalAdminEnvironmentFilterState;
|
||||
use App\Support\Filament\TablePaginationProfiles;
|
||||
use App\Support\Navigation\CanonicalNavigationContext;
|
||||
use App\Support\Navigation\WorkspaceHubEnvironmentFilter;
|
||||
use App\Support\OperateHub\OperateHubShell;
|
||||
use App\Support\Rbac\UiEnforcement;
|
||||
use App\Support\Rbac\UiTooltips;
|
||||
@ -79,7 +80,7 @@ public static function actionSurfaceDeclaration(): ActionSurfaceDeclaration
|
||||
{
|
||||
return ActionSurfaceDeclaration::forPage(ActionSurfaceProfile::ListOnlyReadOnly, ActionSurfaceType::ReadOnlyRegistryReport)
|
||||
->withListRowPrimaryActionLimit(1)
|
||||
->satisfy(ActionSurfaceSlot::ListHeader, 'Header actions keep the shared-unassigned scope fixed and expose only a tenant-prefilter clear action when needed.')
|
||||
->satisfy(ActionSurfaceSlot::ListHeader, 'Header actions keep the shared-unassigned scope fixed and expose only an environment-prefilter clear action when needed.')
|
||||
->satisfy(ActionSurfaceSlot::InspectAffordance, ActionSurfaceInspectAffordance::ClickableRow->value)
|
||||
->exempt(ActionSurfaceSlot::ListRowMoreMenu, 'The intake queue keeps Claim finding inline and does not render a secondary More menu on rows.')
|
||||
->exempt(ActionSurfaceSlot::ListBulkMoreGroup, 'The intake queue does not expose bulk actions in v1.')
|
||||
@ -521,14 +522,22 @@ private function tenantFilterOptions(): array
|
||||
|
||||
private function applyRequestedTenantPrefilter(): void
|
||||
{
|
||||
$requestedTenant = request()->query('tenant');
|
||||
$workspace = $this->workspace();
|
||||
|
||||
if (! is_string($requestedTenant) && ! is_numeric($requestedTenant)) {
|
||||
if (! $workspace instanceof Workspace) {
|
||||
return;
|
||||
}
|
||||
|
||||
$filter = WorkspaceHubEnvironmentFilter::fromRequest(request(), $workspace);
|
||||
|
||||
if (! $filter instanceof WorkspaceHubEnvironmentFilter) {
|
||||
return;
|
||||
}
|
||||
|
||||
$environmentId = $filter->environmentId();
|
||||
|
||||
foreach ($this->visibleTenants() as $tenant) {
|
||||
if ((string) $tenant->getKey() !== (string) $requestedTenant && (string) $tenant->external_id !== (string) $requestedTenant) {
|
||||
if ((int) $tenant->getKey() !== $environmentId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -537,6 +546,8 @@ private function applyRequestedTenantPrefilter(): void
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
throw new NotFoundHttpException;
|
||||
}
|
||||
|
||||
private function normalizeTenantFilterState(): void
|
||||
@ -720,9 +731,9 @@ private function incomingGovernanceContext(): ?CanonicalNavigationContext
|
||||
|
||||
private function queueUrl(array $overrides = []): string
|
||||
{
|
||||
$resolvedTenant = array_key_exists('tenant', $overrides)
|
||||
? $overrides['tenant']
|
||||
: $this->filteredTenant()?->external_id;
|
||||
$resolvedEnvironment = array_key_exists('environment_id', $overrides)
|
||||
? $overrides['environment_id']
|
||||
: $this->filteredTenant()?->getKey();
|
||||
$resolvedView = array_key_exists('view', $overrides)
|
||||
? $overrides['view']
|
||||
: $this->currentQueueView();
|
||||
@ -730,7 +741,7 @@ private function queueUrl(array $overrides = []): string
|
||||
return static::getUrl(
|
||||
panel: 'admin',
|
||||
parameters: array_filter([
|
||||
'tenant' => is_string($resolvedTenant) && $resolvedTenant !== '' ? $resolvedTenant : null,
|
||||
'environment_id' => is_numeric($resolvedEnvironment) ? (int) $resolvedEnvironment : null,
|
||||
'view' => $resolvedView === 'needs_triage' ? 'needs_triage' : null,
|
||||
], static fn (mixed $value): bool => $value !== null && $value !== ''),
|
||||
);
|
||||
|
||||
@ -18,6 +18,7 @@
|
||||
use App\Support\Filament\CanonicalAdminEnvironmentFilterState;
|
||||
use App\Support\Filament\TablePaginationProfiles;
|
||||
use App\Support\Navigation\CanonicalNavigationContext;
|
||||
use App\Support\Navigation\WorkspaceHubEnvironmentFilter;
|
||||
use App\Support\OperateHub\OperateHubShell;
|
||||
use App\Support\Ui\ActionSurface\ActionSurfaceDeclaration;
|
||||
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceInspectAffordance;
|
||||
@ -206,7 +207,7 @@ public function availableFilters(): array
|
||||
'options' => [],
|
||||
],
|
||||
[
|
||||
'key' => 'tenant',
|
||||
'key' => 'environment_id',
|
||||
'label' => 'Managed environment',
|
||||
'fixed' => false,
|
||||
'options' => collect($this->visibleTenants())
|
||||
@ -461,14 +462,22 @@ private function tenantFilterOptions(): array
|
||||
|
||||
private function applyRequestedTenantPrefilter(): void
|
||||
{
|
||||
$requestedTenant = request()->query('tenant');
|
||||
$workspace = $this->workspace();
|
||||
|
||||
if (! is_string($requestedTenant) && ! is_numeric($requestedTenant)) {
|
||||
if (! $workspace instanceof Workspace) {
|
||||
return;
|
||||
}
|
||||
|
||||
$filter = WorkspaceHubEnvironmentFilter::fromRequest(request(), $workspace);
|
||||
|
||||
if (! $filter instanceof WorkspaceHubEnvironmentFilter) {
|
||||
return;
|
||||
}
|
||||
|
||||
$environmentId = $filter->environmentId();
|
||||
|
||||
foreach ($this->visibleTenants() as $tenant) {
|
||||
if ((string) $tenant->getKey() !== (string) $requestedTenant && (string) $tenant->external_id !== (string) $requestedTenant) {
|
||||
if ((int) $tenant->getKey() !== $environmentId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -477,6 +486,8 @@ private function applyRequestedTenantPrefilter(): void
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
throw new NotFoundHttpException;
|
||||
}
|
||||
|
||||
private function normalizeTenantFilterState(): void
|
||||
@ -667,8 +678,8 @@ private function queueUrl(): string
|
||||
return static::getUrl(
|
||||
panel: 'admin',
|
||||
parameters: array_filter([
|
||||
'tenant' => $tenant?->external_id,
|
||||
], static fn (mixed $value): bool => $value !== null && $value !== ''),
|
||||
'environment_id' => $tenant?->getKey(),
|
||||
], static fn (mixed $value): bool => is_numeric($value)),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -307,8 +307,8 @@ private function assignedFindingsSection(
|
||||
MyFindingsInbox::getUrl(
|
||||
panel: 'admin',
|
||||
parameters: array_filter([
|
||||
'tenant' => $selectedTenant?->external_id,
|
||||
], static fn (mixed $value): bool => is_string($value) && $value !== ''),
|
||||
'environment_id' => $selectedTenant?->getKey(),
|
||||
], static fn (mixed $value): bool => is_numeric($value)),
|
||||
),
|
||||
$navigationContext?->toQuery() ?? [],
|
||||
),
|
||||
@ -349,7 +349,7 @@ private function intakeFindingsSection(
|
||||
FindingsIntakeQueue::getUrl(
|
||||
panel: 'admin',
|
||||
parameters: array_filter([
|
||||
'tenant' => $selectedTenant?->external_id,
|
||||
'environment_id' => $selectedTenant?->getKey(),
|
||||
'view' => $needsTriageCount > 0 ? 'needs_triage' : null,
|
||||
], static fn (mixed $value): bool => $value !== null && $value !== ''),
|
||||
),
|
||||
|
||||
@ -10,6 +10,7 @@
|
||||
enum AdminSurfaceScope: string
|
||||
{
|
||||
case WorkspaceWideSurface = 'workspace_wide_surface';
|
||||
case WorkspaceOwnedAnalysisSurface = 'workspace_owned_analysis_surface';
|
||||
case WorkspaceScoped = 'workspace_scoped';
|
||||
case WorkspaceChooserException = 'workspace_chooser_exception';
|
||||
case EnvironmentBound = 'environment_bound';
|
||||
@ -42,6 +43,10 @@ public static function fromPath(string $path): self
|
||||
return self::WorkspaceWideSurface;
|
||||
}
|
||||
|
||||
if (self::isWorkspaceOwnedAnalysisSurfacePath($normalizedPath)) {
|
||||
return self::WorkspaceOwnedAnalysisSurface;
|
||||
}
|
||||
|
||||
if (
|
||||
str_starts_with($normalizedPath, '/admin/evidence/')
|
||||
&& ! str_starts_with($normalizedPath, '/admin/evidence/overview')
|
||||
@ -80,6 +85,7 @@ public function allowsEnvironmentlessState(): bool
|
||||
{
|
||||
return match ($this) {
|
||||
self::WorkspaceWideSurface,
|
||||
self::WorkspaceOwnedAnalysisSurface,
|
||||
self::WorkspaceScoped,
|
||||
self::WorkspaceChooserException,
|
||||
self::OnboardingWorkflow,
|
||||
@ -92,6 +98,7 @@ public function forcesEnvironmentlessShellContext(): bool
|
||||
{
|
||||
return match ($this) {
|
||||
self::WorkspaceWideSurface,
|
||||
self::WorkspaceOwnedAnalysisSurface,
|
||||
self::WorkspaceChooserException,
|
||||
self::CanonicalWorkspaceRecordViewer => true,
|
||||
default => false,
|
||||
@ -116,6 +123,13 @@ private static function isWorkspaceWideSurfacePath(string $normalizedPath): bool
|
||||
return WorkspaceHubRegistry::isWorkspaceHubPath($normalizedPath);
|
||||
}
|
||||
|
||||
private static function isWorkspaceOwnedAnalysisSurfacePath(string $normalizedPath): bool
|
||||
{
|
||||
return preg_match('#^/admin/(baseline-profiles|baseline-snapshots)(?:/.*)?$#', $normalizedPath) === 1
|
||||
|| preg_match('#^/admin/findings/(?:my-work|intake|hygiene)/?$#', $normalizedPath) === 1
|
||||
|| preg_match('#^/admin/cross-environment-compare/?$#', $normalizedPath) === 1;
|
||||
}
|
||||
|
||||
private static function effectivePath(Request $request): string
|
||||
{
|
||||
$path = '/'.ltrim((string) $request->path(), '/');
|
||||
|
||||
@ -21,6 +21,7 @@ public static function fromSurfaceScope(AdminSurfaceScope $pageCategory): self
|
||||
AdminSurfaceScope::EnvironmentScopedEvidence => self::AdministrativeManagement,
|
||||
AdminSurfaceScope::CanonicalWorkspaceRecordViewer => self::CanonicalWorkspaceRecord,
|
||||
AdminSurfaceScope::WorkspaceWideSurface,
|
||||
AdminSurfaceScope::WorkspaceOwnedAnalysisSurface,
|
||||
AdminSurfaceScope::WorkspaceScoped,
|
||||
AdminSurfaceScope::WorkspaceChooserException => self::StandardActiveOperating,
|
||||
};
|
||||
|
||||
@ -18,7 +18,7 @@
|
||||
</h1>
|
||||
|
||||
<p class="max-w-3xl text-sm leading-6 text-gray-600 dark:text-gray-300">
|
||||
Review visible broken assignments and stale in-progress work across entitled tenants in one read-first repair queue. Existing finding detail stays the only place where reassignment or lifecycle repair happens.
|
||||
Review visible broken assignments and stale in-progress work across entitled environments in one read-first repair queue. Existing finding detail stays the only place where reassignment or lifecycle repair happens.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -68,14 +68,11 @@
|
||||
{{ $scope['reason_filter_label'] }}
|
||||
</div>
|
||||
<div class="mt-1 text-sm text-gray-600 dark:text-gray-300">
|
||||
@if (($scope['tenant_prefilter_source'] ?? 'none') === 'active_tenant_context')
|
||||
ManagedEnvironment prefilter from active context:
|
||||
<span class="font-medium text-gray-950 dark:text-white">{{ $scope['tenant_label'] }}</span>
|
||||
@elseif (($scope['tenant_prefilter_source'] ?? 'none') === 'explicit_filter')
|
||||
ManagedEnvironment filter applied:
|
||||
@if (($scope['tenant_prefilter_source'] ?? 'none') === 'explicit_filter')
|
||||
Environment filter applied:
|
||||
<span class="font-medium text-gray-950 dark:text-white">{{ $scope['tenant_label'] }}</span>
|
||||
@else
|
||||
All visible tenants are currently included.
|
||||
All visible environments are currently included.
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -18,7 +18,7 @@
|
||||
</h1>
|
||||
|
||||
<p class="max-w-3xl text-sm leading-6 text-gray-600 dark:text-gray-300">
|
||||
Review visible unassigned open findings across entitled tenants in one queue. ManagedEnvironment context can narrow the view, but the intake scope stays fixed.
|
||||
Review visible unassigned open findings across entitled environments in one queue. An explicit environment filter can narrow the view, but the intake scope stays fixed.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -68,14 +68,11 @@
|
||||
{{ $scope['queue_view_label'] }}
|
||||
</div>
|
||||
<div class="mt-1 text-sm text-gray-600 dark:text-gray-300">
|
||||
@if (($scope['tenant_prefilter_source'] ?? 'none') === 'active_tenant_context')
|
||||
ManagedEnvironment prefilter from active context:
|
||||
<span class="font-medium text-gray-950 dark:text-white">{{ $scope['tenant_label'] }}</span>
|
||||
@elseif (($scope['tenant_prefilter_source'] ?? 'none') === 'explicit_filter')
|
||||
ManagedEnvironment filter applied:
|
||||
@if (($scope['tenant_prefilter_source'] ?? 'none') === 'explicit_filter')
|
||||
Environment filter applied:
|
||||
<span class="font-medium text-gray-950 dark:text-white">{{ $scope['tenant_label'] }}</span>
|
||||
@else
|
||||
All visible tenants are currently included.
|
||||
All visible environments are currently included.
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -18,7 +18,7 @@
|
||||
</h1>
|
||||
|
||||
<p class="max-w-3xl text-sm leading-6 text-gray-600 dark:text-gray-300">
|
||||
Review open assigned findings across visible tenants in one queue. ManagedEnvironment context can narrow the view, but the personal assignment scope stays fixed.
|
||||
Review open assigned findings across visible environments in one queue. An explicit environment filter can narrow the view, but the personal assignment scope stays fixed.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -56,14 +56,11 @@
|
||||
Assigned to me only
|
||||
</div>
|
||||
<div class="mt-1 text-sm text-gray-600 dark:text-gray-300">
|
||||
@if (($scope['tenant_prefilter_source'] ?? 'none') === 'active_tenant_context')
|
||||
ManagedEnvironment prefilter from active context:
|
||||
<span class="font-medium text-gray-950 dark:text-white">{{ $scope['tenant_label'] }}</span>
|
||||
@elseif (($scope['tenant_prefilter_source'] ?? 'none') === 'explicit_filter')
|
||||
ManagedEnvironment filter applied:
|
||||
@if (($scope['tenant_prefilter_source'] ?? 'none') === 'explicit_filter')
|
||||
Environment filter applied:
|
||||
<span class="font-medium text-gray-950 dark:text-white">{{ $scope['tenant_label'] }}</span>
|
||||
@else
|
||||
All visible tenants are currently included.
|
||||
All visible environments are currently included.
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -43,11 +43,13 @@ function findingsHygienePage(?User $user = null, array $query = [])
|
||||
|
||||
setAdminPanelContext();
|
||||
|
||||
$factory = $query === []
|
||||
? Livewire::actingAs(auth()->user())
|
||||
: Livewire::withQueryParams($query)->actingAs(auth()->user());
|
||||
$factory = Livewire::withHeaders(['referer' => FindingsHygieneReport::getUrl(panel: 'admin')]);
|
||||
|
||||
return $factory->test(FindingsHygieneReport::class);
|
||||
if ($query !== []) {
|
||||
$factory = $factory->withQueryParams($query);
|
||||
}
|
||||
|
||||
return $factory->actingAs(auth()->user())->test(FindingsHygieneReport::class);
|
||||
}
|
||||
|
||||
function makeFindingsHygieneFinding(ManagedEnvironment $tenant, array $attributes = []): Finding
|
||||
@ -256,7 +258,7 @@ function recordFindingsHygieneAudit(Finding $finding, string $action, CarbonImmu
|
||||
'options' => [],
|
||||
],
|
||||
[
|
||||
'key' => 'tenant',
|
||||
'key' => 'environment_id',
|
||||
'label' => 'ManagedEnvironment',
|
||||
'fixed' => false,
|
||||
'options' => [
|
||||
@ -351,7 +353,68 @@ function recordFindingsHygieneAudit(Finding $finding, string $action, CarbonImmu
|
||||
]);
|
||||
});
|
||||
|
||||
it('explains when the active tenant prefilter hides otherwise visible hygiene issues and clears it in place', function (): void {
|
||||
it('ignores remembered environments and retired tenant query aliases on the workspace-owned hygiene surface', function (): void {
|
||||
[$user, $tenantA] = findingsHygieneActingUser();
|
||||
|
||||
$tenantB = ManagedEnvironment::factory()->create([
|
||||
'status' => 'active',
|
||||
'workspace_id' => (int) $tenantA->workspace_id,
|
||||
'name' => 'Beta ManagedEnvironment',
|
||||
]);
|
||||
createUserWithTenant($tenantB, $user, role: 'readonly', workspaceRole: 'readonly');
|
||||
|
||||
$lostMemberA = User::factory()->create(['name' => 'Lost Member A']);
|
||||
createUserWithTenant($tenantA, $lostMemberA, role: 'readonly', workspaceRole: 'readonly');
|
||||
ManagedEnvironmentMembership::query()
|
||||
->where('managed_environment_id', (int) $tenantA->getKey())
|
||||
->where('user_id', (int) $lostMemberA->getKey())
|
||||
->delete();
|
||||
|
||||
$lostMemberB = User::factory()->create(['name' => 'Lost Member B']);
|
||||
createUserWithTenant($tenantB, $lostMemberB, role: 'readonly', workspaceRole: 'readonly');
|
||||
ManagedEnvironmentMembership::query()
|
||||
->where('managed_environment_id', (int) $tenantB->getKey())
|
||||
->where('user_id', (int) $lostMemberB->getKey())
|
||||
->delete();
|
||||
|
||||
$tenantAIssue = makeFindingsHygieneFinding($tenantA, [
|
||||
'owner_user_id' => (int) $user->getKey(),
|
||||
'assignee_user_id' => (int) $lostMemberA->getKey(),
|
||||
'subject_display_name' => 'ManagedEnvironment A Issue',
|
||||
]);
|
||||
$tenantBIssue = makeFindingsHygieneFinding($tenantB, [
|
||||
'owner_user_id' => (int) $user->getKey(),
|
||||
'assignee_user_id' => (int) $lostMemberB->getKey(),
|
||||
'subject_display_name' => 'ManagedEnvironment B Issue',
|
||||
]);
|
||||
|
||||
session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [
|
||||
(string) $tenantA->workspace_id => (int) $tenantB->getKey(),
|
||||
]);
|
||||
|
||||
$component = findingsHygienePage($user, [
|
||||
'tenant' => (string) $tenantB->external_id,
|
||||
'tenant_id' => (int) $tenantB->getKey(),
|
||||
'managed_environment_id' => (int) $tenantB->getKey(),
|
||||
'tenant_scope' => 'environment',
|
||||
'tableFilters' => [
|
||||
'managed_environment_id' => ['value' => (string) $tenantB->getKey()],
|
||||
],
|
||||
])
|
||||
->assertSet('tableFilters.managed_environment_id.value', null)
|
||||
->assertCanSeeTableRecords([$tenantAIssue, $tenantBIssue]);
|
||||
|
||||
expect($component->instance()->appliedScope())->toBe([
|
||||
'workspace_scoped' => true,
|
||||
'fixed_scope' => 'visible_findings_hygiene_only',
|
||||
'reason_filter' => 'all',
|
||||
'reason_filter_label' => 'All issues',
|
||||
'tenant_prefilter_source' => 'none',
|
||||
'tenant_label' => null,
|
||||
]);
|
||||
});
|
||||
|
||||
it('explains when the explicit environment_id prefilter hides otherwise visible hygiene issues and clears it in place', function (): void {
|
||||
[$user, $tenantA] = findingsHygieneActingUser();
|
||||
|
||||
$tenantB = ManagedEnvironment::factory()->create([
|
||||
@ -374,11 +437,7 @@ function recordFindingsHygieneAudit(Finding $finding, string $action, CarbonImmu
|
||||
'subject_display_name' => 'ManagedEnvironment A Issue',
|
||||
]);
|
||||
|
||||
session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [
|
||||
(string) $tenantA->workspace_id => (int) $tenantB->getKey(),
|
||||
]);
|
||||
|
||||
$component = findingsHygienePage($user)
|
||||
$component = findingsHygienePage($user, ['environment_id' => (int) $tenantB->getKey()])
|
||||
->assertSet('tableFilters.managed_environment_id.value', (string) $tenantB->getKey())
|
||||
->assertCanNotSeeTableRecords([$tenantAIssue])
|
||||
->assertSee('No hygiene issues match this environment scope')
|
||||
|
||||
@ -39,15 +39,16 @@
|
||||
tenantId: (int) $tenant->getKey(),
|
||||
familyKey: 'intake_findings',
|
||||
backLinkUrl: GovernanceInbox::getUrl(panel: 'admin', parameters: [
|
||||
'managed_environment_id' => (string) $tenant->getKey(),
|
||||
'environment_id' => (int) $tenant->getKey(),
|
||||
'family' => 'intake_findings',
|
||||
]),
|
||||
);
|
||||
|
||||
Livewire::withQueryParams(array_replace($context->toQuery(), [
|
||||
'tenant' => (string) $tenant->external_id,
|
||||
'view' => 'needs_triage',
|
||||
]))
|
||||
Livewire::withHeaders(['referer' => FindingsIntakeQueue::getUrl(panel: 'admin')])
|
||||
->withQueryParams(array_replace($context->toQuery(), [
|
||||
'environment_id' => (int) $tenant->getKey(),
|
||||
'view' => 'needs_triage',
|
||||
]))
|
||||
->actingAs($user)
|
||||
->test(FindingsIntakeQueue::class)
|
||||
->assertSet('tableFilters.managed_environment_id.value', (string) $tenant->getKey())
|
||||
|
||||
@ -31,11 +31,13 @@ function findingsIntakePage(?User $user = null, array $query = [])
|
||||
|
||||
setAdminPanelContext();
|
||||
|
||||
$factory = $query === []
|
||||
? Livewire::actingAs(auth()->user())
|
||||
: Livewire::withQueryParams($query)->actingAs(auth()->user());
|
||||
$factory = Livewire::withHeaders(['referer' => FindingsIntakeQueue::getUrl(panel: 'admin')]);
|
||||
|
||||
return $factory->test(FindingsIntakeQueue::class);
|
||||
if ($query !== []) {
|
||||
$factory = $factory->withQueryParams($query);
|
||||
}
|
||||
|
||||
return $factory->actingAs(auth()->user())->test(FindingsIntakeQueue::class);
|
||||
}
|
||||
|
||||
function makeIntakeFinding(ManagedEnvironment $tenant, array $attributes = []): Finding
|
||||
@ -140,7 +142,7 @@ function makeIntakeFinding(ManagedEnvironment $tenant, array $attributes = []):
|
||||
->and($queueViews['needs_triage']['active'])->toBeFalse();
|
||||
});
|
||||
|
||||
it('defaults to the active tenant prefilter and lets the operator clear it without dropping intake scope', function (): void {
|
||||
it('applies the explicit environment_id prefilter and lets the operator clear it without dropping intake scope', function (): void {
|
||||
[$user, $tenantA] = findingsIntakeActingUser();
|
||||
|
||||
$tenantB = ManagedEnvironment::factory()->create([
|
||||
@ -159,11 +161,7 @@ function makeIntakeFinding(ManagedEnvironment $tenant, array $attributes = []):
|
||||
'status' => Finding::STATUS_TRIAGED,
|
||||
]);
|
||||
|
||||
session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [
|
||||
(string) $tenantA->workspace_id => (int) $tenantB->getKey(),
|
||||
]);
|
||||
|
||||
$component = findingsIntakePage($user)
|
||||
$component = findingsIntakePage($user, ['environment_id' => (int) $tenantB->getKey()])
|
||||
->assertSet('tableFilters.managed_environment_id.value', (string) $tenantB->getKey())
|
||||
->assertCanSeeTableRecords([$findingB])
|
||||
->assertCanNotSeeTableRecords([$findingA])
|
||||
@ -174,7 +172,7 @@ function makeIntakeFinding(ManagedEnvironment $tenant, array $attributes = []):
|
||||
'fixed_scope' => 'visible_unassigned_open_findings_only',
|
||||
'queue_view' => 'unassigned',
|
||||
'queue_view_label' => 'Unassigned',
|
||||
'tenant_prefilter_source' => 'active_tenant_context',
|
||||
'tenant_prefilter_source' => 'explicit_filter',
|
||||
'tenant_label' => $tenantB->name,
|
||||
]);
|
||||
|
||||
@ -191,7 +189,7 @@ function makeIntakeFinding(ManagedEnvironment $tenant, array $attributes = []):
|
||||
]);
|
||||
});
|
||||
|
||||
it('keeps the needs triage view active when clearing the tenant prefilter', function (): void {
|
||||
it('keeps the needs triage view active when clearing the environment_id prefilter', function (): void {
|
||||
[$user, $tenantA] = findingsIntakeActingUser();
|
||||
|
||||
$tenantB = ManagedEnvironment::factory()->create([
|
||||
@ -215,11 +213,10 @@ function makeIntakeFinding(ManagedEnvironment $tenant, array $attributes = []):
|
||||
'status' => Finding::STATUS_TRIAGED,
|
||||
]);
|
||||
|
||||
session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [
|
||||
(string) $tenantA->workspace_id => (int) $tenantB->getKey(),
|
||||
]);
|
||||
|
||||
$component = findingsIntakePage($user, ['view' => 'needs_triage'])
|
||||
$component = findingsIntakePage($user, [
|
||||
'environment_id' => (int) $tenantB->getKey(),
|
||||
'view' => 'needs_triage',
|
||||
])
|
||||
->assertSet('tableFilters.managed_environment_id.value', (string) $tenantB->getKey())
|
||||
->assertCanSeeTableRecords([$tenantBTriage])
|
||||
->assertCanNotSeeTableRecords([$tenantATriage, $tenantBBacklog]);
|
||||
@ -229,7 +226,7 @@ function makeIntakeFinding(ManagedEnvironment $tenant, array $attributes = []):
|
||||
'fixed_scope' => 'visible_unassigned_open_findings_only',
|
||||
'queue_view' => 'needs_triage',
|
||||
'queue_view_label' => 'Needs triage',
|
||||
'tenant_prefilter_source' => 'active_tenant_context',
|
||||
'tenant_prefilter_source' => 'explicit_filter',
|
||||
'tenant_label' => $tenantB->name,
|
||||
]);
|
||||
|
||||
@ -252,6 +249,51 @@ function makeIntakeFinding(ManagedEnvironment $tenant, array $attributes = []):
|
||||
->and($queueViews['needs_triage']['active'])->toBeTrue();
|
||||
});
|
||||
|
||||
it('ignores remembered environments and retired tenant query aliases on the workspace-owned intake surface', function (): void {
|
||||
[$user, $tenantA] = findingsIntakeActingUser();
|
||||
|
||||
$tenantB = ManagedEnvironment::factory()->create([
|
||||
'status' => 'active',
|
||||
'workspace_id' => (int) $tenantA->workspace_id,
|
||||
'name' => 'Beta ManagedEnvironment',
|
||||
]);
|
||||
createUserWithTenant($tenantB, $user, role: 'readonly', workspaceRole: 'readonly');
|
||||
|
||||
$findingA = makeIntakeFinding($tenantA, [
|
||||
'subject_external_id' => 'tenant-a',
|
||||
'status' => Finding::STATUS_NEW,
|
||||
]);
|
||||
$findingB = makeIntakeFinding($tenantB, [
|
||||
'subject_external_id' => 'tenant-b',
|
||||
'status' => Finding::STATUS_TRIAGED,
|
||||
]);
|
||||
|
||||
session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [
|
||||
(string) $tenantA->workspace_id => (int) $tenantB->getKey(),
|
||||
]);
|
||||
|
||||
$component = findingsIntakePage($user, [
|
||||
'tenant' => (string) $tenantB->external_id,
|
||||
'tenant_id' => (int) $tenantB->getKey(),
|
||||
'managed_environment_id' => (int) $tenantB->getKey(),
|
||||
'tenant_scope' => 'environment',
|
||||
'tableFilters' => [
|
||||
'managed_environment_id' => ['value' => (string) $tenantB->getKey()],
|
||||
],
|
||||
])
|
||||
->assertSet('tableFilters.managed_environment_id.value', null)
|
||||
->assertCanSeeTableRecords([$findingA, $findingB]);
|
||||
|
||||
expect($component->instance()->appliedScope())->toBe([
|
||||
'workspace_scoped' => true,
|
||||
'fixed_scope' => 'visible_unassigned_open_findings_only',
|
||||
'queue_view' => 'unassigned',
|
||||
'queue_view_label' => 'Unassigned',
|
||||
'tenant_prefilter_source' => 'none',
|
||||
'tenant_label' => null,
|
||||
]);
|
||||
});
|
||||
|
||||
it('separates needs triage from the remaining backlog and keeps deterministic urgency ordering', function (): void {
|
||||
[$user, $tenant] = findingsIntakeActingUser();
|
||||
|
||||
@ -303,7 +345,7 @@ function makeIntakeFinding(ManagedEnvironment $tenant, array $attributes = []):
|
||||
]);
|
||||
|
||||
$component = findingsIntakePage($user, [
|
||||
'tenant' => (string) $tenant->external_id,
|
||||
'environment_id' => (int) $tenant->getKey(),
|
||||
'view' => 'needs_triage',
|
||||
]);
|
||||
|
||||
@ -333,9 +375,7 @@ function makeIntakeFinding(ManagedEnvironment $tenant, array $attributes = []):
|
||||
'subject_external_id' => 'available-elsewhere',
|
||||
]);
|
||||
|
||||
findingsIntakePage($user, [
|
||||
'tenant' => (string) $tenantA->external_id,
|
||||
])
|
||||
findingsIntakePage($user, ['environment_id' => (int) $tenantA->getKey()])
|
||||
->assertSee('No intake findings match this environment scope')
|
||||
->assertTableEmptyStateActionsExistInOrder(['clear_tenant_filter_empty']);
|
||||
|
||||
|
||||
@ -38,14 +38,15 @@
|
||||
tenantId: (int) $tenant->getKey(),
|
||||
familyKey: 'assigned_findings',
|
||||
backLinkUrl: GovernanceInbox::getUrl(panel: 'admin', parameters: [
|
||||
'managed_environment_id' => (string) $tenant->getKey(),
|
||||
'environment_id' => (int) $tenant->getKey(),
|
||||
'family' => 'assigned_findings',
|
||||
]),
|
||||
);
|
||||
|
||||
Livewire::withQueryParams(array_replace($context->toQuery(), [
|
||||
'tenant' => (string) $tenant->external_id,
|
||||
]))
|
||||
Livewire::withHeaders(['referer' => MyFindingsInbox::getUrl(panel: 'admin')])
|
||||
->withQueryParams(array_replace($context->toQuery(), [
|
||||
'environment_id' => (int) $tenant->getKey(),
|
||||
]))
|
||||
->actingAs($user)
|
||||
->test(MyFindingsInbox::class)
|
||||
->assertSet('tableFilters.managed_environment_id.value', (string) $tenant->getKey())
|
||||
|
||||
@ -31,9 +31,13 @@ function myWorkInboxPage(?User $user = null, array $query = [])
|
||||
|
||||
setAdminPanelContext();
|
||||
|
||||
$factory = $query === [] ? Livewire::actingAs(auth()->user()) : Livewire::withQueryParams($query)->actingAs(auth()->user());
|
||||
$factory = Livewire::withHeaders(['referer' => MyFindingsInbox::getUrl(panel: 'admin')]);
|
||||
|
||||
return $factory->test(MyFindingsInbox::class);
|
||||
if ($query !== []) {
|
||||
$factory = $factory->withQueryParams($query);
|
||||
}
|
||||
|
||||
return $factory->actingAs(auth()->user())->test(MyFindingsInbox::class);
|
||||
}
|
||||
|
||||
function makeAssignedFindingForInbox(ManagedEnvironment $tenant, User $assignee, array $attributes = []): Finding
|
||||
@ -121,7 +125,7 @@ function makeAssignedFindingForInbox(ManagedEnvironment $tenant, User $assignee,
|
||||
'options' => [],
|
||||
],
|
||||
[
|
||||
'key' => 'tenant',
|
||||
'key' => 'environment_id',
|
||||
'label' => 'Managed environment',
|
||||
'fixed' => false,
|
||||
'options' => [
|
||||
@ -150,7 +154,50 @@ function makeAssignedFindingForInbox(ManagedEnvironment $tenant, User $assignee,
|
||||
]);
|
||||
});
|
||||
|
||||
it('defaults to the active environment prefilter and lets the operator clear it without dropping personal scope', function (): void {
|
||||
it('applies the explicit environment_id prefilter and lets the operator clear it without dropping personal scope', function (): void {
|
||||
[$user, $tenantA] = myWorkInboxActingUser();
|
||||
|
||||
$tenantB = ManagedEnvironment::factory()->create([
|
||||
'status' => 'active',
|
||||
'workspace_id' => (int) $tenantA->workspace_id,
|
||||
'name' => 'Beta ManagedEnvironment',
|
||||
]);
|
||||
createUserWithTenant($tenantB, $user, role: 'readonly', workspaceRole: 'readonly');
|
||||
|
||||
$findingA = makeAssignedFindingForInbox($tenantA, $user, [
|
||||
'subject_external_id' => 'tenant-a',
|
||||
'status' => Finding::STATUS_NEW,
|
||||
]);
|
||||
$findingB = makeAssignedFindingForInbox($tenantB, $user, [
|
||||
'subject_external_id' => 'tenant-b',
|
||||
'status' => Finding::STATUS_TRIAGED,
|
||||
]);
|
||||
|
||||
$component = myWorkInboxPage($user, ['environment_id' => (int) $tenantB->getKey()])
|
||||
->assertSet('tableFilters.managed_environment_id.value', (string) $tenantB->getKey())
|
||||
->assertCanSeeTableRecords([$findingB])
|
||||
->assertCanNotSeeTableRecords([$findingA])
|
||||
->assertActionVisible('clear_tenant_filter');
|
||||
|
||||
expect($component->instance()->appliedScope())->toBe([
|
||||
'workspace_scoped' => true,
|
||||
'assignee_scope' => 'current_user_only',
|
||||
'tenant_prefilter_source' => 'explicit_filter',
|
||||
'tenant_label' => $tenantB->name,
|
||||
]);
|
||||
|
||||
$component->callAction('clear_tenant_filter')
|
||||
->assertCanSeeTableRecords([$findingA, $findingB]);
|
||||
|
||||
expect($component->instance()->appliedScope())->toBe([
|
||||
'workspace_scoped' => true,
|
||||
'assignee_scope' => 'current_user_only',
|
||||
'tenant_prefilter_source' => 'none',
|
||||
'tenant_label' => null,
|
||||
]);
|
||||
});
|
||||
|
||||
it('ignores remembered environments and retired tenant query aliases on the workspace-owned analysis surface', function (): void {
|
||||
[$user, $tenantA] = myWorkInboxActingUser();
|
||||
|
||||
$tenantB = ManagedEnvironment::factory()->create([
|
||||
@ -173,20 +220,16 @@ function makeAssignedFindingForInbox(ManagedEnvironment $tenant, User $assignee,
|
||||
(string) $tenantA->workspace_id => (int) $tenantB->getKey(),
|
||||
]);
|
||||
|
||||
$component = myWorkInboxPage($user)
|
||||
->assertSet('tableFilters.managed_environment_id.value', (string) $tenantB->getKey())
|
||||
->assertCanSeeTableRecords([$findingB])
|
||||
->assertCanNotSeeTableRecords([$findingA])
|
||||
->assertActionVisible('clear_tenant_filter');
|
||||
|
||||
expect($component->instance()->appliedScope())->toBe([
|
||||
'workspace_scoped' => true,
|
||||
'assignee_scope' => 'current_user_only',
|
||||
'tenant_prefilter_source' => 'active_tenant_context',
|
||||
'tenant_label' => $tenantB->name,
|
||||
]);
|
||||
|
||||
$component->callAction('clear_tenant_filter')
|
||||
$component = myWorkInboxPage($user, [
|
||||
'tenant' => (string) $tenantB->external_id,
|
||||
'tenant_id' => (int) $tenantB->getKey(),
|
||||
'managed_environment_id' => (int) $tenantB->getKey(),
|
||||
'tenant_scope' => 'environment',
|
||||
'tableFilters' => [
|
||||
'managed_environment_id' => ['value' => (string) $tenantB->getKey()],
|
||||
],
|
||||
])
|
||||
->assertSet('tableFilters.managed_environment_id.value', null)
|
||||
->assertCanSeeTableRecords([$findingA, $findingB]);
|
||||
|
||||
expect($component->instance()->appliedScope())->toBe([
|
||||
@ -282,9 +325,7 @@ function makeAssignedFindingForInbox(ManagedEnvironment $tenant, User $assignee,
|
||||
'subject_external_id' => 'available-elsewhere',
|
||||
]);
|
||||
|
||||
$component = myWorkInboxPage($user, [
|
||||
'tenant' => (string) $tenantA->external_id,
|
||||
])
|
||||
$component = myWorkInboxPage($user, ['environment_id' => (int) $tenantA->getKey()])
|
||||
->assertCanNotSeeTableRecords([])
|
||||
->assertSee('No assigned findings match this environment scope')
|
||||
->assertTableEmptyStateActionsExistInOrder(['clear_tenant_filter_empty']);
|
||||
@ -306,7 +347,7 @@ function makeAssignedFindingForInbox(ManagedEnvironment $tenant, User $assignee,
|
||||
->assertTableEmptyStateActionsExistInOrder(['choose_environment_empty']);
|
||||
});
|
||||
|
||||
it('uses the active visible environment for the calm empty-state drillback when environment context exists', function (): void {
|
||||
it('keeps the calm empty-state drillback workspace-owned when remembered environment context exists', function (): void {
|
||||
[$user, $tenant] = myWorkInboxActingUser();
|
||||
|
||||
session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [
|
||||
@ -315,13 +356,13 @@ function makeAssignedFindingForInbox(ManagedEnvironment $tenant, User $assignee,
|
||||
|
||||
$component = myWorkInboxPage($user)
|
||||
->assertSee('No visible assigned findings right now')
|
||||
->assertTableEmptyStateActionsExistInOrder(['open_tenant_findings_empty']);
|
||||
->assertTableEmptyStateActionsExistInOrder(['choose_environment_empty']);
|
||||
|
||||
expect($component->instance()->emptyState())->toMatchArray([
|
||||
'action_name' => 'open_tenant_findings_empty',
|
||||
'action_label' => 'Open environment findings',
|
||||
'action_name' => 'choose_environment_empty',
|
||||
'action_label' => 'Choose an environment',
|
||||
'action_kind' => 'url',
|
||||
'action_url' => FindingResource::getUrl('index', panel: 'admin', tenant: $tenant),
|
||||
'action_url' => route('filament.admin.pages.choose-environment'),
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
@ -44,6 +44,13 @@
|
||||
->and(WorkspaceHubRegistry::isWorkspaceHubPath('/admin/workspaces/1/environments/2/baseline-compare'))->toBeFalse()
|
||||
->and(WorkspaceHubRegistry::isExplicitlyExcludedPath('/admin/workspaces/1/environments/2/baseline-compare'))->toBeTrue()
|
||||
->and(WorkspaceHubRegistry::isWorkspaceHubPath('/admin/baseline-compare-landing'))->toBeFalse()
|
||||
->and(WorkspaceHubRegistry::isWorkspaceHubPath('/admin/baseline-profiles'))->toBeFalse()
|
||||
->and(WorkspaceHubRegistry::isWorkspaceHubPath('/admin/baseline-profiles/42/compare-matrix'))->toBeFalse()
|
||||
->and(WorkspaceHubRegistry::isWorkspaceHubPath('/admin/baseline-snapshots'))->toBeFalse()
|
||||
->and(WorkspaceHubRegistry::isWorkspaceHubPath('/admin/findings/my-work'))->toBeFalse()
|
||||
->and(WorkspaceHubRegistry::isWorkspaceHubPath('/admin/findings/intake'))->toBeFalse()
|
||||
->and(WorkspaceHubRegistry::isWorkspaceHubPath('/admin/findings/hygiene'))->toBeFalse()
|
||||
->and(WorkspaceHubRegistry::isWorkspaceHubPath('/admin/cross-environment-compare'))->toBeFalse()
|
||||
->and(WorkspaceHubRegistry::isWorkspaceHubPath('/admin/workspaces/1/environments/2/stored-reports'))->toBeFalse()
|
||||
->and(WorkspaceHubRegistry::isExplicitlyExcludedPath('/admin/workspaces/1/environments/2/stored-reports'))->toBeTrue();
|
||||
});
|
||||
|
||||
@ -83,6 +83,81 @@
|
||||
->and($resolved->recoveryReason)->toBeNull();
|
||||
});
|
||||
|
||||
it('keeps workspace owned analysis surfaces tenantless when a remembered environment exists', function (string $path): void {
|
||||
$rememberedEnvironment = ManagedEnvironment::factory()->active()->create(['name' => 'Remembered ManagedEnvironment']);
|
||||
[$user, $rememberedEnvironment] = createUserWithTenant(tenant: $rememberedEnvironment, role: 'owner');
|
||||
|
||||
$this->actingAs($user);
|
||||
Filament::setTenant(null, true);
|
||||
|
||||
$workspaceId = (int) $rememberedEnvironment->workspace_id;
|
||||
|
||||
session()->put(WorkspaceContext::SESSION_KEY, $workspaceId);
|
||||
session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [
|
||||
(string) $workspaceId => (int) $rememberedEnvironment->getKey(),
|
||||
]);
|
||||
|
||||
$request = Request::create($path);
|
||||
$request->setLaravelSession(app('session.store'));
|
||||
$request->setUserResolver(static fn () => $user);
|
||||
|
||||
$resolved = app(OperateHubShell::class)->resolvedContext($request);
|
||||
|
||||
expect($resolved->workspace?->getKey())->toBe($workspaceId)
|
||||
->and($resolved->tenant)->toBeNull()
|
||||
->and($resolved->tenantSource)->toBe('none')
|
||||
->and($resolved->state)->toBe('tenantless_workspace');
|
||||
})->with([
|
||||
'baseline profiles list' => ['/admin/baseline-profiles'],
|
||||
'baseline profiles detail' => ['/admin/baseline-profiles/42'],
|
||||
'baseline profiles edit' => ['/admin/baseline-profiles/42/edit'],
|
||||
'baseline profiles compare matrix' => ['/admin/baseline-profiles/42/compare-matrix'],
|
||||
'baseline snapshots list' => ['/admin/baseline-snapshots'],
|
||||
'baseline snapshots detail' => ['/admin/baseline-snapshots/42'],
|
||||
'my findings' => ['/admin/findings/my-work'],
|
||||
'findings intake' => ['/admin/findings/intake'],
|
||||
'findings hygiene' => ['/admin/findings/hygiene'],
|
||||
'cross-environment compare' => ['/admin/cross-environment-compare'],
|
||||
]);
|
||||
|
||||
it('does not resolve explicit environment_id query hints as shell tenant context on workspace owned analysis surfaces', function (string $path): void {
|
||||
$workspaceTenant = ManagedEnvironment::factory()->active()->create(['name' => 'Workspace ManagedEnvironment']);
|
||||
[$user, $workspaceTenant] = createUserWithTenant(tenant: $workspaceTenant, role: 'owner');
|
||||
|
||||
$hintedTenant = ManagedEnvironment::factory()->active()->create([
|
||||
'workspace_id' => (int) $workspaceTenant->workspace_id,
|
||||
'name' => 'Hinted ManagedEnvironment',
|
||||
]);
|
||||
createUserWithTenant(tenant: $hintedTenant, user: $user, role: 'owner');
|
||||
|
||||
$this->actingAs($user);
|
||||
Filament::setTenant(null, true);
|
||||
|
||||
$workspaceId = (int) $workspaceTenant->workspace_id;
|
||||
|
||||
session()->put(WorkspaceContext::SESSION_KEY, $workspaceId);
|
||||
|
||||
$request = Request::create($path, parameters: [
|
||||
'environment_id' => (int) $hintedTenant->getKey(),
|
||||
]);
|
||||
$request->setLaravelSession(app('session.store'));
|
||||
$request->setUserResolver(static fn () => $user);
|
||||
|
||||
$resolved = app(OperateHubShell::class)->resolvedContext($request);
|
||||
|
||||
expect($resolved->workspace?->getKey())->toBe($workspaceId)
|
||||
->and($resolved->tenant)->toBeNull()
|
||||
->and($resolved->tenantSource)->toBe('none')
|
||||
->and($resolved->state)->toBe('tenantless_workspace');
|
||||
})->with([
|
||||
'baseline profiles' => ['/admin/baseline-profiles'],
|
||||
'baseline snapshots' => ['/admin/baseline-snapshots'],
|
||||
'my findings' => ['/admin/findings/my-work'],
|
||||
'findings intake' => ['/admin/findings/intake'],
|
||||
'findings hygiene' => ['/admin/findings/hygiene'],
|
||||
'cross-environment compare' => ['/admin/cross-environment-compare'],
|
||||
]);
|
||||
|
||||
it('uses the routed tenant workspace when the tenant panel is entered without a selected workspace session', function (): void {
|
||||
$tenant = ManagedEnvironment::factory()->active()->create(['name' => 'ManagedEnvironment Panel Scope']);
|
||||
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner');
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Support\Navigation\AdminSurfaceScope;
|
||||
use App\Support\Tenants\TenantInteractionLane;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
@ -17,6 +18,16 @@
|
||||
'retired tenant panel route' => ['/admin/t/tenant-123', AdminSurfaceScope::WorkspaceScoped],
|
||||
'workspace environment detail' => ['/admin/workspaces/acme/environments/tenant-123', AdminSurfaceScope::EnvironmentBound],
|
||||
'baseline compare environment route' => ['/admin/workspaces/acme/environments/tenant-123/baseline-compare', AdminSurfaceScope::EnvironmentBound],
|
||||
'baseline profiles list' => ['/admin/baseline-profiles', AdminSurfaceScope::WorkspaceOwnedAnalysisSurface],
|
||||
'baseline profiles detail' => ['/admin/baseline-profiles/42', AdminSurfaceScope::WorkspaceOwnedAnalysisSurface],
|
||||
'baseline profiles edit' => ['/admin/baseline-profiles/42/edit', AdminSurfaceScope::WorkspaceOwnedAnalysisSurface],
|
||||
'baseline profiles compare matrix' => ['/admin/baseline-profiles/42/compare-matrix', AdminSurfaceScope::WorkspaceOwnedAnalysisSurface],
|
||||
'baseline snapshots list' => ['/admin/baseline-snapshots', AdminSurfaceScope::WorkspaceOwnedAnalysisSurface],
|
||||
'baseline snapshots detail' => ['/admin/baseline-snapshots/42', AdminSurfaceScope::WorkspaceOwnedAnalysisSurface],
|
||||
'my findings inbox' => ['/admin/findings/my-work', AdminSurfaceScope::WorkspaceOwnedAnalysisSurface],
|
||||
'findings intake' => ['/admin/findings/intake', AdminSurfaceScope::WorkspaceOwnedAnalysisSurface],
|
||||
'findings hygiene' => ['/admin/findings/hygiene', AdminSurfaceScope::WorkspaceOwnedAnalysisSurface],
|
||||
'cross-environment compare' => ['/admin/cross-environment-compare', AdminSurfaceScope::WorkspaceOwnedAnalysisSurface],
|
||||
'tenant scoped evidence detail' => ['/admin/evidence/123', AdminSurfaceScope::EnvironmentScopedEvidence],
|
||||
'evidence overview' => ['/admin/evidence/overview', AdminSurfaceScope::WorkspaceWideSurface],
|
||||
'customer review workspace' => ['/admin/reviews/workspace', AdminSurfaceScope::WorkspaceWideSurface],
|
||||
@ -31,3 +42,14 @@
|
||||
'retired operation run detail' => ['/admin/operations/44', AdminSurfaceScope::WorkspaceScoped],
|
||||
'operation run detail' => ['/admin/workspaces/acme/operations/44', AdminSurfaceScope::CanonicalWorkspaceRecordViewer],
|
||||
]);
|
||||
|
||||
it('keeps workspace owned analysis surfaces tenantless without query hint or remembered environment restore', function (): void {
|
||||
$surface = AdminSurfaceScope::WorkspaceOwnedAnalysisSurface;
|
||||
|
||||
expect($surface->allowsQueryEnvironmentHints())->toBeFalse()
|
||||
->and($surface->allowsRememberedEnvironmentRestore())->toBeFalse()
|
||||
->and($surface->allowsEnvironmentlessState())->toBeTrue()
|
||||
->and($surface->forcesEnvironmentlessShellContext())->toBeTrue()
|
||||
->and($surface->requiresExplicitEnvironment())->toBeFalse()
|
||||
->and($surface->lane())->toBe(TenantInteractionLane::StandardActiveOperating);
|
||||
});
|
||||
|
||||
|
After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 71 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 53 KiB |
@ -0,0 +1,75 @@
|
||||
# Specification Quality Checklist: Workspace-Owned Analysis Surface Registration & Shell Cutover
|
||||
|
||||
**Purpose**: Validate specification completeness and quality before implementation
|
||||
**Created**: 2026-05-16
|
||||
**Feature**: [spec.md](/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/320-workspace-owned-analysis-surface-registration-shell-cutover/spec.md)
|
||||
|
||||
## Candidate Selection Gate
|
||||
|
||||
- [x] Explicit user-provided Spec 320 request was selected as the source of truth for this preparation pass.
|
||||
- [x] Completed-spec guardrail checked that no existing `specs/320-*` package was present before generation.
|
||||
- [x] Specs 313, 314, 315, 316, 317, 318, and 319 were treated as dependency/historical context, not rewritten.
|
||||
- [x] Roadmap/spec-candidate queue was reviewed; active auto-prep queue is empty, so this package proceeds only because the user directly supplied/promoted Spec 320.
|
||||
- [x] Close alternatives were deferred to follow-up Specs 321 and 322.
|
||||
- [x] The selected slice is workspace-owned analysis/library shell classification and remembered Environment fallback removal only.
|
||||
|
||||
## Content Quality
|
||||
|
||||
- [x] Problem statement is operator-visible and tied to workspace-owned page / Environment shell mismatch.
|
||||
- [x] User value is clear: clean workspace-owned analysis URLs show Workspace shell only.
|
||||
- [x] Scope is bounded to Spec 318 M2/M4 targets plus directly related unregistered workspace analysis pages.
|
||||
- [x] Hard cutover/no compatibility posture is explicit.
|
||||
- [x] No unresolved clarification-marker placeholders remain.
|
||||
- [x] Mandatory Spec Candidate Check is complete.
|
||||
- [x] Spec Scope Fields are complete.
|
||||
- [x] Shared pattern, OperationRun, provider boundary, UI/surface, testing, acceptance, and browser sections are complete.
|
||||
|
||||
## Requirement Completeness
|
||||
|
||||
- [x] Functional requirements are testable and unambiguous.
|
||||
- [x] Requirements cover Baselines, Baseline Snapshots, baseline detail/edit/matrix pages, My Findings, Findings Intake, Findings Hygiene, Cross-environment Compare, legacy aliases, remembered fallback, reload/back-forward, Baseline Compare regression, and Decision Register regression.
|
||||
- [x] Non-goals prevent Spec 321/322 scope creep.
|
||||
- [x] Edge cases are identified.
|
||||
- [x] Assumptions and risks are documented.
|
||||
- [x] Success and acceptance criteria are measurable.
|
||||
- [x] Open questions do not block implementation; they require per-page documentation during implementation.
|
||||
|
||||
## Plan Quality
|
||||
|
||||
- [x] Laravel, Filament, Livewire, Pest, PostgreSQL, Sail, and Dokploy context is recorded.
|
||||
- [x] Livewire v4.0+ compliance is explicitly noted through Livewire 4.1.4.
|
||||
- [x] Laravel 12 panel provider location remains `apps/platform/bootstrap/providers.php`.
|
||||
- [x] Global search impact is assessed as unchanged unless a touched Resource is deliberately changed and tested.
|
||||
- [x] Destructive/high-impact action handling is addressed for existing archive/capture/compare/preflight actions.
|
||||
- [x] Asset strategy is assessed as no new Filament assets/no new `filament:assets` step.
|
||||
- [x] No migration, seeder, package, env var, queue, scheduler, storage, or deployment asset change is planned.
|
||||
- [x] Existing repo seams are named.
|
||||
- [x] Test strategy and browser verification plan are concrete.
|
||||
|
||||
## Task Quality
|
||||
|
||||
- [x] Tasks are ordered from guardrails/tests through classification, page/link alignment, browser verification, and final validation.
|
||||
- [x] Tests are required before or alongside implementation.
|
||||
- [x] Task IDs follow the required checkbox format.
|
||||
- [x] File paths are concrete where repo surfaces are known.
|
||||
- [x] Non-tasks explicitly prevent compatibility layers, query alias support, Baseline Compare changes, and follow-up-spec scope creep.
|
||||
- [x] Browser screenshot paths are specified.
|
||||
- [x] Validation commands are specified.
|
||||
|
||||
## Constitution Alignment
|
||||
|
||||
- [x] Workspace and Environment isolation are covered.
|
||||
- [x] Cross-workspace Environment filters cannot create shell context or leak identity.
|
||||
- [x] No new persisted truth is introduced.
|
||||
- [x] Possible classifier complexity has a proportionality review.
|
||||
- [x] OperationRun semantics are preserved for existing high-impact actions.
|
||||
- [x] Audit/authorization/confirmation expectations for existing high-impact actions remain explicit.
|
||||
- [x] Test governance lane impact is explicit.
|
||||
- [x] Provider/platform boundary is explicit.
|
||||
|
||||
## Readiness Result
|
||||
|
||||
- [x] Candidate Selection Gate passes.
|
||||
- [x] Spec Readiness Gate passes.
|
||||
- [x] Ready for separate implementation loop.
|
||||
- [x] No application implementation was performed during this preparation step.
|
||||
@ -0,0 +1,287 @@
|
||||
# Implementation Plan: Workspace-Owned Analysis Surface Registration & Shell Cutover
|
||||
|
||||
**Branch**: `320-workspace-owned-analysis-surface-registration-shell-cutover` | **Date**: 2026-05-16 | **Spec**: [spec.md](/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/320-workspace-owned-analysis-surface-registration-shell-cutover/spec.md)
|
||||
**Input**: Feature specification from `/specs/320-workspace-owned-analysis-surface-registration-shell-cutover/spec.md`
|
||||
|
||||
**Preparation status**: Specification artifacts only. No runtime implementation has been performed by this preparation step.
|
||||
|
||||
## Summary
|
||||
|
||||
Spec 320 hard-cuts workspace-owned analysis/library pages away from remembered Environment shell inheritance:
|
||||
|
||||
```text
|
||||
Workspace-owned analysis surface
|
||||
-> clean workspace route
|
||||
-> Workspace shell only
|
||||
-> no active Environment shell
|
||||
-> no remembered Environment fallback
|
||||
-> optional Environment filter only through canonical environment_id + visible chip
|
||||
```
|
||||
|
||||
The implementation must classify the audited workspace-owned analysis surfaces from Spec 318, especially Baselines/Baseline Profiles, Baseline Snapshots, Baseline Compare Matrix, My Findings, Findings Intake, Findings Hygiene, and Cross-environment Compare. Baseline Compare remains Environment-owned from Spec 319. Alerts/Audit Log filter decisions remain Spec 321.
|
||||
|
||||
## 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, Laravel Sail
|
||||
**Storage**: PostgreSQL; no schema changes planned
|
||||
**Testing**: Pest 4 / PHPUnit 12, Filament/Livewire tests, focused browser smoke
|
||||
**Validation Lanes**: fast-feedback, confidence, browser
|
||||
**Target Platform**: `apps/platform` Laravel/Filament admin panel, local Sail, staging/production through Dokploy
|
||||
**Project Type**: Web application
|
||||
**Performance Goals**: No material performance change. Classification and shell resolution must remain cheap request-time logic.
|
||||
**Constraints**: No migrations, seeders, packages, env vars, queues, scheduler, storage, deployment assets, compatibility routes, redirects, or legacy query alias support.
|
||||
**Scale/Scope**: Focused shell/context classification for workspace-owned analysis surfaces plus targeted regressions for Specs 314-319.
|
||||
|
||||
## UI / Surface Guardrail Plan
|
||||
|
||||
- **Guardrail scope**: route/shell/query/copy/navigation behavior for existing operator-facing workspace-owned analysis pages.
|
||||
- **Native vs custom classification summary**: Existing Filament Resources/Pages and Blade views. No styling redesign.
|
||||
- **Shared-family relevance**: navigation, shell context, breadcrumbs, filter chips, clear actions, OperationRun links.
|
||||
- **State layers in scope**: shell, page, record/detail, URL query, Filament table filter state, Livewire referer classification.
|
||||
- **Audience modes in scope**: operator-MSP and support-platform.
|
||||
- **Decision/diagnostic/raw hierarchy plan**: Workspace ownership must be default-visible; Environment context may appear as data/filter state only.
|
||||
- **Raw/support gating plan**: no raw/support evidence exposure change.
|
||||
- **One-primary-action / duplicate-truth control**: The shell is the single ownership signal; page filters/selectors do not become shell ownership.
|
||||
- **Handling modes by drift class or surface**: hard-stop for remembered Environment shell on in-scope clean URLs; review-mandatory for any newly discovered workspace analysis route.
|
||||
- **Repository-signal treatment**: Spec 318 M2/M4 are implementation inputs; pages outside those findings are inspect-only unless repo evidence proves same mismatch.
|
||||
- **Special surface test profiles**: `global-context-shell`.
|
||||
- **Required tests or manual smoke**: classifier tests, URL/query tests, Livewire/Feature render tests, browser smoke for workspace origin, Environment origin, reload, and back/forward.
|
||||
- **Exception path and spread control**: A distinct workspace-owned analysis classifier is allowed only if `WorkspaceHubRegistry` would overstate hub/filter behavior. Document the choice in implementation close-out.
|
||||
- **Active feature PR close-out entry**: Guardrail / Exception / Smoke Coverage.
|
||||
|
||||
## Shared Pattern & System Fit
|
||||
|
||||
- **Cross-cutting feature marker**: yes.
|
||||
- **Systems touched**: `AdminSurfaceScope`, `WorkspaceHubRegistry`, `WorkspaceSidebarNavigation`, `WorkspaceHubEnvironmentFilter`, `WorkspaceHubFilterStateResetter`, `WorkspaceScopedEnvironmentRoutes`, `ManagedEnvironmentLinks`, `OperateHubShell`, baseline resources/pages, findings analysis pages, cross-environment compare page, tests, browser smoke artifacts.
|
||||
- **Shared abstractions reused**: existing workspace hub shell contract, `AdminSurfaceScope`, `WorkspaceHubRegistry` query cleaning, `WorkspaceHubEnvironmentFilter`, `CanonicalAdminEnvironmentFilterState`, and current route/link helpers.
|
||||
- **New abstraction introduced? why?**: none preferred. If necessary, add the narrowest possible `AdminSurfaceScope` classification for workspace-owned analysis surfaces to force environmentless shell without implying full workspace-hub behavior.
|
||||
- **Why the existing abstraction was sufficient or insufficient**: `WorkspaceHubRegistry` works for registered hubs; generic `WorkspaceScoped` is insufficient because it allows remembered Environment restore.
|
||||
- **Bounded deviation / spread control**: Do not introduce a general surface taxonomy beyond the audited route list. Do not register pages as hubs unless they truly satisfy hub/filter/clear behavior.
|
||||
|
||||
## OperationRun UX Impact
|
||||
|
||||
- **Touches OperationRun start/completion/link UX?**: yes, as regression only for existing baseline capture/compare actions.
|
||||
- **Central contract reused**: `OperationUxPresenter`, `OpsUxBrowserEvents`, `OperationRunLinks`, current services/jobs.
|
||||
- **Delegated UX behaviors**: existing queued toast, run link, browser event, authorization, and audit behavior remain unchanged.
|
||||
- **Surface-owned behavior kept local**: existing initiation inputs/actions on Baseline Profile and Baseline Compare Matrix.
|
||||
- **Queued DB-notification policy**: N/A.
|
||||
- **Terminal notification path**: Existing central lifecycle only.
|
||||
- **Exception path**: none.
|
||||
|
||||
## Provider Boundary & Portability Fit
|
||||
|
||||
- **Shared provider/platform boundary touched?**: yes.
|
||||
- **Provider-owned seams**: Microsoft/Intune baseline content, provider IDs, Graph-backed snapshot evidence, compare strategy internals.
|
||||
- **Platform-core seams**: Workspace/Environment route ownership, shell classification, query key semantics, navigation ownership language.
|
||||
- **Neutral platform terms / contracts preserved**: Workspace, Environment, workspace-owned analysis surface, workspace hub, filtered workspace hub, Environment-owned page.
|
||||
- **Retained provider-specific semantics and why**: Baseline content remains Microsoft/Intune-shaped because it is current provider implementation truth.
|
||||
- **Bounded extraction or follow-up path**: Spec 321 and Spec 322 remain follow-ups; no provider abstraction work here.
|
||||
|
||||
## Constitution Check
|
||||
|
||||
*GATE: Must pass before implementation. Re-check after runtime changes.*
|
||||
|
||||
- Inventory-first: no inventory/snapshot truth change. Baseline snapshots remain immutable evidence/artifacts.
|
||||
- Read/write separation: no new write behavior. Existing capture/compare/preflight actions keep confirmation, authorization, audit, and OperationRun semantics.
|
||||
- Graph contract path: no new Graph calls; page render must remain DB-only.
|
||||
- Deterministic capabilities: existing capability checks remain; route/shell classification must be testable.
|
||||
- RBAC-UX: non-member / not entitled Workspace or Environment access remains deny-as-not-found where applicable; member missing capability follows existing behavior.
|
||||
- Workspace isolation: current Workspace remains the primary shell and authorization boundary.
|
||||
- Tenant/Environment isolation: Environment-owned rows remain scoped by accessible environments; optional filters must resolve only inside current Workspace and entitlement.
|
||||
- Run observability: existing baseline operation runs remain observable; no new operation type.
|
||||
- Test governance: lane purpose, heavy/browser visibility, fixture cost, and reviewer handoff are explicit.
|
||||
- Proportionality: possible classifier addition is justified by current browser-proven shell mismatch and bounded route list.
|
||||
- No premature abstraction: prefer extending existing classifier/registry paths over a new framework.
|
||||
- Persisted truth: no tables/entities/artifacts added.
|
||||
- Behavioral state: no new business state/status/reason family.
|
||||
- UI semantics: no new badge/status taxonomy; direct shell/page/filter truth only.
|
||||
- Shared pattern first: reuse workspace hub and shell resolution contracts.
|
||||
- Provider boundary: no provider tenant ID alias or fallback becomes platform shell truth.
|
||||
- V1 explicitness / few layers: hard cutover, no compatibility layer.
|
||||
- Filament-native UI: no ad-hoc styling or published internals.
|
||||
- Filament v5 / Livewire v4: Livewire 4.1.4 satisfies Filament v5 requirement; no Livewire v3 APIs.
|
||||
- Provider registration: Laravel 12 panel providers remain in `apps/platform/bootstrap/providers.php`; no provider registration work planned.
|
||||
- Global search: no global search behavior should change. Baseline Profile and Baseline Snapshot global search remain disabled unless implementation explicitly verifies a safe View/Edit route and updates tests.
|
||||
- Destructive/high-impact actions: no new destructive action. Existing archive/capture/compare/preflight actions must keep `->action(...)`, confirmation where required, authorization, audit, notifications, and tests.
|
||||
- Asset strategy: no Filament assets planned; no new `filament:assets` deployment requirement.
|
||||
|
||||
## Test Governance Check
|
||||
|
||||
- **Test purpose / classification by changed surface**: Unit for classifier/registry; Feature/Livewire for shell/access/filter behavior; Browser for integrated shell/sidebar/reload/history.
|
||||
- **Affected validation lanes**: fast-feedback, confidence, browser.
|
||||
- **Why this lane mix is the narrowest sufficient proof**: The defect is both classification logic and browser-visible shell drift.
|
||||
- **Narrowest proving command(s)**:
|
||||
- `cd apps/platform && ./vendor/bin/sail artisan test --filter=AdminSurfaceScope`
|
||||
- `cd apps/platform && ./vendor/bin/sail artisan test --filter=WorkspaceHubRegistry`
|
||||
- `cd apps/platform && ./vendor/bin/sail artisan test --filter=WorkspaceOwnedAnalysis`
|
||||
- `cd apps/platform && ./vendor/bin/sail artisan test --filter=BaselineCompareEnvironmentRouteContract`
|
||||
- `cd apps/platform && ./vendor/bin/sail artisan test --filter=WorkspaceHubEnvironmentFilterContract`
|
||||
- focused Spec 320 browser smoke.
|
||||
- **Fixture / helper / factory / seed / context cost risks**: workspace/environment/member plus small baseline/finding fixtures; keep any full browser fixture explicit and named.
|
||||
- **Expensive defaults or shared helper growth introduced?**: no. If a helper is needed, make expensive setup opt-in.
|
||||
- **Heavy-family additions, promotions, or visibility changes**: explicit browser smoke only; no broad discovery guard in this spec.
|
||||
- **Surface-class relief / special coverage rule**: `global-context-shell`.
|
||||
- **Closing validation and reviewer handoff**: prove clean URLs, filtered URLs, legacy aliases, remembered fallback rejection, Baseline Compare regression, Decision Register regression, and browser screenshots.
|
||||
- **Budget / baseline / trend follow-up**: none expected; document if browser smoke runtime grows materially.
|
||||
- **Review-stop questions**: Are any workspace-owned analysis URLs still generic `WorkspaceScoped` with remembered restore? Did any page get registered as a hub without supporting hub/filter/clear behavior? Did legacy aliases return?
|
||||
- **Escalation path**: document-in-feature.
|
||||
- **Active feature PR close-out entry**: Guardrail / Exception / Smoke Coverage.
|
||||
- **Why no dedicated follow-up spec is needed**: This is the dedicated follow-up for Spec 318 M2/M4. Alerts/Audit and durable browser guard already have 321/322.
|
||||
|
||||
## Project Structure
|
||||
|
||||
### Documentation (this feature)
|
||||
|
||||
```text
|
||||
specs/320-workspace-owned-analysis-surface-registration-shell-cutover/
|
||||
+-- spec.md
|
||||
+-- plan.md
|
||||
+-- tasks.md
|
||||
+-- checklists/
|
||||
+-- requirements.md
|
||||
```
|
||||
|
||||
### Source Code (likely affected during later implementation)
|
||||
|
||||
```text
|
||||
apps/platform/app/
|
||||
+-- Support/Navigation/
|
||||
| +-- AdminSurfaceScope.php
|
||||
| +-- WorkspaceHubRegistry.php
|
||||
| +-- WorkspaceSidebarNavigation.php
|
||||
+-- Support/OperateHub/OperateHubShell.php
|
||||
+-- Support/ManagedEnvironmentLinks.php
|
||||
+-- Filament/Resources/
|
||||
| +-- BaselineProfileResource.php
|
||||
| +-- BaselineSnapshotResource.php
|
||||
+-- Filament/Pages/
|
||||
| +-- BaselineCompareMatrix.php
|
||||
| +-- CrossEnvironmentComparePage.php
|
||||
| +-- Findings/
|
||||
| +-- MyFindingsInbox.php
|
||||
| +-- FindingsIntakeQueue.php
|
||||
| +-- FindingsHygieneReport.php
|
||||
+-- Filament/Concerns/WorkspaceScopedEnvironmentRoutes.php
|
||||
|
||||
apps/platform/tests/
|
||||
+-- Unit/Tenants/AdminSurfaceScopeTest.php
|
||||
+-- Unit/Support/OperateHub/OperateHubShellResolutionTest.php
|
||||
+-- Feature/Navigation/WorkspaceHubRegistryTest.php
|
||||
+-- Feature/Navigation/WorkspaceHubEnvironmentFilterContractTest.php
|
||||
+-- Feature/Filament/
|
||||
| +-- BaselineCompareEnvironmentRouteContractTest.php
|
||||
| +-- BaselineProfile*.php
|
||||
| +-- BaselineSnapshot*.php
|
||||
+-- Feature/Findings/
|
||||
| +-- MyFindingsInboxNavigationContextTest.php
|
||||
| +-- FindingsIntakeQueueNavigationContextTest.php
|
||||
+-- Browser/
|
||||
+-- Spec320WorkspaceOwnedAnalysisSurfaceSmokeTest.php
|
||||
```
|
||||
|
||||
**Structure Decision**: Use existing Laravel/Filament app structure. Do not create new base folders. Add test files only where the existing test families already live, or extend existing focused files when lower churn is clearer.
|
||||
|
||||
## Complexity Tracking
|
||||
|
||||
| Violation | Why Needed | Simpler Alternative Rejected Because |
|
||||
|-----------|------------|-------------------------------------|
|
||||
| Possible new `AdminSurfaceScope` value | Generic `WorkspaceScoped` allows remembered Environment restore; workspace hubs already force environmentless shell but not every analysis page is a hub | Blindly adding every page to `WorkspaceHubRegistry` could falsely imply hub/filter/clear behavior |
|
||||
|
||||
## Proportionality Review
|
||||
|
||||
- **Current operator problem**: Workspace-owned pages can appear Environment-owned in the shell with no filter chip or route ownership.
|
||||
- **Existing structure is insufficient because**: `WorkspaceScoped` restores remembered Environment; `WorkspaceHubRegistry` is reserved for hub-like surfaces.
|
||||
- **Narrowest correct implementation**: Classify a route list or narrow scope category as workspace-owned analysis and make it force environmentless shell.
|
||||
- **Ownership cost created**: Focused classifier tests, URL/filter tests, browser smoke, and implementation close-out documentation.
|
||||
- **Alternative intentionally rejected**: Keep generic fallback, add legacy aliases, or add all pages to workspace hub registry regardless of actual hub behavior.
|
||||
- **Release truth**: Current-release cleanup before production.
|
||||
|
||||
## Implementation Phases
|
||||
|
||||
### Phase 0 - Repo Verification
|
||||
|
||||
Re-read Spec 318 artifacts and current code paths. Confirm the final in-scope list before runtime edits:
|
||||
|
||||
- Baselines/Baseline Profiles.
|
||||
- Baseline Profile view/edit/create if applicable.
|
||||
- Baseline Compare Matrix.
|
||||
- Baseline Snapshots.
|
||||
- My Findings, Findings Intake, Findings Hygiene.
|
||||
- Cross-environment Compare.
|
||||
- Any other Spec 318 unregistered workspace analysis surface still present in routes/navigation.
|
||||
|
||||
### Phase 1 - Tests First
|
||||
|
||||
Add focused tests that fail on current remembered Environment inheritance:
|
||||
|
||||
- `AdminSurfaceScope` classification for in-scope paths.
|
||||
- Shell resolution with remembered Environment present.
|
||||
- Clean URL opens without active Environment.
|
||||
- Legacy alias rejection.
|
||||
- Optional `environment_id` filter behavior where supported.
|
||||
- Baseline Compare and Decision Register regressions.
|
||||
|
||||
### Phase 2 - Classification / Registry Cutover
|
||||
|
||||
Implement the narrowest classifier mechanism:
|
||||
|
||||
- Prefer existing registry/classifier extensions.
|
||||
- Add a distinct workspace-owned analysis surface classification only if needed.
|
||||
- Ensure `forcesEnvironmentlessShellContext()` returns true for these paths.
|
||||
- Ensure `allowsRememberedEnvironmentRestore()` is false for these paths.
|
||||
- Preserve Workspace state and authorization.
|
||||
|
||||
### Phase 3 - Page / Link / Copy Alignment
|
||||
|
||||
Update only in-scope surfaces:
|
||||
|
||||
- Sidebar/global/workspace links emit clean workspace URLs unless explicit `environment_id` filter is supported.
|
||||
- Environment-origin links do not carry active Environment shell ownership.
|
||||
- Headers, breadcrumbs, empty states, and copy use workspace/library/work-queue wording.
|
||||
- Existing Environment columns, selectors, badges, and filters remain data/filter state.
|
||||
|
||||
### Phase 4 - Regression and Browser Proof
|
||||
|
||||
Run focused tests and browser smoke:
|
||||
|
||||
- Baselines workspace origin, environment origin, reload.
|
||||
- Baseline Snapshots workspace origin, environment origin, reload.
|
||||
- Baseline Compare Matrix direct/reload/back-forward.
|
||||
- My Findings/Intake/Hygiene and Cross-environment Compare direct/from Environment context.
|
||||
- Baseline Compare Environment-owned regression.
|
||||
- Decision Register workspace hub/filtered hub regression.
|
||||
|
||||
Save screenshots under:
|
||||
|
||||
```text
|
||||
specs/320-workspace-owned-analysis-surface-registration-shell-cutover/artifacts/screenshots/
|
||||
```
|
||||
|
||||
### Phase 5 - Close-Out
|
||||
|
||||
Document:
|
||||
|
||||
- classification outcomes for Baselines/Baseline Snapshots.
|
||||
- unregistered workspace analysis pages fixed or excluded.
|
||||
- whether any page was reclassified Environment-owned.
|
||||
- how remembered Environment inheritance was prevented.
|
||||
- tests and browser verification.
|
||||
- no migrations/packages/env vars/queues/scheduler/storage/assets.
|
||||
- no backwards compatibility or legacy alias support.
|
||||
|
||||
## Rollout Considerations
|
||||
|
||||
- No database migration or data backfill.
|
||||
- No production compatibility burden because pre-production hard cutover is the standing policy.
|
||||
- No Dokploy runtime change.
|
||||
- No new `filament:assets` deployment step.
|
||||
- Staging validation should run focused tests and browser smoke before promotion.
|
||||
|
||||
## Risk Controls
|
||||
|
||||
- Keep route list explicit and tested.
|
||||
- Do not generalize classifier beyond audited need.
|
||||
- Do not add Environment filter feature work unless a page already has product need and visible chip/clear behavior can be proven.
|
||||
- Replace old tests that assert broken remembered fallback; do not keep them as compatibility coverage.
|
||||
- Keep Spec 321 and Spec 322 out of scope.
|
||||
@ -0,0 +1,344 @@
|
||||
# Feature Specification: Workspace-Owned Analysis Surface Registration & Shell Cutover
|
||||
|
||||
**Feature Branch**: `320-workspace-owned-analysis-surface-registration-shell-cutover`
|
||||
**Created**: 2026-05-16
|
||||
**Status**: Completed
|
||||
**Input**: User supplied Spec 320 draft: workspace-owned analysis and library pages must be classified centrally, open with Workspace shell only, and stop inheriting remembered Environment shell context.
|
||||
|
||||
## Spec Candidate Check *(mandatory - SPEC-GATE-001)*
|
||||
|
||||
- **Problem**: Workspace-owned analysis and library surfaces such as Baselines, Baseline Snapshots, baseline detail/matrix pages, and several unregistered analysis pages can show an active Environment shell when opened from Environment navigation or remembered context.
|
||||
- **Today's failure**: Operators can see `Workspace > Environment` shell on pages whose route and data model are workspace-owned, with no visible Environment filter chip and no route-level Environment ownership.
|
||||
- **User-visible improvement**: Opening a workspace-owned analysis page from sidebar, global navigation, direct URL, reload, or browser history shows Workspace shell only. If an Environment focus is supported, it is explicit through canonical `environment_id` plus a visible chip.
|
||||
- **Smallest enterprise-capable version**: Classify the exact workspace-owned analysis/library surfaces found by Spec 318, prevent remembered Environment shell fallback for them, update links/copy where needed, and add focused regression coverage.
|
||||
- **Explicit non-goals**: No Baseline Compare changes except regression checks, no Alerts/Audit Log filter decision, no durable browser no-drift framework, no redesign, no new product feature, no baseline assignment semantic change, no migrations, no compatibility redirects, no legacy query aliases.
|
||||
- **Permanent complexity imported**: Possible narrow classifier entries or a new `AdminSurfaceScope` value for workspace-owned analysis surfaces, plus targeted tests. No persisted entities, tables, enum/status families, broad registries, or cross-domain UI framework.
|
||||
- **Why now**: Spec 318 identified workspace-owned baseline/analysis surfaces as the remaining opposite-side mismatch after Specs 314-317 stabilized workspace hubs and Spec 319 handles Environment-owned Baseline Compare.
|
||||
- **Why not local**: Page-local copy or sidebar-only fixes would leave direct URLs, Livewire requests, reload/back-forward, and remembered-context shell restoration inconsistent.
|
||||
- **Approval class**: Cleanup / Consolidation.
|
||||
- **Red flags triggered**: New classifier semantics are possible, but the scope is bounded by existing audit evidence and does not create a new product taxonomy beyond shell-context ownership.
|
||||
- **Score**: Nutzen: 2 | Dringlichkeit: 2 | Scope: 2 | Komplexitaet: 2 | Produktnaehe: 2 | Wiederverwendung: 2 | **Gesamt: 12/12**
|
||||
- **Decision**: approve.
|
||||
|
||||
## Spec Scope Fields *(mandatory)*
|
||||
|
||||
- **Scope**: workspace.
|
||||
- **Primary Routes**:
|
||||
- `/admin/baseline-profiles`
|
||||
- `/admin/baseline-profiles/{record}`
|
||||
- `/admin/baseline-profiles/{record}/edit`
|
||||
- `/admin/baseline-profiles/{record}/compare-matrix`
|
||||
- `/admin/baseline-snapshots`
|
||||
- `/admin/baseline-snapshots/{record}`
|
||||
- `/admin/findings/my-work`
|
||||
- `/admin/findings/intake`
|
||||
- `/admin/findings/hygiene`
|
||||
- `/admin/cross-environment-compare`
|
||||
- any additional unregistered workspace analysis surface confirmed from Spec 318 or repo inspection.
|
||||
- **Data Ownership**: Existing workspace-owned Baseline Profile and Baseline Snapshot records, workspace analysis pages that aggregate across accessible Environments, and optional Environment filter state. No schema ownership change.
|
||||
- **RBAC**: Workspace membership and existing workspace capabilities remain required. Environment entitlements still constrain row/data visibility where the existing page aggregates Environment-owned rows. UI visibility is not authorization.
|
||||
|
||||
For canonical-view specs:
|
||||
|
||||
- **Default filter behavior when tenant-context is active**: Clean workspace-owned analysis URLs must ignore active/remembered Environment shell context. They may retain data filters only when the page explicitly supports canonical `environment_id` and shows a visible filter chip.
|
||||
- **Explicit entitlement checks preventing cross-tenant leakage**: Any `environment_id` filter must resolve only inside the current Workspace and only to an Environment visible to the actor. Cross-workspace or unauthorized IDs must not create shell context or leak Environment identity.
|
||||
|
||||
## Cross-Cutting / Shared Pattern Reuse *(mandatory)*
|
||||
|
||||
- **Cross-cutting feature?**: yes.
|
||||
- **Interaction class(es)**: navigation, shell/context bar, breadcrumbs, URL/query state, Filament resource/page routing, filter chips, clear-filter actions, browser reload/history behavior.
|
||||
- **Systems touched**: `AdminSurfaceScope`, `WorkspaceHubRegistry`, `WorkspaceSidebarNavigation`, `WorkspaceScopedEnvironmentRoutes`, `ManagedEnvironmentLinks`, `OperateHubShell`, `WorkspaceHubEnvironmentFilter`, `WorkspaceHubFilterStateResetter`, baseline resources/pages, findings analysis pages, cross-environment compare page, tests, and browser smoke artifacts.
|
||||
- **Existing pattern(s) to extend**: Workspace hub environmentless shell contract from Specs 314-316 and hard-cut no-legacy policy from Spec 317.
|
||||
- **Shared contract / presenter / builder / renderer to reuse**: Prefer existing `AdminSurfaceScope`, `WorkspaceHubRegistry` query cleaning, `WorkspaceHubEnvironmentFilter`/clear-filter behavior, and `OperateHubShell` shell resolution over page-local shell hacks.
|
||||
- **Why the existing shared path is sufficient or insufficient**: Registered workspace hubs already force environmentless shell. The gap is that unregistered workspace analysis pages currently fall into generic `WorkspaceScoped`, which still allows remembered Environment restore.
|
||||
- **Allowed deviation and why**: A distinct `workspace_owned_analysis_surface` classification is allowed only if adding these pages to `WorkspaceHubRegistry` would falsely make them first-class workspace hubs or filtered hub participants.
|
||||
- **Consistency impact**: Sidebar, URL, shell, breadcrumb, page header, copy, filter chip, and data scope must agree on Workspace ownership.
|
||||
- **Review focus**: Verify no remembered Environment, Filament tenant fallback, legacy Tenant query key, or `tableFilters` state creates active Environment shell on in-scope clean URLs.
|
||||
|
||||
## OperationRun UX Impact
|
||||
|
||||
- **Touches OperationRun start/completion/link UX?**: yes, only because existing Baseline Profile/Baseline Compare Matrix actions can start baseline capture/compare work or link to existing runs.
|
||||
- **Shared OperationRun UX contract/layer reused**: Existing `OperationUxPresenter`, `OpsUxBrowserEvents`, `OperationRunLinks`, services, jobs, and audit behavior must remain in place.
|
||||
- **Delegated start/completion UX behaviors**: Existing queued toast, run link, browser event, tenant/workspace-safe URL resolution, authorization, and audit behavior remain delegated to current helpers/services.
|
||||
- **Local surface-owned behavior that remains**: The pages keep their current initiation controls and filtering UI; this spec changes shell/scope classification, not operation semantics.
|
||||
- **Queued DB-notification policy**: N/A - no new DB notification behavior.
|
||||
- **Terminal notification path**: Existing central lifecycle behavior only.
|
||||
- **Exception required?**: none.
|
||||
|
||||
## Provider Boundary / Platform Core Check
|
||||
|
||||
- **Shared provider/platform boundary touched?**: yes.
|
||||
- **Boundary classification**: platform-core route/shell/scope contract; provider-specific Microsoft tenant identity remains provider-owned.
|
||||
- **Seams affected**: shell resolution, navigation scope, route classification, query key handling, operator vocabulary (`Workspace`, `Environment`, `ManagedEnvironment`), and baseline analysis links.
|
||||
- **Neutral platform terms preserved or introduced**: `Workspace`, `Environment`, `workspace-owned analysis surface`, `workspace hub`, `filtered workspace hub`, `Environment-owned page`.
|
||||
- **Provider-specific semantics retained and why**: Existing Intune baseline details, compare strategy, and Graph-backed snapshot content remain provider-specific because they are current product implementation truth.
|
||||
- **Why this does not deepen provider coupling accidentally**: The fix removes hidden Environment/provider tenant fallback from workspace-owned pages instead of adding provider ID routes or alias query support.
|
||||
- **Follow-up path**: Spec 321 decides Alerts/Audit Log `environment_id` behavior. Spec 322 adds durable browser no-drift regression infrastructure.
|
||||
|
||||
## 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 |
|
||||
|---|---:|---|---|---|---|---|
|
||||
| Baselines / Baseline Profiles | yes | Existing Filament Resource | navigation, shell, breadcrumbs, actions | route, shell, page, copy | no | Workspace-owned baseline library. |
|
||||
| Baseline Profile detail/edit | yes | Existing Filament Resource pages | breadcrumbs, header actions, related links | route, shell, record page | no | Workspace-owned record pages. |
|
||||
| Baseline Compare Matrix | yes | Existing Filament Page + Blade view | analysis page, action links, OperationRun links | route, shell, query state | no | Workspace-owned analysis matrix, not Baseline Compare landing. |
|
||||
| Baseline Snapshots | yes | Existing Filament Resource | navigation, shell, filters | route, shell, page, copy | no | Workspace-owned snapshot library/report. |
|
||||
| My Findings / Intake / Hygiene | yes | Existing Filament Pages + tables | analysis queues, filters, clear actions | route, shell, URL query, table filters | no | Workspace-owned analysis pages with optional Environment filtering. |
|
||||
| Cross-environment Compare | yes | Existing Filament Page + form | analysis page and environment selectors | route, shell, query state | no | Workspace-owned portfolio analysis. |
|
||||
| Baseline Compare | yes | Existing Environment-owned page | regression only | route, shell | no | Must remain Environment-owned from Spec 319. |
|
||||
|
||||
## 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 |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| Baselines / Baseline Profiles | Secondary Context | Operator manages reusable baseline definitions | profile name, status, capture mode, snapshot truth, next step | assignments, compare matrix, capture run detail | Library/register, not Environment posture page | Workspace governance library | Removes false selected-Environment ownership. |
|
||||
| Baseline Snapshots | Tertiary Evidence / Diagnostics | Operator inspects immutable snapshot evidence | baseline, captured time, outcome, coverage, next step | snapshot detail and related context | Evidence library, not active Environment dashboard | Workspace baseline evidence review | Keeps evidence scan workspace-owned. |
|
||||
| Baseline Compare Matrix | Secondary Context | Operator reviews assigned-environment compare matrix for one baseline | matrix state, filters, visible assignments, compare readiness | run/finding/environment drilldowns | Workspace-owned analysis across assigned environments | Baseline profile detail to analysis | Clarifies that selected Environment shell is not the page owner. |
|
||||
| My Findings / Intake / Hygiene | Primary/Secondary Decision Surfaces | Operator reviews assigned, intake, or hygiene finding work across visible environments | queue counts, finding state, Environment column/filter | finding detail | Workspace work queues with optional Environment filter | Governance inbox/workspace overview to queue | Environment becomes a filter, not shell owner. |
|
||||
| Cross-environment Compare | Secondary Context | Operator selects source/target environments for portfolio comparison | source, target, policy type selection, preview/preflight state | environment detail links and operation links | Workspace portfolio analysis | Workspace portfolio flow | Avoids hidden current Environment changing a two-environment workflow. |
|
||||
|
||||
## 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 |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| Workspace-owned baseline library/snapshots | operator-MSP, support-platform | Workspace library/snapshot status and next step | snapshot fidelity, compare readiness, related runs | raw Graph payloads remain out of scope/default-hidden | Open profile/snapshot or capture/compare where already allowed | raw payload/debug detail | Shell states Workspace once; Environment appears only as data column/filter. |
|
||||
| Findings analysis queues | operator-MSP, support-platform | queue state, finding severity/status, Environment column/filter | hygiene/intake reason, owner/assignee context | raw provider evidence remains on detail/evidence surfaces | Open finding, claim finding, or clear filter where allowed | support/raw evidence | Active filter chip and shell do not duplicate ownership. |
|
||||
| Cross-environment Compare | operator-MSP, support-platform | source/target selection and preview readiness | preflight detail, operation/run links | raw policy payloads remain out of scope/default-hidden | Generate preflight where allowed | support/raw evidence | Source/target controls are page state, not shell context. |
|
||||
|
||||
## 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 |
|
||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
||||
| Baselines | List / Table / Bulk | Workspace-owned library resource | Open or create baseline profile | Existing clickable row | existing | Existing More/detail header | Existing archive/capture/compare confirmations stay as implemented | `/admin/baseline-profiles` | `/admin/baseline-profiles/{record}` | Workspace shell, no active Environment | Baselines | profile status, capture mode, snapshot truth | none |
|
||||
| Baseline Snapshots | List / Table | Workspace-owned evidence report | Open snapshot | Existing clickable row | existing | Existing related action | N/A | `/admin/baseline-snapshots` | `/admin/baseline-snapshots/{record}` | Workspace shell, no active Environment | Baseline Snapshots | outcome, coverage, next step | none |
|
||||
| Baseline Compare Matrix | Workflow / Page | Workspace-owned analysis page | Compare assigned environments or inspect matrix | Explicit controls/links | N/A | Existing header/actions | Existing high-impact compare action keeps confirmation | `/admin/baseline-profiles/{record}/compare-matrix` | N/A | Workspace shell, explicit source data in page | Compare matrix | baseline and matrix scope | none |
|
||||
| Findings analysis queues | List / Table | Workspace-owned work queues/reports | Open or claim finding | Existing clickable row/action | existing | Existing header/table actions | Existing claim behavior remains authorized | `/admin/findings/my-work`, `/admin/findings/intake`, `/admin/findings/hygiene` | existing finding detail route | Workspace shell, optional Environment filter chip | Findings | queue scope, severity, status, Environment column | none |
|
||||
| Cross-environment Compare | Workflow / Page | Workspace-owned portfolio analysis | Select source/target, generate preflight | Explicit form/actions | N/A | Header actions | Existing promotion preflight/execution confirmations remain | `/admin/cross-environment-compare` | N/A | Workspace shell, explicit source/target selectors | Cross-environment compare | selected environments and preview readiness | none |
|
||||
| Baseline Compare | Workflow / Page | Environment-owned posture page | Regression only | Environment route | N/A | Existing | Existing Compare Now remains confirmed | N/A | `/admin/workspaces/{workspace}/environments/{environment}/baseline-compare` | Workspace + Environment shell | Baseline Compare | selected Environment posture | regression only |
|
||||
|
||||
## 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 |
|
||||
|---|---|---|---|---|---|---|---|---|---|---|
|
||||
| Baselines | Workspace operator / manager | Manage reusable governance baseline definitions | Workspace-owned library | Which baseline should govern environments? | status, capture mode, snapshot truth, assignments count, next step | capture details, compare matrix, runs | lifecycle, data completeness, governance result | TenantPilot baseline library plus existing queued capture/compare | create/open/edit/capture/compare per current capability | archive/capture/compare remain confirmation/authorization governed |
|
||||
| Baseline Snapshots | Workspace operator / support | Inspect immutable baseline evidence | Workspace-owned evidence report | Which baseline snapshots are usable? | captured time, outcome, coverage, next step | fidelity detail, related context | lifecycle, evidence completeness | TenantPilot immutable snapshot evidence | open snapshot/related record | none |
|
||||
| Findings analysis queues | Workspace operator | Review assigned/intake/hygiene work across visible environments | Workspace-owned queues | What governance work needs attention? | finding, severity, status, Environment, queue reason | owner/assignee/hygiene diagnostics | workflow state, severity, SLA | Existing finding workflow only | open finding, claim finding, clear filter | existing mutations remain authorized |
|
||||
| Cross-environment Compare | Workspace operator / manager | Compare/promote configuration across environments | Workspace-owned portfolio analysis | Which source/target comparison is valid? | selected source/target, preview/preflight readiness | preflight detail, run links | selection validity, execution readiness | Existing queued operation / Microsoft read-write behavior | generate preflight / execute if already implemented | existing execution action remains guarded |
|
||||
|
||||
## Proportionality Review *(mandatory when structural complexity is introduced)*
|
||||
|
||||
- **New source of truth?**: no. The classification expresses existing product ownership; it does not create new domain truth.
|
||||
- **New persisted entity/table/artifact?**: no.
|
||||
- **New abstraction?**: possibly a narrow enum case or registry section if existing `WorkspaceHubRegistry` cannot represent workspace-owned analysis surfaces without falsely making them workspace hubs.
|
||||
- **New enum/state/reason family?**: no business state. A technical surface-scope enum case is allowed only if it replaces ambiguous generic `WorkspaceScoped` behavior.
|
||||
- **New cross-domain UI framework/taxonomy?**: no. This uses the existing shell/surface classification vocabulary.
|
||||
- **Current operator problem**: Clean workspace-owned analysis URLs can show selected Environment shell, making page ownership and data scope misleading.
|
||||
- **Existing structure is insufficient because**: Generic `AdminSurfaceScope::WorkspaceScoped` allows remembered Environment restore, while `WorkspaceHubRegistry` only covers first-class hubs and filtered hub behavior.
|
||||
- **Narrowest correct implementation**: Add explicit classifier coverage for the audited workspace-owned analysis paths and make those paths force environmentless shell; use `environment_id` only where the page already supports a visible filter.
|
||||
- **Ownership cost**: A small set of classifier/link/copy tests plus focused browser smoke. No schema, package, or product workflow cost.
|
||||
- **Alternative intentionally rejected**: Add the pages blindly to `WorkspaceHubRegistry` if that would imply hub/filter contracts they do not support, or keep remembered fallback because it is convenient.
|
||||
- **Release truth**: Current-release cleanup before production.
|
||||
|
||||
### Compatibility posture
|
||||
|
||||
This feature assumes pre-production hard cutover.
|
||||
|
||||
Backward compatibility, legacy aliases, remembered Environment fallback, redirect shims, migration shims, dual route models, and compatibility-specific tests are out of scope.
|
||||
|
||||
## Testing / Lane / Runtime Impact *(mandatory for runtime behavior changes)*
|
||||
|
||||
- **Test purpose / classification**: Unit for classifier/registry behavior; Feature/Livewire for page access, shell state, URL/query/filter behavior, and existing action regression; Browser for integrated shell/sidebar/reload/back-forward verification.
|
||||
- **Validation lane(s)**: fast-feedback, confidence, browser.
|
||||
- **Why this classification and these lanes are sufficient**: The defect is visible shell/context drift caused by classifier and navigation behavior; unit/feature tests prove the contract and browser smoke proves integrated UI state.
|
||||
- **New or expanded test families**: Focused `global-context-shell` coverage for workspace-owned analysis pages. Browser additions remain explicit and bounded to Spec 320 flows.
|
||||
- **Fixture / helper cost impact**: Reuse existing workspace/environment/member/baseline/finding fixtures. Do not introduce global expensive setup defaults.
|
||||
- **Heavy-family visibility / justification**: Browser coverage is required because reload/back-forward/sidebar shell mismatch was found by browser audit. It must be named as Spec 320 browser smoke, not hidden inside fast lanes.
|
||||
- **Special surface test profile**: `global-context-shell`.
|
||||
- **Standard-native relief or required special coverage**: Existing native Filament pages keep normal UI shape; special coverage is shell/context and filter visibility only.
|
||||
- **Reviewer handoff**: Verify in-scope clean URLs show Workspace shell only, explicit filtered URLs show visible chip where supported, legacy aliases do not create shell/filter state, and Baseline Compare remains Environment-owned.
|
||||
- **Budget / baseline / trend impact**: none expected; if browser runtime grows materially, record in the active PR close-out.
|
||||
- **Escalation needed**: document-in-feature.
|
||||
- **Active feature PR close-out entry**: Guardrail / Exception / Smoke Coverage.
|
||||
- **Planned validation commands**:
|
||||
- `cd apps/platform && ./vendor/bin/sail artisan test --filter=AdminSurfaceScope`
|
||||
- `cd apps/platform && ./vendor/bin/sail artisan test --filter=WorkspaceHubRegistry`
|
||||
- `cd apps/platform && ./vendor/bin/sail artisan test --filter=WorkspaceOwnedAnalysis`
|
||||
- `cd apps/platform && ./vendor/bin/sail artisan test --filter=BaselineCompareEnvironmentRouteContract`
|
||||
- `cd apps/platform && ./vendor/bin/sail artisan test --filter=WorkspaceHubEnvironmentFilterContract`
|
||||
- focused Spec 320 browser smoke for Baselines, Baseline Snapshots, Baseline Compare Matrix, My Findings/Intake/Hygiene, Cross-environment Compare, Baseline Compare regression, and Decision Register regression.
|
||||
|
||||
## User Scenarios & Testing *(mandatory)*
|
||||
|
||||
### User Story 1 - Open Baseline Library Without Environment Shell (Priority: P1)
|
||||
|
||||
As a workspace operator, I can open Baselines and Baseline Snapshots from workspace or environment navigation and see Workspace shell only, so reusable baseline libraries do not appear owned by the last Environment I visited.
|
||||
|
||||
**Why this priority**: Baseline library ownership is the main known mismatch and directly affects governance trust.
|
||||
|
||||
**Independent Test**: Start from an active Environment context, open clean Baselines and Baseline Snapshots URLs, and assert the shell has Workspace only with no active Environment.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** a remembered Environment exists, **When** the operator opens `/admin/baseline-profiles`, **Then** the shell shows Workspace only and no visible active Environment.
|
||||
2. **Given** the operator navigates from Environment Dashboard sidebar to Baseline Snapshots, **When** `/admin/baseline-snapshots` opens, **Then** it is a clean workspace-owned page with no hidden Environment shell.
|
||||
3. **Given** a valid baseline profile record, **When** view/edit/compare-matrix pages open by clean workspace URL, **Then** remembered Environment state does not become shell context.
|
||||
|
||||
---
|
||||
|
||||
### User Story 2 - Workspace Analysis Pages Do Not Inherit Environment Context (Priority: P1)
|
||||
|
||||
As a workspace operator, I can open My Findings, Findings Intake, Findings Hygiene, and Cross-environment Compare without the previous Environment owning the shell, so workspace analysis/work queues stay scope-honest.
|
||||
|
||||
**Why this priority**: Spec 318 found these unregistered workspace analysis pages inherit remembered Environment shell because they are not registered or classified.
|
||||
|
||||
**Independent Test**: With remembered Environment state present, open each clean URL and assert Workspace shell only; if `environment_id` is supported, assert it is a visible filter, not shell ownership.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** an active Environment context, **When** the operator opens `/admin/findings/my-work`, `/admin/findings/intake`, or `/admin/findings/hygiene`, **Then** shell context remains Workspace only.
|
||||
2. **Given** a valid `environment_id` filter is supported for a findings analysis page, **When** the page opens with `?environment_id=...`, **Then** shell remains Workspace only and a visible Environment filter chip/clear action exists.
|
||||
3. **Given** the operator opens `/admin/cross-environment-compare`, **When** source/target selectors are empty or query-hydrated, **Then** shell is Workspace only and selected source/target environments are page state.
|
||||
|
||||
---
|
||||
|
||||
### User Story 3 - Legacy Query Aliases Cannot Recreate Hidden Environment Shell (Priority: P1)
|
||||
|
||||
As a security reviewer, I need legacy query aliases and Filament table filter query payloads to be ignored or stripped on workspace-owned analysis surfaces, so old Tenant/Environment parameters cannot silently resurrect shell context.
|
||||
|
||||
**Why this priority**: Specs 314-317 intentionally removed legacy Tenant context and hidden filter state.
|
||||
|
||||
**Independent Test**: Open each in-scope page with `tenant`, `tenant_id`, `managed_environment_id`, `tenant_scope`, and `tableFilters` payloads and assert no active Environment shell or hidden filter appears.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** a clean workspace-owned analysis page, **When** the URL includes `tenant`, `tenant_id`, `managed_environment_id`, `tenant_scope`, or `tableFilters`, **Then** those aliases do not set shell Environment or filter state.
|
||||
2. **Given** the page supports canonical `environment_id`, **When** invalid or cross-workspace `environment_id` is supplied, **Then** it is rejected/ignored without shell context or leakage.
|
||||
|
||||
---
|
||||
|
||||
### User Story 4 - Environment-Owned and Workspace Hub Regressions Remain Green (Priority: P2)
|
||||
|
||||
As a reviewer, I can verify Baseline Compare remains Environment-owned and Decision Register remains a filtered workspace hub, so Spec 320 does not undo Specs 314-319.
|
||||
|
||||
**Why this priority**: The fix targets the opposite side of Spec 319 and must not flatten Environment-owned pages into workspace pages.
|
||||
|
||||
**Independent Test**: Assert canonical Baseline Compare route shows Workspace + Environment shell, old workspace-style Baseline Compare URLs still do not render, and Decision Register clean/filtered contracts remain green.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** a canonical Baseline Compare Environment route, **When** the page opens, **Then** Workspace + active Environment shell remains present.
|
||||
2. **Given** `/admin/governance/decisions`, **When** opened clean or with `?environment_id=...`, **Then** it behaves exactly as Specs 314-316 require.
|
||||
|
||||
### Edge Cases
|
||||
|
||||
- A workspace-owned analysis page has no Environment selected or remembered.
|
||||
- A remembered Environment belongs to the current Workspace.
|
||||
- A remembered Environment belongs to a different Workspace.
|
||||
- A valid `environment_id` is supplied on a page that supports explicit filtering.
|
||||
- A valid `environment_id` is supplied on a page that does not support explicit filtering.
|
||||
- Legacy query aliases are supplied individually and together.
|
||||
- Browser reload/back/forward crosses Environment Dashboard, workspace-owned analysis pages, Baseline Compare, and Decision Register.
|
||||
- Livewire update requests use referer-based surface classification and must not reintroduce remembered Environment shell.
|
||||
|
||||
## Functional Requirements
|
||||
|
||||
- **FR-001**: Baselines/Baseline Profiles MUST be explicitly classified as workspace-owned analysis/library surfaces unless implementation evidence proves a narrower Environment-owned split is required.
|
||||
- **FR-002**: Baseline Snapshots MUST be explicitly classified as workspace-owned analysis/library surfaces unless implementation evidence proves a narrower Environment-owned split is required.
|
||||
- **FR-003**: Baseline Profile view/edit pages and Baseline Compare Matrix MUST be classified so clean URLs force Workspace shell only.
|
||||
- **FR-004**: My Findings, Findings Intake, Findings Hygiene, Cross-environment Compare, and any other Spec 318 unregistered workspace analysis pages confirmed in repo inspection MUST be registered/classified or explicitly excluded with evidence.
|
||||
- **FR-005**: In-scope clean workspace-owned analysis URLs MUST NOT restore remembered Environment shell context.
|
||||
- **FR-006**: In-scope clean URLs MUST open without requiring an active Environment context.
|
||||
- **FR-007**: Workspace context MUST remain selected and valid; the fix MUST NOT clear Workspace state.
|
||||
- **FR-008**: If a page supports Environment filtering, the only public filter key MUST be `environment_id`.
|
||||
- **FR-009**: `environment_id` filtering MUST keep Workspace shell only and show a visible Environment filter chip or equivalent clear affordance.
|
||||
- **FR-010**: `tenant`, `tenant_id`, `managed_environment_id`, `tenant_scope`, and `tableFilters` MUST NOT create active Environment shell or hidden Environment filter state on in-scope pages.
|
||||
- **FR-011**: Cross-workspace or unauthorized `environment_id` MUST NOT leak Environment identity or become shell context.
|
||||
- **FR-012**: Sidebar/global/workspace navigation entries for in-scope pages MUST emit clean workspace URLs unless they intentionally emit canonical `environment_id` filter URLs with visible chips.
|
||||
- **FR-013**: Environment Dashboard links to workspace-owned analysis pages MUST use clean workspace URLs or explicit `environment_id` filter URLs; they MUST NOT carry active Environment shell ownership.
|
||||
- **FR-014**: Breadcrumb/header/copy for workspace-owned analysis pages MUST not imply active Environment ownership as the primary scope.
|
||||
- **FR-015**: Workspace-owned analysis pages MAY show Environment columns, badges, selectors, and filter labels as data/filter state.
|
||||
- **FR-016**: Baseline Compare MUST remain Environment-owned according to Spec 319.
|
||||
- **FR-017**: Decision Register, Governance Inbox, Operations, Finding Exceptions Queue, Provider Connections, Evidence Overview, Review Register, Customer Review Workspace, Workspace Settings, Manage Workspaces, and Managed Environments MUST retain their existing workspace hub contracts.
|
||||
- **FR-018**: Alerts/Audit Log `environment_id` behavior MUST NOT be decided or changed except as regression context; that belongs to Spec 321.
|
||||
- **FR-019**: No compatibility route, redirect shim, legacy alias support, migration, seeder, package, env var, queue, scheduler, storage, or deployment asset change may be introduced for this spec.
|
||||
- **FR-020**: Browser verification screenshots SHOULD be saved under `specs/320-workspace-owned-analysis-surface-registration-shell-cutover/artifacts/screenshots/` when generated.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
### Surface Classification
|
||||
|
||||
- [x] Baselines/Baseline Profiles classified as workspace-owned or explicitly reclassified with evidence.
|
||||
- [x] Baseline Snapshots classified as workspace-owned or explicitly reclassified with evidence.
|
||||
- [x] Baseline Profile detail/edit/compare-matrix classified as workspace-owned analysis surfaces.
|
||||
- [x] My Findings, Findings Intake, Findings Hygiene, and Cross-environment Compare classified or explicitly excluded with evidence.
|
||||
- [x] No in-scope workspace-owned analysis page remains generic ambiguous `WorkspaceScoped` if that permits remembered Environment shell fallback.
|
||||
- [x] Baseline Compare remains Environment-owned.
|
||||
|
||||
### Shell Context
|
||||
|
||||
- [x] Baselines clean URL shows Workspace shell only.
|
||||
- [x] Baseline Snapshots clean URL shows Workspace shell only.
|
||||
- [x] Baseline Profile detail/edit/compare-matrix clean URLs show Workspace shell only.
|
||||
- [x] Findings analysis and Cross-environment Compare clean URLs show Workspace shell only.
|
||||
- [x] Opening in-scope pages from Environment context cuts to Workspace shell only.
|
||||
- [x] Reload and browser back/forward do not restore active Environment shell.
|
||||
|
||||
### URL / Query Contract
|
||||
|
||||
- [x] In-scope clean URLs do not require Environment context.
|
||||
- [x] Legacy query aliases do not create shell/filter state.
|
||||
- [x] Supported Environment filters use only `environment_id`.
|
||||
- [x] Visible filter chip/clear behavior exists for pages that support `environment_id`.
|
||||
- [x] Unsupported `environment_id` is ignored/stripped/rejected without hidden shell/data mismatch; chosen behavior is documented in implementation close-out.
|
||||
|
||||
### Regression
|
||||
|
||||
- [x] Spec 314 clean workspace hub entry remains green.
|
||||
- [x] Spec 315 Environment CTA `environment_id` contract remains green.
|
||||
- [x] Spec 316 clear filter contract remains green.
|
||||
- [x] Spec 317 legacy Tenant cleanup remains green.
|
||||
- [x] Spec 319 Baseline Compare Environment-owned route remains green.
|
||||
- [x] Spec 318 workspace-owned analysis mismatch is resolved for in-scope surfaces.
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- Baseline Compare implementation changes beyond regression checks.
|
||||
- Alerts/Audit Log filter decision from Spec 321.
|
||||
- Durable browser no-drift automation from Spec 322.
|
||||
- New product features, new baseline assignment semantics, or baseline data model redesign.
|
||||
- Migrations, seeders, data backfills, package changes, env vars, queues, scheduler, storage, or deployment asset changes.
|
||||
- Legacy alias support, backwards compatibility, redirects, dual shell models, or remembered Environment fallback preservation.
|
||||
|
||||
## Source Evidence
|
||||
|
||||
- `specs/318-admin-surface-scope-shell-context-audit/audit-report.md` identifies the classifier gap and workspace-owned baseline/analysis mismatches.
|
||||
- `specs/318-admin-surface-scope-shell-context-audit/surface-inventory.md` lists Baselines/Baseline Profiles, Baseline Snapshots, Baseline Compare Matrix, My Findings, Findings Intake, Findings Hygiene, and Cross-environment Compare as mismatches or unregistered workspace analysis pages.
|
||||
- `specs/318-admin-surface-scope-shell-context-audit/mismatch-findings.md` records M2 and M4 as the direct targets.
|
||||
- `specs/318-admin-surface-scope-shell-context-audit/recommended-fixes.md` recommends a Workspace-Owned Baseline Registry Contract and broader classifier coverage.
|
||||
- `specs/319-environment-owned-surface-routing-shell-context-contract/spec.md` leaves workspace-owned baseline analysis surfaces explicitly to Spec 320.
|
||||
|
||||
## Assumptions
|
||||
|
||||
- Baseline Profiles and Baseline Snapshots are workspace-owned based on current `workspace_id` queries and Spec 318 evidence.
|
||||
- Baseline Compare Matrix is workspace-owned analysis over assigned Environments, while Baseline Compare landing remains Environment-owned.
|
||||
- Findings analysis pages are workspace-owned queues/reports with optional Environment filters, not Environment-owned pages.
|
||||
- Cross-environment Compare is workspace-owned portfolio analysis because it explicitly selects source/target Environments.
|
||||
- Current product is pre-production; hard cutover is correct.
|
||||
|
||||
## Risks
|
||||
|
||||
- Adding all in-scope pages to `WorkspaceHubRegistry` could accidentally imply filtered-hub behavior for pages that do not support chips/clear state.
|
||||
- A new classifier enum case could become a broader taxonomy if not constrained to the audited route list.
|
||||
- Existing tests may assert remembered Environment fallback and must be replaced, not preserved.
|
||||
- Browser smoke can become too broad; keep it focused to Spec 318 evidence and required regressions.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- Exact unsupported `environment_id` behavior for pages without filter support: ignore, strip, or reject. Implementation must choose per page and document the result.
|
||||
- Whether Baseline Profile create page should be included in workspace-owned analysis classification if implementation finds it inherits Environment shell.
|
||||
- Whether findings analysis pages should converge on canonical `environment_id` visible chip behavior in this spec or only prevent shell inheritance; if current filter UI is not chip-compatible, keep the fix narrow and document follow-up rather than adding feature scope.
|
||||
|
||||
## Follow-up Spec Candidates
|
||||
|
||||
- **321 - Alerts / Audit Log Environment Filter Contract Decision**: decide support vs rejection of `environment_id` with no half-state.
|
||||
- **322 - Browser No-Drift Regression Guard**: durable browser coverage for shell/context/filter/reload/back-forward drift.
|
||||
@ -0,0 +1,151 @@
|
||||
# Tasks: Workspace-Owned Analysis Surface Registration & Shell Cutover
|
||||
|
||||
**Input**: Design documents from `/specs/320-workspace-owned-analysis-surface-registration-shell-cutover/`
|
||||
**Prerequisites**: `plan.md`, `spec.md`
|
||||
**Tests**: Required. This is a runtime route/shell/query/navigation contract change.
|
||||
|
||||
## Test Governance Checklist
|
||||
|
||||
- [x] Lane assignment is named and is the narrowest sufficient proof for classifier, shell, query, reload/history, and regression behavior.
|
||||
- [x] New or changed tests stay in the smallest honest family; browser additions are 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` is explicit.
|
||||
- [x] Any material budget, baseline, trend, or escalation note is recorded in the implementation close-out.
|
||||
|
||||
## Phase 1: Guardrails and Repo Verification
|
||||
|
||||
**Purpose**: Confirm current repo truth before runtime edits.
|
||||
|
||||
- [x] T001 Verify implementation starts from branch `320-workspace-owned-analysis-surface-registration-shell-cutover` and record any unrelated uncommitted files.
|
||||
- [x] T002 Re-read `specs/318-admin-surface-scope-shell-context-audit/audit-report.md`, `surface-inventory.md`, `page-matrix.md`, `mismatch-findings.md`, and `recommended-fixes.md`.
|
||||
- [x] T003 Re-read `specs/319-environment-owned-surface-routing-shell-context-contract/spec.md`, `plan.md`, and `tasks.md` as dependency context only.
|
||||
- [x] T004 Confirm Laravel/Filament/Livewire/Pest versions through Laravel Boost `application_info`.
|
||||
- [x] T005 Confirm no migration, seeder, package, env var, queue, scheduler, storage, or deployment asset change is required.
|
||||
- [x] T006 Inventory current classifier behavior in `apps/platform/app/Support/Navigation/AdminSurfaceScope.php`.
|
||||
- [x] T007 Inventory current workspace hub behavior in `apps/platform/app/Support/Navigation/WorkspaceHubRegistry.php` and `WorkspaceSidebarNavigation.php`.
|
||||
- [x] T008 Inventory shell resolution in `apps/platform/app/Support/OperateHub/OperateHubShell.php`, including remembered Environment and query hint behavior.
|
||||
- [x] T009 Inventory in-scope baseline routes/pages in `BaselineProfileResource`, `BaselineSnapshotResource`, and `BaselineCompareMatrix`.
|
||||
- [x] T010 Inventory in-scope workspace analysis routes/pages in `MyFindingsInbox`, `FindingsIntakeQueue`, `FindingsHygieneReport`, and `CrossEnvironmentComparePage`.
|
||||
- [x] T011 Search routes/navigation for any additional Spec 318 unregistered workspace analysis page still present and decide include/exclude with evidence.
|
||||
- [x] T012 Identify any existing tests asserting remembered Environment fallback on in-scope pages and mark them for replacement.
|
||||
|
||||
## Phase 2: Tests First / Contract Coverage
|
||||
|
||||
**Purpose**: Add failing or alongside tests that define the new contract.
|
||||
|
||||
- [x] T013 Add/update `apps/platform/tests/Unit/Tenants/AdminSurfaceScopeTest.php` proving Baselines/Baseline Profiles paths are workspace-owned analysis or environmentless shell paths.
|
||||
- [x] T014 Add/update `apps/platform/tests/Unit/Tenants/AdminSurfaceScopeTest.php` proving Baseline Snapshots paths are workspace-owned analysis or environmentless shell paths.
|
||||
- [x] T015 Add/update `apps/platform/tests/Unit/Tenants/AdminSurfaceScopeTest.php` proving Baseline Profile detail/edit/compare-matrix paths do not allow remembered Environment restore.
|
||||
- [x] T016 Add/update `apps/platform/tests/Unit/Tenants/AdminSurfaceScopeTest.php` proving My Findings, Findings Intake, Findings Hygiene, and Cross-environment Compare paths do not allow remembered Environment restore.
|
||||
- [x] T017 Add/update `apps/platform/tests/Unit/Support/OperateHub/OperateHubShellResolutionTest.php` proving workspace-owned analysis clean URLs show Workspace shell only when a remembered Environment exists.
|
||||
- [x] T018 Add/update `apps/platform/tests/Feature/Navigation/WorkspaceHubRegistryTest.php` proving in-scope pages are not accidentally treated as full workspace hubs unless implementation intentionally registers them with hub behavior.
|
||||
- [x] T019 Add/update tests proving clean Baselines and Baseline Snapshots URLs open without active Environment context.
|
||||
- [x] T020 Add/update tests proving Baseline Profile view/edit/compare-matrix URLs open with Workspace shell only.
|
||||
- [x] T021 Add/update tests proving My Findings, Findings Intake, Findings Hygiene, and Cross-environment Compare clean URLs open with Workspace shell only.
|
||||
- [x] T022 Add/update tests proving remembered Environment state alone does not set shell context on in-scope pages.
|
||||
- [x] T023 Add/update tests proving `tenant`, `tenant_id`, `managed_environment_id`, `tenant_scope`, and `tableFilters` do not create shell or filter state on in-scope pages.
|
||||
- [x] T024 For pages that support canonical `environment_id`, add/update tests proving shell remains Workspace only and a visible chip/clear affordance exists.
|
||||
- [x] T025 For pages that do not support canonical `environment_id`, add/update tests proving unsupported `environment_id` is ignored, stripped, or rejected without hidden shell/data mismatch.
|
||||
- [x] T026 Add/update tests proving Environment Dashboard or Environment-origin links to in-scope workspace-owned analysis pages do not carry active Environment shell ownership.
|
||||
- [x] T027 Keep/add Baseline Compare regression coverage proving its canonical route remains Environment-owned.
|
||||
- [x] T028 Keep/add Decision Register regression coverage proving clean and filtered workspace hub behavior remains green.
|
||||
- [x] T029 Keep/add Specs 314-317 regression coverage for clean workspace hub entry, Environment CTA `environment_id`, clear filter, and no legacy Tenant aliases.
|
||||
- [x] T030 Add/update existing high-impact baseline action tests only as needed to prove capture/compare actions still keep confirmation, authorization, audit, and OperationRun UX after shell classification changes.
|
||||
|
||||
## Phase 3: Classification and Shell Cutover
|
||||
|
||||
**Purpose**: Implement the narrowest route/shell classification fix.
|
||||
|
||||
- [x] T031 Update `apps/platform/app/Support/Navigation/AdminSurfaceScope.php` to classify in-scope workspace-owned analysis paths explicitly.
|
||||
- [x] T032 Ensure the chosen classification forces environmentless shell context for clean workspace-owned analysis URLs.
|
||||
- [x] T033 Ensure the chosen classification does not allow remembered Environment restore.
|
||||
- [x] T034 Ensure Livewire referer-based classification uses the same in-scope path behavior.
|
||||
- [x] T035 If adding a new `AdminSurfaceScope` enum case, complete the proportionality note in implementation close-out and avoid using it outside audited routes.
|
||||
- [x] T036 If registering any in-scope page in `WorkspaceHubRegistry`, verify it truly satisfies workspace hub/filter/clear semantics and add matching registry tests.
|
||||
- [x] T037 Keep `WorkspaceHubRegistry::forbiddenQueryKeys()` and related query-cleaning behavior aligned with no legacy aliases.
|
||||
- [x] T038 Do not add Baseline Compare to workspace hub or workspace-owned analysis classification.
|
||||
- [x] T039 Do not alter Environment-bound route classification for required permissions, inventory, backups, evidence, reviews, stored reports, review packs, or other Spec 319 out-of-scope Environment pages.
|
||||
|
||||
## Phase 4: Baseline Surfaces
|
||||
|
||||
**Purpose**: Align baseline library/report pages with workspace-owned shell semantics.
|
||||
|
||||
- [x] T040 Update `apps/platform/app/Filament/Resources/BaselineProfileResource.php` only if needed so list/view/edit/create URLs and navigation do not rely on active Environment shell.
|
||||
- [x] T041 Update Baseline Profile breadcrumbs/header/copy if any primary wording implies active Environment ownership.
|
||||
- [x] T042 Update Baseline Profile related navigation links if they carry hidden Environment shell ownership or legacy query aliases.
|
||||
- [x] T043 Update `apps/platform/app/Filament/Pages/BaselineCompareMatrix.php` only if needed so the page shell is Workspace only while source Environment drilldowns remain explicit page links.
|
||||
- [x] T044 Preserve Baseline Compare Matrix filter query keys such as `tenant_sort` only as page matrix state, not shell Environment ownership.
|
||||
- [x] T045 Update `apps/platform/app/Filament/Resources/BaselineSnapshotResource.php` only if needed so list/view URLs and copy remain workspace snapshot library/report semantics.
|
||||
- [x] T046 Preserve Baseline Profile and Baseline Snapshot global search disabled status unless implementation deliberately verifies and tests safe View/Edit pages.
|
||||
- [x] T047 Preserve existing baseline archive/capture/compare action confirmation, authorization, notifications, audit, and OperationRun behavior.
|
||||
|
||||
## Phase 5: Findings and Portfolio Analysis Surfaces
|
||||
|
||||
**Purpose**: Align unregistered workspace analysis pages found by Spec 318.
|
||||
|
||||
- [x] T048 Update `apps/platform/app/Filament/Pages/Findings/MyFindingsInbox.php` only if needed so clean URL shell is Workspace only.
|
||||
- [x] T049 Update `apps/platform/app/Filament/Pages/Findings/FindingsIntakeQueue.php` only if needed so clean URL shell is Workspace only.
|
||||
- [x] T050 Update `apps/platform/app/Filament/Pages/Findings/FindingsHygieneReport.php` only if needed so clean URL shell is Workspace only.
|
||||
- [x] T051 Replace `tenant` query prefilter handling on findings analysis pages with canonical `environment_id` if this is already intended product behavior and can show visible filter/clear affordance.
|
||||
- [x] T052 If findings analysis pages cannot safely migrate filter behavior in this slice, prevent shell inheritance and document filter-key follow-up rather than adding half-state support.
|
||||
- [x] T053 Update `apps/platform/app/Filament/Pages/CrossEnvironmentComparePage.php` only if needed so source/target Environment selectors remain page state and shell is Workspace only.
|
||||
- [x] T054 Ensure Cross-environment Compare direct URL, query-hydrated URL, and reload do not restore remembered Environment shell.
|
||||
- [x] T055 Preserve existing promotion/preflight authorization, confirmation, audit, OperationRun links, and provider-boundary behavior.
|
||||
|
||||
## Phase 6: Navigation, Links, Copy, and Legacy Aliases
|
||||
|
||||
**Purpose**: Make visible navigation agree with page ownership.
|
||||
|
||||
- [x] T056 Update `WorkspaceSidebarNavigation` or related navigation builders only if in-scope pages are present there and currently emit ambiguous URLs.
|
||||
- [x] T057 Update `ManagedEnvironmentLinks` only if Environment-origin cards/actions link to workspace-owned analysis pages with hidden shell ownership.
|
||||
- [x] T058 Ensure sidebar/global/workspace entry to Baselines, Baseline Snapshots, baseline matrix, findings analysis pages, and Cross-environment Compare uses clean workspace URLs unless canonical `environment_id` filter is intentionally supported.
|
||||
- [x] T059 Ensure Environment Dashboard links to in-scope workspace-owned analysis pages use clean workspace URLs or explicit `environment_id` filter URLs with visible chip/clear behavior.
|
||||
- [x] T060 Remove or replace user-facing copy that says `this environment`, `current environment`, or similar primary ownership wording on in-scope workspace-owned analysis pages.
|
||||
- [x] T061 Keep Environment columns, Environment badges, source/target selectors, and Environment filters where they are data/filter state rather than shell ownership.
|
||||
- [x] T062 Ensure no in-scope page starts accepting `tenant`, `tenant_id`, `managed_environment_id`, `tenant_scope`, or `tableFilters` as public Environment filter aliases.
|
||||
|
||||
## Phase 7: Browser Verification
|
||||
|
||||
**Purpose**: Prove visible route/shell/copy behavior.
|
||||
|
||||
- [x] T063 Start local platform stack using Sail or the repo platform dev command.
|
||||
- [x] T064 Browser Flow A: Workspace Overview -> Baselines; verify Workspace shell only, no active Environment, workspace/library wording.
|
||||
- [x] T065 Save Flow A screenshot to `specs/320-workspace-owned-analysis-surface-registration-shell-cutover/artifacts/screenshots/workspace-origin--baselines.png`.
|
||||
- [x] T066 Browser Flow B: Environment Dashboard -> Baselines through sidebar/global/navigation/card; verify shell cuts to Workspace only.
|
||||
- [x] T067 Save Flow B screenshot to `artifacts/screenshots/environment-origin--baselines.png`.
|
||||
- [x] T068 Browser Flow C: reload Baselines and verify active Environment shell does not return.
|
||||
- [x] T069 Save Flow C screenshot to `artifacts/screenshots/baselines--after-reload.png`.
|
||||
- [x] T070 Repeat workspace origin, environment origin, and reload flows for Baseline Snapshots.
|
||||
- [x] T071 Save Baseline Snapshots screenshots using `workspace-origin--baseline-snapshots.png`, `environment-origin--baseline-snapshots.png`, and `baseline-snapshots--after-reload.png`.
|
||||
- [x] T072 Browser verify Baseline Compare Matrix direct/reload/back-forward behavior if local fixture has a baseline profile.
|
||||
- [x] T073 Browser verify My Findings, Findings Intake, Findings Hygiene, and Cross-environment Compare clean URLs from remembered Environment context.
|
||||
- [x] T074 Browser verify Baseline Compare remains Environment-owned.
|
||||
- [x] T075 Save Baseline Compare regression screenshot to `artifacts/screenshots/baseline-compare--regression-environment-owned.png`.
|
||||
- [x] T076 Browser verify Decision Register clean and filtered workspace hub regressions.
|
||||
- [x] T077 Save Decision Register regression screenshot to `artifacts/screenshots/decision-register--regression-workspace-hub.png`.
|
||||
- [x] T078 If browser setup or fixture data blocks any flow, document the exact blocker and alternate proof in the implementation close-out.
|
||||
|
||||
> Browser coverage note: Matrix and Baseline Compare browser flows were partially blocked by local Spec 180 fixture/capability state. The local fixture has no baseline profile for a matrix browser flow, and the environment-owned Baseline Compare browser route is intentionally capability-denied. The environment-owned Baseline Compare contract and matrix route behavior are covered by focused Pest route/RBAC tests. No runtime gap remains in Spec 320 scope.
|
||||
|
||||
## Phase 8: Final Validation and Close-Out
|
||||
|
||||
**Purpose**: Complete implementation proof without broad rebaseline.
|
||||
|
||||
- [x] T079 Run `git diff --check`.
|
||||
- [x] T080 Run the focused Pest commands listed in `plan.md`.
|
||||
- [x] T081 Run formatting with the repo-standard Pint command for touched PHP files.
|
||||
- [x] T082 Review `git diff --stat` and confirm only in-scope runtime/test/spec artifacts changed.
|
||||
- [x] T083 Confirm no migrations, seeders, package files, env files, queue/scheduler/storage config, or deployment asset files changed.
|
||||
- [x] T084 Confirm no backwards compatibility layer, redirect shim, dual route model, or legacy query alias support was introduced.
|
||||
- [x] T085 Prepare final implementation report with changed behavior, workspace-owned analysis surfaces classified, surfaces registered, files changed, tests, browser verification, screenshots path, follow-ups 321/322, and any unrelated residual failures.
|
||||
- [x] T086 Include the Filament v5 output contract in the final report: Livewire v4.0+ compliance, provider registration location, global search status, destructive/high-impact actions, asset strategy, and testing plan/results.
|
||||
|
||||
## Explicit Non-Tasks
|
||||
|
||||
- [x] NT001 Do not implement Alerts/Audit Log filter behavior; leave it to Spec 321.
|
||||
- [x] NT002 Do not build durable browser no-drift infrastructure; leave it to Spec 322.
|
||||
- [x] NT003 Do not change Baseline Compare except regression coverage needed to prove Spec 319 remains intact.
|
||||
- [x] NT004 Do not add migrations, seeders, packages, env vars, queues, scheduler, storage, or deployment asset changes.
|
||||
- [x] NT005 Do not add legacy `tenant`, `tenant_id`, `managed_environment_id`, `tenant_scope`, or `tableFilters` aliases.
|
||||
- [x] NT006 Do not preserve remembered Environment fallback for workspace-owned analysis clean URLs.
|
||||