Complete Spec 287 prerequisite cutover #346

Merged
ahmido merged 1 commits from 287-cutover-quality-gates-no-legacy-enforcement into platform-dev 2026-05-10 18:22:36 +00:00
30 changed files with 1415 additions and 91 deletions

View File

@ -15,6 +15,8 @@
class TenantMembershipManager
{
private const string SCOPE_PLACEHOLDER_ROLE = 'readonly';
public function __construct(
public AuditLogger $auditLogger,
private readonly WorkspaceCapabilityResolver $workspaceCapabilityResolver,
@ -57,7 +59,7 @@ public function grantScope(
if ($existing) {
$existing->forceFill([
'role' => $memberWorkspaceRole,
'role' => self::SCOPE_PLACEHOLDER_ROLE,
'source' => $source,
'source_ref' => $sourceRef,
'created_by_user_id' => (int) $actor->getKey(),
@ -69,7 +71,7 @@ public function grantScope(
$membership = ManagedEnvironmentMembership::query()->create([
'managed_environment_id' => (int) $tenant->getKey(),
'user_id' => (int) $member->getKey(),
'role' => $memberWorkspaceRole,
'role' => self::SCOPE_PLACEHOLDER_ROLE,
'source' => $source,
'source_ref' => $sourceRef,
'created_by_user_id' => (int) $actor->getKey(),

View File

@ -22,11 +22,13 @@ public function __construct(
public function resolve(ProviderConnection $connection): ProviderIdentityResolution
{
$targetScopeIdentifier = trim((string) $connection->entra_tenant_id);
$connectionType = $this->resolveConnectionType($connection);
$targetScopeResult = $this->targetScopeNormalizer->normalizeConnection($connection);
$targetScope = $targetScopeResult['target_scope'] ?? null;
$providerContextDetails = $this->targetScopeNormalizer->contextualIdentityDetailsForConnection($connection);
$targetScopeIdentifier = $targetScope instanceof ProviderConnectionTargetScopeDescriptor
? trim((string) $targetScope->scopeIdentifier)
: '';
if ($connectionType === null) {
return ProviderIdentityResolution::blocked(
@ -117,7 +119,7 @@ private function resolveDedicatedIdentity(
$targetScope = ProviderConnectionTargetScopeDescriptor::fromInput(
provider: 'microsoft',
scopeKind: ProviderConnectionTargetScopeDescriptor::SCOPE_KIND_TENANT,
scopeIdentifier: $targetScopeIdentifier !== '' ? $targetScopeIdentifier : 'organizations',
scopeIdentifier: $targetScopeIdentifier !== '' ? $targetScopeIdentifier : $connection->tenant?->providerTenantContext() ?? 'organizations',
);
}

View File

@ -352,15 +352,18 @@ private function targetScopeContextForConnection(ProviderConnection $connection)
try {
return $this->targetScopeNormalizer->descriptorForConnection($connection)->toArray();
} catch (InvalidArgumentException) {
$identifier = trim((string) $connection->entra_tenant_id);
$fallbackIdentifier = $connection->tenant instanceof ManagedEnvironment
? trim((string) $connection->tenant->graphTenantId())
$identifier = $connection->tenant instanceof ManagedEnvironment
? trim($connection->tenant->providerTenantContext())
: '';
if ($identifier === '') {
$identifier = trim((string) $connection->entra_tenant_id);
}
return ProviderConnectionTargetScopeDescriptor::fromInput(
provider: (string) $connection->provider,
scopeKind: ProviderConnectionTargetScopeDescriptor::SCOPE_KIND_TENANT,
scopeIdentifier: $identifier !== '' ? $identifier : ($fallbackIdentifier !== '' ? $fallbackIdentifier : (string) $connection->getKey()),
scopeIdentifier: $identifier !== '' ? $identifier : (string) $connection->getKey(),
scopeDisplayName: (string) ($connection->tenant?->name ?? $connection->display_name ?? $identifier),
)->toArray();
}

View File

@ -25,12 +25,17 @@ public static function fromConnection(ProviderConnection $connection): self
$tenantName = is_string($connection->tenant?->name) && trim($connection->tenant->name) !== ''
? trim($connection->tenant->name)
: trim((string) $connection->display_name);
$scopeIdentifier = trim((string) $connection->entra_tenant_id);
if ($scopeIdentifier === '') {
$scopeIdentifier = trim((string) $connection->tenant?->providerTenantContext());
}
return new self(
provider: trim((string) $connection->provider),
scopeKind: self::SCOPE_KIND_TENANT,
scopeIdentifier: trim((string) $connection->entra_tenant_id),
scopeDisplayName: $tenantName !== '' ? $tenantName : trim((string) $connection->entra_tenant_id),
scopeIdentifier: $scopeIdentifier,
scopeDisplayName: $tenantName !== '' ? $tenantName : $scopeIdentifier,
);
}

View File

@ -96,13 +96,16 @@ public function normalizeInput(
*/
public function normalizeConnection(ProviderConnection $connection): array
{
$descriptor = ProviderConnectionTargetScopeDescriptor::fromConnection($connection);
$connectionTenantId = trim((string) $connection->entra_tenant_id);
return $this->normalizeInput(
provider: trim((string) $connection->provider),
scopeKind: ProviderConnectionTargetScopeDescriptor::SCOPE_KIND_TENANT,
scopeIdentifier: trim((string) $connection->entra_tenant_id),
scopeDisplayName: $connection->tenant?->name ?? $connection->display_name,
scopeIdentifier: $descriptor->scopeIdentifier,
scopeDisplayName: $descriptor->scopeDisplayName,
providerSpecificIdentity: [
'microsoft_tenant_id' => trim((string) $connection->entra_tenant_id),
'microsoft_tenant_id' => $connectionTenantId !== '' ? $connectionTenantId : $descriptor->scopeIdentifier,
],
);
}
@ -124,11 +127,14 @@ public function descriptorForConnection(ProviderConnection $connection): Provide
*/
public function contextualIdentityDetailsForConnection(ProviderConnection $connection): array
{
$descriptor = ProviderConnectionTargetScopeDescriptor::fromConnection($connection);
$connectionTenantId = trim((string) $connection->entra_tenant_id);
return $this->contextualIdentityDetails(
provider: trim((string) $connection->provider),
scopeIdentifier: trim((string) $connection->entra_tenant_id),
scopeIdentifier: $descriptor->scopeIdentifier,
providerSpecificIdentity: [
'microsoft_tenant_id' => trim((string) $connection->entra_tenant_id),
'microsoft_tenant_id' => $connectionTenantId !== '' ? $connectionTenantId : $descriptor->scopeIdentifier,
],
);
}

View File

@ -142,7 +142,7 @@ private function isExternalUrl(string $url): bool
private function isInternalDiagnosticPath(string $path): bool
{
return (bool) preg_match(
'/^\/admin\/(?:workspaces\/[^\/]+\/environments\/[^\/]+\/required-permissions|tenants\/[^\/]+\/provider-connections(?:\/create|\/[^\/]+(?:\/edit)?)?|provider-connections(?:\/create|\/[^\/]+(?:\/edit)?)?)$/',
'/^\/admin\/(?:workspaces\/[^\/]+\/environments\/[^\/]+\/required-permissions|provider-connections(?:\/create|\/[^\/]+(?:\/edit)?)?)$/',
$path,
);
}

View File

@ -450,34 +450,8 @@
FilamentAuthenticate::class,
'ensure-workspace-selected',
])
->prefix('/admin/tenants/{tenant:slug}/provider-connections')
->group(function () use ($authorizeManagedTenantRoute): void {
Route::get('/', function (ManagedEnvironment $tenant, Request $request) use ($authorizeManagedTenantRoute) {
$authorizeManagedTenantRoute($tenant, $request);
return redirect()->to('/admin/provider-connections?managed_environment_id='.$tenant->external_id);
})->name('admin.provider-connections.legacy-index');
Route::get('/create', function (ManagedEnvironment $tenant, Request $request) use ($authorizeManagedTenantRoute) {
$authorizeManagedTenantRoute($tenant, $request);
return redirect()->to('/admin/provider-connections/create?managed_environment_id='.$tenant->external_id);
})->name('admin.provider-connections.legacy-create');
Route::get('/{record}/edit', function (ManagedEnvironment $tenant, mixed $record, Request $request) use ($authorizeManagedTenantRoute) {
$authorizeManagedTenantRoute($tenant, $request);
$connection = ProviderConnection::query()
->whereKey((int) $record)
->where('managed_environment_id', (int) $tenant->getKey())
->where('workspace_id', (int) $tenant->workspace_id)
->first();
abort_unless($connection instanceof ProviderConnection, 404);
return redirect()->to('/admin/provider-connections/'.$connection->getKey().'/edit?managed_environment_id='.$tenant->external_id);
})->name('admin.provider-connections.legacy-edit');
});
->get('/admin/evidence/overview', \App\Filament\Pages\Monitoring\EvidenceOverview::class)
->name('admin.evidence.overview');
Route::middleware([
'web',

View File

@ -9,6 +9,7 @@
use App\Models\WorkspaceMembership;
use App\Services\Auth\CapabilityResolver;
use App\Services\Auth\ManagedEnvironmentAccessScopeResolver;
use App\Support\Auth\Capabilities;
use App\Support\Workspaces\WorkspaceContext;
use Filament\PanelRegistry;
@ -56,7 +57,7 @@
WorkspaceMembership::factory()->create([
'workspace_id' => (int) $workspace->getKey(),
'user_id' => (int) $user->getKey(),
'role' => 'operator',
'role' => 'manager',
]);
session()->put(WorkspaceContext::SESSION_KEY, (int) $workspace->getKey());
@ -64,7 +65,7 @@
ManagedEnvironmentMembership::query()->create([
'managed_environment_id' => (int) $allowedTenant->getKey(),
'user_id' => (int) $user->getKey(),
'role' => 'operator',
'role' => 'readonly',
'source' => 'manual',
]);
@ -75,10 +76,13 @@
/** @var \Filament\Panel $panel */
$panel = app(PanelRegistry::class)->get('admin');
$allowedDecision = app(ManagedEnvironmentAccessScopeResolver::class)->decision($user, $allowedTenant, Capabilities::WORKSPACE_MEMBERSHIP_MANAGE);
$defaultTenant = $user->getDefaultTenant($panel);
$tenants = $user->getTenants($panel);
expect($defaultTenant?->getKey())->toBe($allowedTenant->getKey())
->and($allowedDecision->workspaceRole)->toBe('manager')
->and($allowedDecision->capabilityAllowed)->toBeTrue()
->and(app(WorkspaceContext::class)->lastTenantId())->toBeNull()
->and($tenants)->toHaveCount(1)
->and($tenants->first()?->getKey())->toBe($allowedTenant->getKey());

View File

@ -31,7 +31,7 @@
WorkspaceMembership::factory()->create([
'workspace_id' => (int) $workspace->getKey(),
'user_id' => (int) $member->getKey(),
'role' => 'readonly',
'role' => 'operator',
]);
Livewire::actingAs($owner)

View File

@ -9,7 +9,7 @@
uses(RefreshDatabase::class);
it('keeps legacy provider routes bound to managed environments and scoped by membership', function (): void {
it('does not serve retired legacy provider routes for entitled members', function (): void {
[$user, $environment] = createUserWithTenant(role: 'owner', workspaceRole: 'manager');
$this->actingAs($user)->withSession([
@ -17,8 +17,9 @@
]);
$this
->get(route('admin.provider-connections.legacy-index', ['tenant' => $environment]))
->assertRedirect('/admin/provider-connections?managed_environment_id='.$environment->slug);
->get('/admin/tenants/'.$environment->slug.'/provider-connections')
->assertNotFound()
->assertHeaderMissing('Location');
});
it('hides managed-environment routes from workspace members without environment membership', function (): void {
@ -39,8 +40,9 @@
]);
$this
->get(route('admin.provider-connections.legacy-index', ['tenant' => $environment]))
->assertNotFound();
->get('/admin/tenants/'.$environment->slug.'/provider-connections')
->assertNotFound()
->assertHeaderMissing('Location');
});
it('hides managed-environment routes when the current workspace differs', function (): void {
@ -58,6 +60,7 @@
]);
$this
->get(route('admin.provider-connections.legacy-index', ['tenant' => $environment]))
->assertNotFound();
->get('/admin/tenants/'.$environment->slug.'/provider-connections')
->assertNotFound()
->assertHeaderMissing('Location');
});

View File

@ -38,8 +38,9 @@
]);
$this
->get(route('admin.provider-connections.legacy-index', ['tenant' => $environment]))
->assertNotFound();
->get('/admin/tenants/'.$environment->slug.'/provider-connections')
->assertNotFound()
->assertHeaderMissing('Location');
});
it('returns not found when a workspace member lacks managed-environment membership', function (): void {

View File

@ -31,7 +31,7 @@
run: $run,
reasonCode: ProviderReasonCodes::ProviderCredentialMissing,
nextSteps: [
['label' => 'Update Credentials', 'url' => '/admin/tenants/demo/provider-connections'],
['label' => 'Update Credentials', 'url' => '/admin/provider-connections?managed_environment_id=demo'],
['label' => '', 'url' => '/invalid'],
],
message: 'client_secret=super-secret',
@ -45,7 +45,7 @@
->and(data_get($finalized->context, 'reason_translation.operator_label'))->toBe('Credentials missing')
->and(data_get($finalized->context, 'reason_translation.short_explanation'))->toContain('credentials required to authenticate')
->and($finalized->context['next_steps'] ?? [])->toBe([
['label' => 'Update Credentials', 'url' => '/admin/tenants/demo/provider-connections'],
['label' => 'Update Credentials', 'url' => '/admin/provider-connections?managed_environment_id=demo'],
])
->and($finalized->failure_summary[0]['reason_code'] ?? null)->toBe(ProviderReasonCodes::ProviderCredentialMissing)
->and((string) ($finalized->failure_summary[0]['message'] ?? ''))->not->toContain('secret');

View File

@ -7,7 +7,7 @@
use App\Models\User;
use App\Support\Workspaces\WorkspaceContext;
it('redirects legacy tenant-scoped provider connection routes for entitled members', function (): void {
it('returns 404 without location header for retired tenant-scoped provider connection routes', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
$connection = ProviderConnection::factory()->create([
@ -18,27 +18,28 @@
$this->actingAs($user)
->get('/admin/tenants/'.$tenant->external_id.'/provider-connections')
->assertStatus(302)
->assertRedirect('/admin/provider-connections?managed_environment_id='.$tenant->external_id);
->assertNotFound()
->assertHeaderMissing('Location');
$this->actingAs($user)
->get('/admin/tenants/'.$tenant->external_id.'/provider-connections/create')
->assertStatus(302)
->assertRedirect('/admin/provider-connections/create?managed_environment_id='.$tenant->external_id);
->assertNotFound()
->assertHeaderMissing('Location');
$this->actingAs($user)
->get('/admin/tenants/'.$tenant->external_id.'/provider-connections/'.$connection->getKey().'/edit')
->assertStatus(302)
->assertRedirect('/admin/provider-connections/'.$connection->getKey().'/edit?managed_environment_id='.$tenant->external_id);
->assertNotFound()
->assertHeaderMissing('Location');
});
it('redirects non-workspace-members on legacy routes', function (): void {
it('returns 404 without location header for non-workspace-members on retired legacy routes', function (): void {
$user = User::factory()->create();
$tenant = ManagedEnvironment::factory()->create();
$this->actingAs($user)
->get('/admin/tenants/'.$tenant->external_id.'/provider-connections')
->assertRedirect();
->assertNotFound()
->assertHeaderMissing('Location');
});
it('returns 404 without location header for non-tenant members on legacy routes', function (): void {

View File

@ -3,6 +3,7 @@
declare(strict_types=1);
use App\Models\ProviderConnection;
use App\Models\ManagedEnvironment;
use App\Services\Providers\ProviderConnectionResolver;
use App\Support\Providers\TargetScope\ProviderConnectionSurfaceSummary;
use Illuminate\Foundation\Testing\RefreshDatabase;
@ -55,3 +56,43 @@
'detail_value' => '11111111-1111-1111-1111-111111111111',
]);
});
it('falls back to the managed environment provider scope when the connection tenant identifier is blank', function (): void {
config()->set('graph.client_id', 'platform-client-id');
config()->set('graph.client_secret', 'platform-client-secret');
$tenant = ManagedEnvironment::factory()->create([
'name' => 'Fallback scope environment',
'managed_environment_id' => '66666666-6666-6666-6666-666666666666',
]);
$connection = ProviderConnection::factory()
->platform()
->consentGranted()
->create([
'workspace_id' => (int) $tenant->workspace_id,
'managed_environment_id' => (int) $tenant->getKey(),
'provider' => 'microsoft',
'display_name' => 'Fallback scope connection',
'entra_tenant_id' => '',
'is_default' => true,
'is_enabled' => true,
'verification_status' => 'healthy',
]);
$resolution = app(ProviderConnectionResolver::class)
->validateConnection($tenant, 'microsoft', $connection->fresh(['tenant']));
$summaryPayload = ProviderConnectionSurfaceSummary::forConnection($connection->fresh(['tenant']))->toArray();
expect($resolution->resolved)->toBeTrue()
->and($resolution->targetScope?->scopeIdentifier)->toBe('66666666-6666-6666-6666-666666666666')
->and($summaryPayload['target_scope'] ?? [])->toMatchArray([
'scope_identifier' => '66666666-6666-6666-6666-666666666666',
'scope_display_name' => 'Fallback scope environment',
])
->and($summaryPayload['provider_context']['details'][0] ?? [])->toMatchArray([
'detail_key' => 'microsoft_tenant_id',
'detail_value' => '66666666-6666-6666-6666-666666666666',
])
->and($summaryPayload['target_scope'])->not->toHaveKey('entra_tenant_id');
});

View File

@ -4,6 +4,7 @@
use App\Models\ProviderConnection;
use App\Models\ProviderCredential;
use App\Models\ManagedEnvironment;
use App\Services\Providers\ProviderIdentityResolver;
use App\Support\Providers\ProviderConnectionType;
use Illuminate\Foundation\Testing\RefreshDatabase;
@ -64,3 +65,30 @@
))->toBeFalse()
->and($resolution->effectiveClientId)->toBe('dedicated-client-id');
});
it('resolves platform identity from the neutral target scope when the connection tenant identifier is blank', function (): void {
config()->set('graph.client_id', 'platform-client-id');
config()->set('graph.client_secret', 'platform-client-secret');
config()->set('graph.managed_environment_id', 'platform-home-tenant-id');
$tenant = ManagedEnvironment::factory()->create([
'managed_environment_id' => '77777777-7777-7777-7777-777777777777',
]);
$connection = ProviderConnection::factory()->platform()->create([
'workspace_id' => (int) $tenant->workspace_id,
'managed_environment_id' => (int) $tenant->getKey(),
'provider' => 'microsoft',
'entra_tenant_id' => '',
]);
$resolution = app(ProviderIdentityResolver::class)->resolve($connection->fresh(['tenant']));
$providerContextDetails = collect($resolution->providerContext()['details']);
expect($resolution->resolved)->toBeTrue()
->and($resolution->targetScopeIdentifier())->toBe('77777777-7777-7777-7777-777777777777')
->and($providerContextDetails->contains(
fn (array $detail): bool => ($detail['detail_key'] ?? null) === 'microsoft_tenant_id'
&& ($detail['detail_value'] ?? null) === '77777777-7777-7777-7777-777777777777',
))->toBeTrue();
});

View File

@ -91,3 +91,40 @@
&& ($pointer['value'] ?? null) === '55555555-5555-5555-5555-555555555555',
))->toBeTrue();
});
it('stores neutral target-scope context when the connection tenant identifier is blank', function (): void {
$tenant = ManagedEnvironment::factory()->create([
'name' => 'Fallback start environment',
'managed_environment_id' => '88888888-8888-8888-8888-888888888888',
]);
$connection = ProviderConnection::factory()->dedicated()->consentGranted()->create([
'workspace_id' => (int) $tenant->workspace_id,
'managed_environment_id' => (int) $tenant->getKey(),
'provider' => 'microsoft',
'entra_tenant_id' => '',
'consent_status' => 'granted',
]);
ProviderCredential::factory()->create([
'provider_connection_id' => (int) $connection->getKey(),
]);
$result = app(ProviderOperationStartGate::class)->start(
tenant: $tenant,
connection: $connection,
operationType: 'provider.connection.check',
dispatcher: fn (OperationRun $run): null => null,
);
$context = $result->run->fresh()->context;
expect($result->status)->toBe('started')
->and($context['target_scope'] ?? [])->toMatchArray([
'scope_identifier' => '88888888-8888-8888-8888-888888888888',
'scope_display_name' => 'Fallback start environment',
])
->and($context['provider_context']['details'][0] ?? [])->toMatchArray([
'detail_key' => 'microsoft_tenant_id',
'detail_value' => '88888888-8888-8888-8888-888888888888',
])
->and($context['target_scope'] ?? [])->not->toHaveKey('entra_tenant_id');
});

View File

@ -48,12 +48,12 @@
WorkspaceMembership::factory()->create([
'workspace_id' => (int) $workspace->getKey(),
'user_id' => (int) $user->getKey(),
'role' => 'operator',
'role' => 'manager',
]);
ManagedEnvironmentMembership::query()->create([
'managed_environment_id' => (int) $allowedTenant->getKey(),
'user_id' => (int) $user->getKey(),
'role' => 'operator',
'role' => 'readonly',
'source' => 'manual',
]);
app(ManagedEnvironmentAccessScopeResolver::class)->clearCache();
@ -71,6 +71,38 @@
->and($response->status())->toBe(404);
});
it('keeps workspace capability authority when explicit environment scope rows use the readonly placeholder', function (): void {
$workspace = Workspace::factory()->create();
$tenant = ManagedEnvironment::factory()->active()->create([
'workspace_id' => (int) $workspace->getKey(),
]);
$user = User::factory()->create();
WorkspaceMembership::factory()->create([
'workspace_id' => (int) $workspace->getKey(),
'user_id' => (int) $user->getKey(),
'role' => 'manager',
]);
ManagedEnvironmentMembership::query()->create([
'managed_environment_id' => (int) $tenant->getKey(),
'user_id' => (int) $user->getKey(),
'role' => 'readonly',
'source' => 'manual',
]);
app(ManagedEnvironmentAccessScopeResolver::class)->clearCache();
$connection = ProviderConnection::factory()->create([
'workspace_id' => (int) $workspace->getKey(),
'managed_environment_id' => (int) $tenant->getKey(),
]);
session()->put(WorkspaceContext::SESSION_KEY, (int) $workspace->getKey());
$response = Gate::forUser($user)->inspect('update', $connection);
expect($response->allowed())->toBeTrue();
});
it('keeps in-scope capability denials distinct from not-found boundaries', function (): void {
$workspace = Workspace::factory()->create();
$tenant = ManagedEnvironment::factory()->active()->create([

View File

@ -39,11 +39,12 @@ function triageReviewDashboardWidget(User $user, ManagedEnvironment $tenant, arr
{
test()->actingAs($user);
session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id);
setTenantPanelContext($tenant);
setAdminPanelContext($tenant);
request()->attributes->remove('portfolio_triage.arrival_context');
return Livewire::withQueryParams([
PortfolioArrivalContextToken::QUERY_PARAMETER => PortfolioArrivalContextToken::encode($state),
'tenant' => (string) $tenant->slug,
])->actingAs($user)->test(TenantTriageArrivalContinuity::class);
}
@ -62,7 +63,7 @@ function triageReviewDashboardWidget(User $user, ManagedEnvironment $tenant, arr
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
->get(TenantDashboard::getUrl([
PortfolioArrivalContextToken::QUERY_PARAMETER => PortfolioArrivalContextToken::encode(triageReviewArrivalState($foreignTenant)),
], panel: 'tenant', tenant: $foreignTenant))
], panel: 'admin', tenant: $foreignTenant))
->assertNotFound();
});
@ -111,7 +112,7 @@ function triageReviewDashboardWidget(User $user, ManagedEnvironment $tenant, arr
});
it('writes review progress and audit state only after the preview-confirmed action executes', function (): void {
[$user, $tenant] = $this->makePortfolioTriageActor('Authorization Success ManagedEnvironment');
[$user, $tenant] = $this->makePortfolioTriageActor('Authorization Success ManagedEnvironment', workspaceRole: 'owner');
$this->seedPortfolioBackupConcern($tenant, TenantBackupHealthAssessment::POSTURE_STALE);
$component = triageReviewDashboardWidget($user, $tenant, triageReviewArrivalState($tenant))
@ -122,8 +123,11 @@ function triageReviewDashboardWidget(User $user, ManagedEnvironment $tenant, arr
expect(TenantTriageReview::query()->count())->toBe(0);
$component
->callMountedAction();
$instance = $component->instance();
setAdminPanelContext($tenant);
$instance->callMountedAction();
expect(TenantTriageReview::query()
->where('managed_environment_id', (int) $tenant->getKey())

View File

@ -87,7 +87,7 @@
]);
$this->actingAs($user)
->get(ReviewPackResource::getUrl('view', ['record' => $pack], tenant: $tenant, panel: 'tenant'))
->get(ReviewPackResource::getUrl('view', ['record' => $pack], tenant: $tenant, panel: 'admin'))
->assertOk()
->assertSee(CustomerReviewWorkspace::tenantPrefilterUrl($tenant), false);
});
@ -116,7 +116,7 @@
'file_disk' => 'exports',
]);
setTenantPanelContext($tenant);
setAdminPanelContext($tenant);
Livewire::actingAs($user)
->test(TenantReviewPackCard::class, ['record' => $tenant])
@ -150,7 +150,7 @@
$review->forceFill(['current_export_review_pack_id' => (int) $pack->getKey()])->save();
setTenantPanelContext($tenant);
setAdminPanelContext($tenant);
Livewire::withQueryParams([
CustomerReviewWorkspace::DETAIL_CONTEXT_QUERY_KEY => 1,

View File

@ -152,7 +152,7 @@
->assertNotFound();
});
it('serves provider connection management under workspace-managed tenant routes only', function (): void {
it('serves provider connection management under canonical admin routes only', function (): void {
[$user, $tenant] = createMinimalUserWithTenant(role: 'owner');
$connection = ProviderConnection::factory()->create([
@ -163,29 +163,29 @@
$this->followingRedirects()
->actingAs($user)
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
->get("/admin/tenants/{$tenant->external_id}/provider-connections")
->get('/admin/provider-connections?managed_environment_id='.$tenant->external_id)
->assertOk();
$this->followingRedirects()
->actingAs($user)
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
->get("/admin/tenants/{$tenant->external_id}/provider-connections/{$connection->getKey()}/edit")
->get('/admin/provider-connections/'.$connection->getKey().'/edit?managed_environment_id='.$tenant->external_id)
->assertOk();
});
it('returns 403 for workspace members missing mutation capability on provider connections', function (): void {
it('returns 403 for workspace members missing mutation capability on canonical provider connection routes', function (): void {
[$user, $tenant] = createMinimalUserWithTenant(role: 'readonly', workspaceRole: 'readonly');
$this->followingRedirects()
->actingAs($user)
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
->get("/admin/tenants/{$tenant->external_id}/provider-connections")
->get('/admin/provider-connections?managed_environment_id='.$tenant->external_id)
->assertOk();
$this->followingRedirects()
->actingAs($user)
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
->get("/admin/tenants/{$tenant->external_id}/provider-connections/create")
->get('/admin/provider-connections/create?managed_environment_id='.$tenant->external_id)
->assertForbidden();
});

View File

@ -15,7 +15,7 @@
'reason_code' => 'missing_configuration',
'message' => 'Missing default provider connection.',
'next_steps' => [
['label' => 'Manage Provider Connections', 'url' => '/admin/tenants/example/provider-connections'],
['label' => 'Manage Provider Connections', 'url' => '/admin/provider-connections?managed_environment_id=example'],
['label' => '', 'url' => '/admin/invalid'],
['label' => 'Missing URL'],
],
@ -25,7 +25,7 @@
expect($report['checks'][0]['next_steps'] ?? [])->toBe([
[
'label' => 'Manage Provider Connections',
'url' => '/admin/tenants/example/provider-connections',
'url' => '/admin/provider-connections?managed_environment_id=example',
],
]);
});

View File

@ -1323,16 +1323,17 @@ function restateTenantReviewEvidenceSnapshot(
function setTenantPanelContext(ManagedEnvironment $tenant): void
{
$tenant->makeCurrent();
Filament::setCurrentPanel('tenant');
Filament::setTenant($tenant, true);
Filament::bootCurrentPanel();
setAdminPanelContext($tenant);
}
function setAdminPanelContext(): void
function setAdminPanelContext(?ManagedEnvironment $tenant = null): void
{
if ($tenant instanceof ManagedEnvironment) {
$tenant->makeCurrent();
}
Filament::setCurrentPanel('admin');
Filament::setTenant(null, true);
Filament::setTenant($tenant, true);
Filament::bootCurrentPanel();
}

View File

@ -0,0 +1,50 @@
# Requirements Checklist: Cutover Prerequisite Completion
## Scope and problem framing
- [x] The package describes the real repo problem as unfinished runtime and test-harness prerequisites, not as missing quality gates.
- [x] The package keeps scope limited to the five named prerequisite areas.
- [x] The package explicitly moves quality gates and no-legacy enforcement to Spec `288`.
- [x] The package explicitly excludes full-suite baselines, package execution, guided operations, UI copy cleanup, and provider capability expansion.
## Repo-truth anchoring
- [x] The package names the surviving provider-connection legacy route family in `apps/platform/routes/web.php`.
- [x] The package pins the in-slice provider-connection launch-point inventory in `apps/platform/app/Providers/Filament/AdminPanelProvider.php`, `apps/platform/app/Filament/Resources/TenantResource.php`, `apps/platform/app/Filament/Pages/TenantRequiredPermissions.php`, `apps/platform/app/Support/OperationRunLinks.php`, `apps/platform/app/Support/Providers/ProviderReasonTranslator.php`, and `apps/platform/app/Support/Verification/VerificationLinkBehavior.php`.
- [x] The package anchors provider target-scope completion to the real shared provider-core seams from the `281` workstream.
- [x] The package anchors environment-scope cleanup to role-bearing `ManagedEnvironmentMembership` persistence drift on the existing auth seams.
- [x] The package anchors helper cutover to `apps/platform/tests/Pest.php`, `apps/platform/tests/Feature/Reviews/CustomerReviewWorkspaceLaunchLinksTest.php`, and `apps/platform/tests/Feature/Rbac/TriageReviewStateAuthorizationTest.php`.
## Completion inventory and boundedness
- [x] The same completion categories appear across `spec.md`, `plan.md`, `data-model.md`, and `quickstart.md`.
- [x] The package defines canonical replacements instead of adding compatibility aliases or a guard inventory.
- [x] The package keeps provider-owned Microsoft detail nested and bounded instead of pretending it disappears entirely.
- [x] The package forbids introducing a new guard subsystem or full-suite wrapper here.
## Validation and workflow
- [x] Planned proof stays bounded to targeted feature tests, targeted browser validation, and formatting.
- [x] The same validation commands appear in `spec.md`, `plan.md`, `tasks.md`, and `quickstart.md`.
- [x] The task package explicitly re-verifies Filament/Livewire/provider-registration invariants and the no-asset-registration / no-deployment-step boundary.
- [x] The package keeps review outcome, workflow outcome, and test-governance outcome aligned.
- [x] The package uses `RuntimePrerequisite` rather than `Guardrail` as the close-out intent.
## Adjacent-spec control
- [x] Spec `288` is named as the explicit follow-up package for quality gates and no-legacy enforcement.
- [x] The package does not silently absorb UI copy cleanup from Spec `286`.
- [x] The package does not silently absorb provider capability expansion from Spec `283`.
- [x] The package does not silently absorb package execution or guided operations work.
## Notes
- Reviewed against `.specify/memory/constitution.md`, current repo route, provider-core, auth, and test-support seams, and adjacent Specs `280`, `281`, `285`, and `286` on 2026-05-10.
- This artifact package is implementation-ready and no longer uses the earlier blocked-by-prerequisites posture.
## Outcome
- **Review outcome class**: `acceptable-special-case`
- **Workflow outcome**: `keep`
- **Test-governance outcome**: `keep`
- **Readiness note**: implementation is ready as a bounded prerequisite-completion slice; no-legacy enforcement is deferred to Spec `288`

View File

@ -0,0 +1,209 @@
openapi: 3.1.0
info:
title: Cutover Prerequisite Completion Logical Contract
version: 0.1.0
summary: Logical completion contract for the remaining runtime and test-harness seams that unblock Spec 288.
x-canonical-command-authority:
- specs/287-cutover-prerequisite-completion/spec.md
- specs/287-cutover-prerequisite-completion/plan.md
- specs/287-cutover-prerequisite-completion/tasks.md
- specs/287-cutover-prerequisite-completion/quickstart.md
paths:
/__logical/prerequisites/provider-connection-routes:
get:
summary: Provider-connection route retirement contract
operationId: getProviderConnectionRouteCompletionContract
responses:
'200':
description: Legacy provider-connection aliases to retire and canonical replacements
content:
application/json:
schema:
$ref: '#/components/schemas/ProviderConnectionRouteContract'
/__logical/prerequisites/provider-target-scope:
get:
summary: Provider target-scope core neutralization contract
operationId: getProviderTargetScopeCompletionContract
responses:
'200':
description: Shared provider target-scope fields and provider-owned nested detail rules
content:
application/json:
schema:
$ref: '#/components/schemas/ProviderTargetScopeContract'
/__logical/prerequisites/environment-scope-persistence:
get:
summary: Workspace-first role authority and environment-scope persistence contract
operationId: getEnvironmentScopePersistenceCompletionContract
responses:
'200':
description: Role-bearing truth and narrowing-only environment scope rules
content:
application/json:
schema:
$ref: '#/components/schemas/EnvironmentScopePersistenceContract'
/__logical/prerequisites/test-helper-cutover:
get:
summary: Tenant-panel test-helper cutover contract
operationId: getTestHelperCutoverCompletionContract
responses:
'200':
description: Shared test helper replacement and named direct-consumer inventory
content:
application/json:
schema:
$ref: '#/components/schemas/TestHelperCutoverContract'
/__logical/prerequisites/targeted-validation:
get:
summary: Targeted validation contract
operationId: getTargetedValidationCompletionContract
responses:
'200':
description: Exact targeted validation commands and bounded proof rules
content:
application/json:
schema:
$ref: '#/components/schemas/TargetedValidationContract'
components:
schemas:
ProviderConnectionRouteContract:
type: object
required:
- retiredRouteFamilies
- canonicalRouteFamilies
- launchPointFilesInScope
- behaviorAfterCompletion
properties:
retiredRouteFamilies:
type: array
items:
type: string
default:
- /admin/tenants/{tenant:slug}/provider-connections
canonicalRouteFamilies:
type: array
items:
type: string
default:
- /admin/provider-connections
- /admin/provider-connections/{record}
launchPointFilesInScope:
type: array
items:
type: string
default:
- apps/platform/app/Providers/Filament/AdminPanelProvider.php
- apps/platform/app/Filament/Resources/TenantResource.php
- apps/platform/app/Filament/Pages/TenantRequiredPermissions.php
- apps/platform/app/Support/OperationRunLinks.php
- apps/platform/app/Support/Providers/ProviderReasonTranslator.php
- apps/platform/app/Support/Verification/VerificationLinkBehavior.php
behaviorAfterCompletion:
type: string
const: Canonical provider-connection routes are the only accepted runtime path.
ProviderTargetScopeContract:
type: object
required:
- forbiddenSharedPrimaryKeys
- requiredNeutralSharedFields
- allowedProviderOwnedNestedFields
properties:
forbiddenSharedPrimaryKeys:
type: array
items:
type: string
default:
- entra_tenant_id
- tenantContext
- target_scope.entra_tenant_id
requiredNeutralSharedFields:
type: array
items:
type: string
default:
- scope_kind
- scope_identifier
- scope_display_name
allowedProviderOwnedNestedFields:
type: array
items:
type: string
default:
- provider_profile.microsoft.tenant_id
- provider_profile.microsoft.authority_tenant
- provider_profile.microsoft.consent_url
EnvironmentScopePersistenceContract:
type: object
required:
- roleAuthoritySource
- narrowingOnlyScopeStore
- forbiddenPersistenceBehavior
properties:
roleAuthoritySource:
type: string
const: workspace_memberships
narrowingOnlyScopeStore:
type: string
const: managed_environment_memberships or its in-place successor
forbiddenPersistenceBehavior:
type: array
items:
type: string
default:
- mirrored workspace role values as environment role truth
- second independent role matrix at environment scope
TestHelperCutoverContract:
type: object
required:
- retiredHelpers
- canonicalHelpers
- directConsumersInScope
properties:
retiredHelpers:
type: array
items:
type: string
default:
- setTenantPanelContext
canonicalHelpers:
type: array
items:
type: string
default:
- setAdminPanelContext
- post-cutover admin/workspace context helper
directConsumersInScope:
type: array
items:
type: string
default:
- tests/Feature/Reviews/CustomerReviewWorkspaceLaunchLinksTest.php
- tests/Feature/Rbac/TriageReviewStateAuthorizationTest.php
TargetedValidationContract:
type: object
required:
- featureCommand
- browserCommand
- formattingCommand
- outOfScopeProof
properties:
featureCommand:
type: string
const: export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && REPO_ROOT="$(git rev-parse --show-toplevel)" && (cd "$REPO_ROOT/apps/platform" && ./vendor/bin/sail artisan test --compact tests/Feature/ProviderConnections/LegacyRedirectTest.php tests/Feature/ProviderConnections/TenantlessListRouteTest.php tests/Feature/ProviderConnections/TenantlessListScopingTest.php tests/Feature/Auth/WorkspaceFirstManagedEnvironmentAccessTest.php tests/Feature/Rbac/ProviderConnectionWorkspaceFirstPolicyTest.php tests/Feature/Reviews/CustomerReviewWorkspaceLaunchLinksTest.php tests/Feature/Rbac/TriageReviewStateAuthorizationTest.php)
browserCommand:
type: string
const: export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && REPO_ROOT="$(git rev-parse --show-toplevel)" && (cd "$REPO_ROOT/apps/platform" && ./vendor/bin/sail artisan test --compact tests/Browser/Spec281ProviderConnectionScopeSmokeTest.php tests/Browser/Spec285WorkspaceRbacEnvironmentAccessSmokeTest.php)
formattingCommand:
type: string
const: export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && REPO_ROOT="$(git rev-parse --show-toplevel)" && (cd "$REPO_ROOT/apps/platform" && ./vendor/bin/sail bin pint --dirty --format agent)
outOfScopeProof:
type: array
items:
type: string
default:
- no global no-legacy guard suite
- no full-suite baseline
- no package execution
- no guided operations
- no UI copy cleanup
- no provider capability expansion

View File

@ -0,0 +1,49 @@
# Data Model: Cutover Prerequisite Completion
## Overview
`287` introduces no new persisted entity, table, lifecycle state, or runtime DTO. The "data model" for this package is a derived seam inventory that pins which runtime and test-harness seams must be completed, which canonical replacements are expected, and which validation commands stay aligned across the package.
## Canonical Completion Categories
| Completion Key | Meaning | Primary Targets | Canonical Replacement |
|---|---|---|---|
| `provider_connection_route_contract` | provider-connection runtime no longer accepts tenant-first legacy aliases | `apps/platform/routes/web.php`, `apps/platform/app/Providers/Filament/AdminPanelProvider.php`, `apps/platform/app/Filament/Resources/TenantResource.php`, `apps/platform/app/Filament/Pages/TenantRequiredPermissions.php`, `apps/platform/app/Support/OperationRunLinks.php`, `apps/platform/app/Support/Providers/ProviderReasonTranslator.php`, `apps/platform/app/Support/Verification/VerificationLinkBehavior.php` | canonical `/admin/provider-connections...` route family |
| `provider_target_scope_core_contract` | shared provider target-scope and identity outputs no longer require Microsoft-only core keys | shared provider target-scope and identity seams | neutral target-scope fields with provider-owned nested detail only |
| `workspace_role_authority_contract` | workspace membership remains the only role-bearing truth | access-scope persistence seams and directly affected policy or resolver paths | workspace membership role truth with narrowing-only environment scope |
| `test_harness_context_contract` | the shared test harness no longer boots the retired tenant panel for the changed seams | `apps/platform/tests/Pest.php`, `apps/platform/tests/Feature/Reviews/CustomerReviewWorkspaceLaunchLinksTest.php`, and `apps/platform/tests/Feature/Rbac/TriageReviewStateAuthorizationTest.php` | post-cutover admin or workspace context helper path |
| `targeted_validation_contract` | proof remains limited to the changed seams | targeted feature tests, targeted browser tests, formatting command | exact command set shared across the package |
## Pinned Canonical Replacements
| Completion Key | Retired Pattern | Canonical Replacement |
|---|---|---|
| `provider_connection_route_contract` | `/admin/tenants/{tenant:slug}/provider-connections...` | `/admin/provider-connections...` |
| `provider_target_scope_core_contract` | `entra_tenant_id`, `tenantContext`, `target_scope.entra_tenant_id` as shared primary truth | provider-neutral target-scope fields such as scope kind, scope identifier, and scope display name |
| `workspace_role_authority_contract` | copied role-bearing `ManagedEnvironmentMembership` persistence | workspace membership role truth plus narrowing-only environment scope |
| `test_harness_context_contract` | `setTenantPanelContext()` and related tenant-panel-era setup | admin or workspace context bootstrapping suitable for the completed runtime |
| `targeted_validation_contract` | guard-suite or full-suite proof expectation | targeted feature and browser validation only |
## Allowed Provider-Owned Detail
| Detail Class | Meaning | Examples |
|---|---|---|
| `provider_owned_profile_detail` | provider-specific detail is allowed where the provider is genuinely the subject | Microsoft tenant identifiers, consent URLs, Graph-specific diagnostics |
| `provider_owned_support_detail` | lower-level support or troubleshooting context remains nested provider detail | raw provider metadata, provider-specific error context |
## Invariants
- `287` adds no new runtime state and no new persistence.
- The same completion categories and the same Spec `288` follow-up boundary must appear across `spec.md`, `plan.md`, `tasks.md`, `quickstart.md`, `data-model.md`, and `checklists/requirements.md`.
- The literal proof commands live only in `spec.md`, `plan.md`, `tasks.md`, and `quickstart.md`; the remaining artifacts reference that canonical command set rather than restating another variant.
- Validation remains targeted to the changed seams only.
- Provider-owned Microsoft detail may remain nested, but it must not stay the shared platform-core contract.
- Environment scope must remain narrowing-only after the cleanup.
## Out of Scope Data Changes
- no database migrations
- no new provider profile table or registry
- no new role family or persisted access overlay
- no global no-legacy guard inventory
- no full-suite baseline or lane-wide enforcement artifact

View File

@ -0,0 +1,230 @@
# Implementation Plan: Cutover Prerequisite Completion
**Branch**: `287-cutover-prerequisite-completion` | **Date**: 2026-05-10 | **Spec**: [spec.md](./spec.md)
**Input**: Feature specification from `specs/287-cutover-prerequisite-completion/spec.md`
## Summary
Complete the remaining runtime and test-harness seams that still block the later quality-gates / no-legacy package. The narrow implementation path retires the legacy provider-connection route family, finishes provider target-scope core neutralization on the shared provider seams, cleans environment-scope role persistence so workspace membership remains the only role-bearing truth, replaces tenant-panel-era test helpers with post-cutover admin or workspace helpers, and validates only those seams with targeted feature and browser coverage.
This plan is intentionally not a no-legacy guard package. Filament remains v5 on Livewire v4, provider registration remains in `apps/platform/bootstrap/providers.php`, no new asset or deployment step is introduced, no full-suite baseline is required, and Spec `288` remains the explicit follow-up for quality gates and no-legacy enforcement.
## Inherited Baseline / Explicit Delta
### Inherited baseline
- Spec `279` already owns the managed-environment core cutover and remains historical baseline context only.
- Spec `280` already owns the workspace-first route and panel-shell convergence, but repo truth still shows a surviving legacy provider-connection route family in `apps/platform/routes/web.php`.
- Spec `281` already owns provider boundary groundwork, but repo truth still shows Microsoft-shaped shared target-scope and identity fields on platform-core provider seams.
- Spec `282` already owns governance-artifact retargeting and remains adjacent historical context only.
- Spec `285` already owns the workspace-first RBAC direction, but repo truth still shows incomplete environment-scope persistence cleanup where managed-environment membership records mirror workspace role values.
- Spec `286` already owns UI copy cleanup and remains explicitly out of scope for this package.
### Explicit delta in this plan
- Retire the remaining legacy provider-connection route family instead of guarding it.
- Finish provider target-scope and identity neutralization on shared provider-core seams instead of leaving that work to a later enforcement slice.
- Complete the runtime cleanup that keeps workspace membership role-bearing and environment scope narrowing-only.
- Replace tenant-panel-era shared test helpers and the proof-command consumer tests `apps/platform/tests/Feature/Reviews/CustomerReviewWorkspaceLaunchLinksTest.php` and `apps/platform/tests/Feature/Rbac/TriageReviewStateAuthorizationTest.php` with post-cutover admin or workspace context helpers.
- Keep proof targeted to changed seams only and hand global enforcement to Spec `288`.
## Technical Context
**Language/Version**: PHP 8.4.15, Laravel 12.52
**Primary Dependencies**: Pest 4, Filament 5.2.1, Livewire 4.1.4, existing provider target-scope and access-scope services, shared Pest test helpers
**Storage**: no new persistence; the package completes behavior on existing route, provider-core, access-scope, and test-support seams
**Testing**: targeted Pest feature tests plus targeted browser validation
**Validation Lanes**: fast-feedback, confidence, browser
**Target Platform**: Laravel monolith in `apps/platform`
**Project Type**: web application
**Performance Goals**: keep validation limited to the changed seams; no full-suite or broad guard lane work
**Constraints**: no global guard suite, no full-suite baseline, no package execution, no guided operations, no UI copy cleanup, no provider capability expansion
**Scale/Scope**: one bounded prerequisite-completion slice immediately preceding Spec `288`
## Likely Affected Repo Surfaces
- `apps/platform/routes/web.php`
- `apps/platform/app/Services/Providers/ProviderConnectionResolver.php`
- `apps/platform/app/Services/Providers/ProviderIdentityResolver.php`
- `apps/platform/app/Services/Providers/ProviderIdentityResolution.php`
- `apps/platform/app/Services/Providers/PlatformProviderIdentityResolver.php`
- `apps/platform/app/Services/Providers/ProviderOperationStartGate.php`
- `apps/platform/app/Support/Providers/TargetScope/ProviderConnectionTargetScopeNormalizer.php`
- `apps/platform/app/Support/Providers/TargetScope/ProviderConnectionTargetScopeDescriptor.php`
- `apps/platform/app/Services/Auth/TenantMembershipManager.php`
- `apps/platform/app/Services/Auth/ManagedEnvironmentAccessScopeResolver.php`
- `apps/platform/app/Providers/Filament/AdminPanelProvider.php`
- `apps/platform/app/Filament/Resources/TenantResource.php`
- `apps/platform/app/Filament/Pages/TenantRequiredPermissions.php`
- `apps/platform/app/Support/OperationRunLinks.php`
- `apps/platform/app/Support/Providers/ProviderReasonTranslator.php`
- `apps/platform/app/Support/Verification/VerificationLinkBehavior.php`
- `apps/platform/tests/Pest.php`
- targeted feature tests in `apps/platform/tests/Feature/ProviderConnections/`, `apps/platform/tests/Feature/Auth/`, `apps/platform/tests/Feature/Rbac/`, `apps/platform/tests/Feature/Reviews/CustomerReviewWorkspaceLaunchLinksTest.php`, and `apps/platform/tests/Feature/Rbac/TriageReviewStateAuthorizationTest.php`
- targeted browser tests in `apps/platform/tests/Browser/`
## Filament v5 / Surface Notes
- **Livewire v4.0+ compliance**: all touched surfaces remain on Filament v5 with Livewire v4.
- **Provider registration location**: provider registration remains in `apps/platform/bootstrap/providers.php`; this package does not add a new panel or change provider registration location.
- **Global search rule**: this slice introduces no new globally-searchable resource. Existing provider-connection resource behavior remains unchanged from a global-search standpoint.
- **Destructive actions**: no new destructive action is introduced. Any touched existing destructive actions must keep `->action(...)`, `->requiresConfirmation()`, and server authorization.
- **Asset strategy**: no new asset registration or deployment step is planned.
## Prerequisite Completion Fit
- Complete runtime truth first, then let Spec `288` enforce that truth.
- Prefer canonical replacement over compatibility routes, copied role persistence, or tenant-panel test-only fallbacks.
- Keep Microsoft-specific detail nested under provider-owned seams instead of the shared provider-core contract.
- Keep the package bounded to five named prerequisite areas and reject adjacent feature work.
- Use exact targeted validation commands across `spec.md`, `plan.md`, `tasks.md`, and `quickstart.md`.
## UI / Surface Guardrail Plan
- **Guardrail scope**: bounded runtime completion over existing provider-connection and provider-backed operator surfaces
- **Native vs custom classification summary**: existing native Filament provider-connection surfaces and shared provider summaries only; no new operator-facing page family
- **Shared-family relevance**: navigation, provider target-scope summaries, run-launch context, and test harness setup
- **State layers in scope**: route, page, detail, shared provider-core payloads, access-scope persistence, and test-support context helpers
- **Audience modes in scope**: operator-MSP, support-platform on existing provider surfaces only
- **Decision/diagnostic/raw hierarchy plan**: keep current provider-owned diagnostics nested; change only the shared contract and canonical route truth
- **Raw/support gating plan**: preserve existing provider-owned raw detail where current support or consent workflows genuinely need it
- **One-primary-action / duplicate-truth control**: no new action family is introduced; existing launch and open actions continue to own the workflow
- **Handling modes by drift class or surface**: implementation-required for the named seams; out of scope for global enforcement and copy cleanup
- **Repository-signal treatment**: direct runtime completion only; no broad source-scan or lint layer
- **Special surface test profiles**: standard-native-filament, global-context-shell
- **Required tests or manual smoke**: functional-core, targeted browser-smoke
- **Exception path and spread control**: provider-owned Microsoft detail only; no other exception expansion
- **Active feature PR close-out entry**: RuntimePrerequisite
## Shared Pattern & System Fit
- **Cross-cutting feature marker**: yes
- **Systems touched**: provider route ownership, shared provider target-scope and identity resolution, workspace-first access persistence cleanup, and shared test helpers
- **Shared abstractions reused**: existing provider target-scope and identity helpers, existing workspace access resolver seams, and existing `tests/Pest.php` helper style
- **New abstraction introduced? why?**: none
- **Why the existing abstraction was sufficient or insufficient**: the abstractions already exist; the incomplete cutover behavior inside them is the real remaining work
- **Bounded deviation / spread control**: Microsoft-specific profile detail remains nested only where provider-owned workflows still need it
## OperationRun UX Impact
- **Touches OperationRun start/completion/link UX?**: yes, as shared provider-backed run context and canonical link truth only
- **Central contract reused**: `ProviderOperationStartGate`, existing OperationRun context, and canonical operation links
- **Delegated UX behaviors**: existing queued, blocked, and run-link semantics remain delegated; this slice changes only the prerequisite data and route truth they rely on
- **Surface-owned behavior kept local**: existing provider launch or open actions only
- **Queued DB-notification policy**: `N/A`
- **Terminal notification path**: existing central lifecycle mechanism
- **Exception path**: none
## Provider Boundary & Portability Fit
- **Shared provider/platform boundary touched?**: yes
- **Provider-owned seams**: consent/profile detail, Microsoft-specific identifiers, Graph-specific diagnostics, and provider-owned support metadata
- **Platform-core seams**: provider-connection route ownership, shared target-scope and identity outputs, provider-backed run context, and workspace-scoped launch truth
- **Neutral platform terms / contracts preserved**: `provider connection`, `target scope`, `scope kind`, `scope identifier`, `scope display name`, `workspace`, `managed environment`
- **Retained provider-specific semantics and why**: existing consent and support flows still need Microsoft tenant/profile data, but only as provider-owned nested detail
- **Bounded extraction or follow-up path**: Spec `288` for no-legacy enforcement after this slice lands
## Constitution Check
*GATE: Must pass before implementation begins and again after design artifacts are complete.*
- Inventory-first: PASS. No new inventory or snapshot truth is introduced.
- Read/write separation: PASS. The slice completes existing runtime seams without adding a new workflow surface.
- Graph contract path: PASS by preservation. No new Graph integration family or registry is introduced.
- Deterministic capabilities: PASS by preservation. Capability families do not expand.
- RBAC-UX: PASS. Workspace membership remains role-bearing and environment scope becomes narrowing-only on the completed seams.
- Workspace isolation: PASS. The package preserves workspace-first routing and entitlement boundaries.
- Managed-environment isolation: PASS. The package narrows environment scope instead of broadening it.
- Run observability: PASS. Existing OperationRun lifecycle and links remain central.
- OperationRun start UX: PASS. No new start or completion UX family is added.
- Data minimization: PASS. No new persistence or compatibility ledger is introduced.
- Test governance: PASS. Validation stays targeted and explicit.
- Proportionality / no premature abstraction: PASS. Existing seams are completed rather than wrapped in new frameworks.
- Persisted truth / behavioral state: PASS. No new persistence or state family is introduced.
- Provider boundary: PASS. Shared provider-core contracts become more neutral while provider-owned detail stays bounded.
**Gate evaluation**: PASS.
**Post-design re-check**: PASS while `spec.md`, `plan.md`, `tasks.md`, and `quickstart.md` keep the same literal proof commands, and `research.md`, `data-model.md`, `contracts/cutover-prerequisite-completion.logical.openapi.yaml`, and `checklists/requirements.md` keep the same seam inventory and Spec `288` follow-up boundary.
## Test Governance Check
- **Test purpose / classification by changed surface**: Feature, Browser
- **Affected validation lanes**: fast-feedback, confidence, browser
- **Why this lane mix is the narrowest sufficient proof**: the changed seams are concrete runtime and test-support paths, so focused feature tests plus a narrow browser smoke are sufficient and honest
- **Narrowest proving command(s)**:
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && REPO_ROOT="$(git rev-parse --show-toplevel)" && (cd "$REPO_ROOT/apps/platform" && ./vendor/bin/sail artisan test --compact tests/Feature/ProviderConnections/LegacyRedirectTest.php tests/Feature/ProviderConnections/TenantlessListRouteTest.php tests/Feature/ProviderConnections/TenantlessListScopingTest.php tests/Feature/Auth/WorkspaceFirstManagedEnvironmentAccessTest.php tests/Feature/Rbac/ProviderConnectionWorkspaceFirstPolicyTest.php tests/Feature/Reviews/CustomerReviewWorkspaceLaunchLinksTest.php tests/Feature/Rbac/TriageReviewStateAuthorizationTest.php)`
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && REPO_ROOT="$(git rev-parse --show-toplevel)" && (cd "$REPO_ROOT/apps/platform" && ./vendor/bin/sail artisan test --compact tests/Browser/Spec281ProviderConnectionScopeSmokeTest.php tests/Browser/Spec285WorkspaceRbacEnvironmentAccessSmokeTest.php)`
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && REPO_ROOT="$(git rev-parse --show-toplevel)" && (cd "$REPO_ROOT/apps/platform" && ./vendor/bin/sail bin pint --dirty --format agent)`
- **Fixture / helper / factory / seed / context cost risks**: moderate only because the shared tenant-panel helper is being replaced on `tests/Feature/Reviews/CustomerReviewWorkspaceLaunchLinksTest.php` and `tests/Feature/Rbac/TriageReviewStateAuthorizationTest.php`
- **Expensive defaults or shared helper growth introduced?**: no; the helper cutover must reduce dependence on retired panel state rather than add new implicit defaults
- **Heavy-family additions, promotions, or visibility changes**: none
- **Surface-class relief / special coverage rule**: `standard-native-filament` and `global-context-shell` remain sufficient; no heavy-governance coverage is justified
- **Closing validation and reviewer handoff**: rerun the exact commands above, verify Filament stays on Livewire v4, provider registration remains in `apps/platform/bootstrap/providers.php`, no asset registration or deployment-step drift was added, no full-suite or guard-family work was added, and confirm Spec `288` remains the explicit follow-up
- **Budget / baseline / trend follow-up**: contained feature-local increase only
- **Review-stop questions**: did the implementation widen into no-legacy guards, UI copy cleanup, package execution, guided operations, or provider capability expansion
- **Escalation path**: `document-in-feature` for contained seam follow-up, `reject-or-split` for scope expansion
- **Active feature PR close-out entry**: RuntimePrerequisite
## Review Checklist Status
- **Review checklist artifact**: `checklists/requirements.md`
- **Review outcome class**: `acceptable-special-case`
- **Workflow outcome**: `keep`
- **Test-governance outcome**: `keep`
- **Resolution note**: the package is implementation-ready as a bounded prerequisite-completion slice and no longer depends on a blocked-by-prerequisites posture
- **Escalation rule**: if implementation starts adding guard suites, full-suite baselines, or adjacent product features, stop and split the work out of `287`
## Rollout Considerations
- Retire the legacy provider-connection route family before widening targeted validation so the canonical route truth settles first.
- Complete provider target-scope neutralization before touching the browser proof so the live provider surfaces already speak the intended shared contract.
- Cut over the shared test helper before updating `apps/platform/tests/Feature/Reviews/CustomerReviewWorkspaceLaunchLinksTest.php` and `apps/platform/tests/Feature/Rbac/TriageReviewStateAuthorizationTest.php` so targeted tests do not drift through two helper systems.
- Keep Spec `288` untouched during runtime completion; it should start from the completed baseline produced by this slice.
## Risk Controls
- Reject any implementation that keeps legacy provider-connection aliases as compatibility routes for convenience.
- Reject any implementation that solves provider target-scope cleanup by hiding Microsoft-only fields in new platform-core wrappers instead of neutralizing the shared contract.
- Reject any implementation that preserves copied role-bearing state on environment-scope persistence after the cleanup.
- Reject any implementation that adds a global guard family, broad source-scan package, or full-suite baseline under this spec.
## Research & Design Outputs
- `research.md` records the completion-first decisions, bounded runtime scope, rejected guard-suite alternative, and evidence anchors.
- `data-model.md` captures the derived seam inventory, canonical replacements, and invariants.
- `quickstart.md` gives reviewers the scope boundary, review scenarios, and exact targeted proof commands.
- `contracts/cutover-prerequisite-completion.logical.openapi.yaml` models the logical completion seams and the targeted validation contract.
- `checklists/requirements.md` records the review outcome and bounded scope rules.
## Project Structure
### Documentation (this feature)
```text
specs/287-cutover-prerequisite-completion/
├── checklists/
│ └── requirements.md
├── contracts/
│ └── cutover-prerequisite-completion.logical.openapi.yaml
├── data-model.md
├── plan.md
├── quickstart.md
├── research.md
├── spec.md
└── tasks.md
```
### Source Code (repository root)
```text
apps/platform/
├── app/
├── routes/
└── tests/
├── Browser/
├── Feature/
└── Pest.php
```
**Structure Decision**: keep the package inside the existing Laravel app, route, provider, access-service, and Pest test-support structure. No new base directory is needed.

View File

@ -0,0 +1,81 @@
# Quickstart: Cutover Prerequisite Completion
## Purpose
Use this guide to review or implement Feature `287` as the bounded runtime and test-harness completion slice that unblocks Spec `288`.
## Preconditions
- The package stays limited to the five named prerequisite areas:
- provider connection legacy route retirement
- provider target scope core neutralization
- environment scope role persistence cleanup
- tenant panel test helper cutover
- targeted validation for the changed seams
- Spec `288` is treated as the follow-up package for quality gates and no-legacy enforcement.
- The implementation does not add a global guard suite, a full-suite baseline, package execution, guided operations, UI copy cleanup, or provider capability expansion.
- Filament remains v5 on Livewire v4 and provider registration remains in `apps/platform/bootstrap/providers.php`.
## Read Order
1. `spec.md`
2. `plan.md`
3. `research.md`
4. `data-model.md`
5. `contracts/cutover-prerequisite-completion.logical.openapi.yaml`
6. `tasks.md`
7. `checklists/requirements.md`
## Implementation Intent
- retire the remaining legacy provider-connection route family instead of guarding it
- finish provider target-scope and identity neutralization on the shared provider-core seams
- complete the cleanup that keeps workspace membership role-bearing and environment scope narrowing-only
- replace tenant-panel-era test helpers with post-cutover admin or workspace helpers and update `apps/platform/tests/Feature/Reviews/CustomerReviewWorkspaceLaunchLinksTest.php` plus `apps/platform/tests/Feature/Rbac/TriageReviewStateAuthorizationTest.php`
- keep validation targeted to the changed seams only and leave quality gates to Spec `288`
## Review Scenarios
### Scenario 1: Canonical provider-connection routing is the only runtime path
- open canonical provider-connection paths and the launch-point inventory in `apps/platform/app/Providers/Filament/AdminPanelProvider.php`, `apps/platform/app/Filament/Resources/TenantResource.php`, `apps/platform/app/Filament/Pages/TenantRequiredPermissions.php`, `apps/platform/app/Support/OperationRunLinks.php`, `apps/platform/app/Support/Providers/ProviderReasonTranslator.php`, and `apps/platform/app/Support/Verification/VerificationLinkBehavior.php`
- confirm they resolve through `/admin/provider-connections...`
- confirm the retired legacy alias family no longer acts as accepted runtime fallback
### Scenario 2: Shared provider target-scope truth is neutral
- exercise the target-scope descriptor, provider identity resolution path, and provider-backed run or start context
- confirm the shared contract no longer depends on Microsoft-only keys such as `entra_tenant_id`, `tenantContext`, or `target_scope.entra_tenant_id`
- confirm provider-owned Microsoft detail remains nested where current consent or diagnostics still need it
### Scenario 3: Workspace membership remains the only role-bearing truth
- create workspace membership plus managed-environment access-scope combinations
- confirm workspace membership still owns role authority
- confirm environment scope only narrows visibility and does not behave like a second role matrix
### Scenario 4: The shared test harness no longer boots the retired tenant panel
- replace `setTenantPanelContext()` on the shared helper path, `apps/platform/tests/Feature/Reviews/CustomerReviewWorkspaceLaunchLinksTest.php`, and `apps/platform/tests/Feature/Rbac/TriageReviewStateAuthorizationTest.php`
- rerun the targeted seam tests and browser validation
- confirm the changed seams no longer require tenant-panel-only test state
## Planned Validation Commands
```bash
export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && REPO_ROOT="$(git rev-parse --show-toplevel)" && (cd "$REPO_ROOT/apps/platform" && ./vendor/bin/sail artisan test --compact tests/Feature/ProviderConnections/LegacyRedirectTest.php tests/Feature/ProviderConnections/TenantlessListRouteTest.php tests/Feature/ProviderConnections/TenantlessListScopingTest.php tests/Feature/Auth/WorkspaceFirstManagedEnvironmentAccessTest.php tests/Feature/Rbac/ProviderConnectionWorkspaceFirstPolicyTest.php tests/Feature/Reviews/CustomerReviewWorkspaceLaunchLinksTest.php tests/Feature/Rbac/TriageReviewStateAuthorizationTest.php)
```
```bash
export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && REPO_ROOT="$(git rev-parse --show-toplevel)" && (cd "$REPO_ROOT/apps/platform" && ./vendor/bin/sail artisan test --compact tests/Browser/Spec281ProviderConnectionScopeSmokeTest.php tests/Browser/Spec285WorkspaceRbacEnvironmentAccessSmokeTest.php)
```
```bash
export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && REPO_ROOT="$(git rev-parse --show-toplevel)" && (cd "$REPO_ROOT/apps/platform" && ./vendor/bin/sail bin pint --dirty --format agent)
```
## Explicit Stop Conditions
- If implementation starts adding no-legacy guard families or quality gates, stop and move that work to Spec `288`.
- If implementation starts requiring a full-suite baseline to prove the slice, stop and narrow the seam inventory.
- If implementation widens into UI copy cleanup, package execution, guided operations, or provider capability expansion, stop and split the extra work out of `287`.

View File

@ -0,0 +1,68 @@
# Research: Cutover Prerequisite Completion
## Decision 1: Spec `287` now completes prerequisites; Spec `288` owns enforcement
- Use this package to finish the remaining runtime and test-harness seams that still block quality-gates / no-legacy enforcement.
- Do not add a guard suite, a full-suite baseline, or global quality gates here.
- Keep the follow-up boundary explicit: Spec `288` starts after this runtime baseline exists.
## Decision 2: Retire the provider-connection legacy route family instead of guarding it
- The provider-connection legacy alias family in `apps/platform/routes/web.php` is a runtime seam, not an enforcement-only concern.
- Remove it in this slice so later enforcement can guard the completed route truth instead of compensating for it.
## Decision 3: Finish provider target-scope core neutralization on shared seams only
- Neutralize the shared provider-core contract where repo truth still depends on Microsoft-shaped identity or target-scope fields.
- Keep Microsoft-specific tenant/profile, consent, and support detail nested under provider-owned seams only.
- Do not add a new provider profile table, registry, or framework.
## Decision 4: Complete workspace-first access persistence instead of layering more RBAC logic
- Treat workspace membership as the only role-bearing truth.
- Finish the cleanup that keeps managed-environment scope narrowing-only.
- Do not introduce a second role system, a compatibility shim, or a new role family.
## Decision 5: Replace tenant-panel-era test helpers with post-cutover admin or workspace helpers
- `apps/platform/tests/Pest.php` still carries tenant-panel-era setup such as `setTenantPanelContext()` and related legacy profile alias helpers.
- Replace the retired panel assumption on the shared helper path and the in-slice direct consumers `tests/Feature/Reviews/CustomerReviewWorkspaceLaunchLinksTest.php` plus `tests/Feature/Rbac/TriageReviewStateAuthorizationTest.php`.
- Do not turn this into a broad test-suite rewrite; keep it to the helpers and direct consumers needed by this slice.
## Decision 6: Validation must stay targeted
- Use focused feature tests and targeted browser validation for the changed seams only.
- Do not add a global guard family, broad source-scan package, or full-suite baseline under this spec.
## Rejected Alternatives
### Rejected: keep `287` as a blocked no-legacy guard package
That would force the later enforcement slice to compete with unfinished runtime work and would keep the package blocked for the wrong reason.
### Rejected: solve the route and helper drift with compatibility aliases
That would preserve the same ambiguity that Spec `288` is supposed to eliminate.
### Rejected: introduce a new provider profile or access-scope framework
The existing seams are already sufficient; they need completion, not a second architectural layer.
### Rejected: use a full-suite baseline as the proof requirement
The slice is bounded and should prove only the changed seams.
## Evidence Anchors
- `apps/platform/routes/web.php` still contains `/admin/tenants/{tenant:slug}/provider-connections` redirect routes.
- `apps/platform/app/Support/Providers/TargetScope/ProviderConnectionTargetScopeNormalizer.php` and related provider-core seams still participate in the shared target-scope contract that this slice completes.
- `apps/platform/app/Services/Auth/TenantMembershipManager.php` still persists managed-environment membership records with copied workspace role values.
- `apps/platform/tests/Pest.php` still contains `setTenantPanelContext()` and `createUserWithTenantLegacyProfileAliases()`.
- The in-slice direct consumer tests `tests/Feature/Reviews/CustomerReviewWorkspaceLaunchLinksTest.php` and `tests/Feature/Rbac/TriageReviewStateAuthorizationTest.php` still depend on the retired tenant-panel helper path.
## Implementation Boundary Summary
- The package is implementation-ready as a bounded prerequisite-completion slice.
- It is no longer a blocked-by-prerequisites guard package.
- If implementation starts adding guard suites, full-suite baselines, or adjacent feature work, stop and split that work out of `287`.
- The canonical executable command set lives only in `spec.md`, `plan.md`, `tasks.md`, and `quickstart.md`; this artifact intentionally references that command authority without restating a second command set.

View File

@ -0,0 +1,269 @@
# Feature Specification: Cutover Prerequisite Completion
**Feature Branch**: `287-cutover-prerequisite-completion`
**Created**: 2026-05-10
**Status**: Ready
**Input**: User description: "We need to re-scope the active Spec 287 so it completes the remaining runtime and test-harness prerequisites that currently block Quality Gates / No-Legacy Enforcement."
## Spec Candidate Check *(mandatory - SPEC-GATE-001)*
- **Problem**: The reserved cutover hardening slot cannot honestly become the quality-gates / no-legacy package yet because the branch still contains unfinished runtime and test-harness seams. Repo truth still shows legacy provider-connection route aliases, Microsoft-shaped provider target-scope core fields, environment-scope membership persistence that still mirrors workspace roles, and tenant-panel test helpers that boot the retired panel in test context.
- **Today's failure**: `apps/platform/routes/web.php` still serves `/admin/tenants/{tenant:slug}/provider-connections` redirect routes, provider-core seams still rely on Microsoft-shaped identity and target-scope fields such as `entra_tenant_id` and related `tenantContext` payloads, shared access cleanup is incomplete while `ManagedEnvironmentMembership` persistence still carries copied role values, and `apps/platform/tests/Pest.php` plus targeted test files still depend on `setTenantPanelContext()` and other tenant-panel-era helpers. If `287` stayed a guard package, it would either fail immediately for the wrong reason or silently absorb unfinished runtime work anyway.
- **User-visible improvement**: Operators keep the already-approved workspace-first, provider-neutral admin baseline because the remaining prerequisite seams are completed directly. After this slice, Spec `288` can focus purely on quality gates and no-legacy enforcement instead of finishing live runtime behavior.
- **Smallest enterprise-capable version**: complete only the remaining prerequisite seams: retire the legacy provider-connection route family, finish provider target-scope core neutralization on the shared provider seams, clean environment-scope role persistence so workspace membership remains the only role-bearing truth, replace tenant-panel test helpers with admin/workspace context helpers, and prove those seams with targeted feature and browser validation only. Do not add a global guard suite, a full-suite baseline, a new product workflow, or a new abstraction framework.
- **Explicit non-goals**: no no-legacy guard suite, no full-suite baseline, no global quality gates, no package execution, no guided operations, no UI copy cleanup, no provider capability expansion, no new provider implementation, no new panel, no new migration-only compatibility shim, and no broad repo-wide wording sweep.
- **Permanent complexity imported**: one bounded runtime seam inventory, a small targeted test-support helper replacement, focused regression tests for the changed seams, and one explicit handoff to Spec `288`. No new persisted truth, no new role family, and no new governance or lint subsystem are introduced.
- **Why now**: the user has explicitly re-scoped Spec `287` so the remaining prerequisites land before the follow-up enforcement package. Without this step, Spec `288` would either be blocked or would be forced to violate scope by finishing runtime work under the label of quality gates.
- **Why not local**: the remaining drift is not isolated to one page or one class. It spans route ownership, shared provider-core contracts, environment-scope persistence semantics, and the common test harness. A local patch would leave the cutover baseline inconsistent and would not give Spec `288` a stable runtime foundation.
- **Approval class**: Cleanup
- **Red flags triggered**: cross-cutting runtime seam completion, shared provider/platform boundary cleanup, and test-harness migration risk. Defense: the slice is bounded to the five named prerequisite areas, explicitly forbids global guard work and adjacent product features, and hands enforcement to Spec `288`.
- **Score**: Nutzen: 2 | Dringlichkeit: 2 | Scope: 2 | Komplexitaet: 1 | Produktnaehe: 1 | Wiederverwendung: 2 | **Gesamt: 10/12**
- **Decision**: approve
## Review Outcome
- **Outcome class**: acceptable-special-case
- **Workflow outcome**: keep
- **Test-governance outcome**: keep
- **Reason**: the package is cross-cutting, but it remains implementation-ready because it completes only explicit prerequisite seams and keeps proof scoped to targeted validation rather than a global enforcement layer.
- **Workflow result**: Ready for implementation as the last runtime/test-harness completion slice before Spec `288`.
## Spec Scope Fields *(mandatory)*
- **Scope**: workspace
- **Primary Routes**:
- `/admin/provider-connections`
- `/admin/provider-connections/create`
- `/admin/provider-connections/{record}`
- `/admin/provider-connections/{record}/edit`
- legacy provider-connection aliases currently mounted below `/admin/tenants/{tenant:slug}/provider-connections`, which are in scope for retirement
- existing workspace-scoped managed-environment pages or launch points that deep-link into provider connections or provider-backed run follow-up
- **Data Ownership**:
- `ProviderConnection` remains the existing workspace-owned, managed-environment-scoped provider binding record
- provider target-scope and identity outputs remain derived shared contracts; Microsoft-specific profile data stays provider-owned nested metadata only where current-release consent or diagnostics actually need it
- `workspace_memberships` remain the only role-bearing access truth
- `managed_environment_memberships` or their in-place successor remain a narrowing-only access-scope overlay and must not persist an independent role-bearing truth
- no new persisted entity, table, or ledger is introduced
- **RBAC**:
- workspace membership remains the first entitlement boundary and the only role-bearing authority
- managed-environment scope may narrow visibility only
- non-members or out-of-scope actors remain `404`
- in-scope actors missing capability remain `403`
## Cross-Cutting / Shared Pattern Reuse *(mandatory when the feature touches notifications, status messaging, action links, header actions, dashboard signals/cards, navigation entry points, alerts, evidence/report viewers, or any other existing shared operator interaction family; otherwise write `N/A - no shared interaction family touched`)*
- **Cross-cutting feature?**: yes
- **Interaction class(es)**: provider-connection navigation entry points, provider target-scope summaries, provider-backed run context, workspace-first access evaluation, and shared test-harness context setup
- **Systems touched**:
- `apps/platform/routes/web.php`
- `apps/platform/app/Services/Providers/ProviderConnectionResolver.php`
- `apps/platform/app/Services/Providers/ProviderIdentityResolver.php`
- `apps/platform/app/Services/Providers/ProviderIdentityResolution.php`
- `apps/platform/app/Services/Providers/PlatformProviderIdentityResolver.php`
- `apps/platform/app/Services/Providers/ProviderOperationStartGate.php`
- `apps/platform/app/Support/Providers/TargetScope/ProviderConnectionTargetScopeNormalizer.php`
- `apps/platform/app/Support/Providers/TargetScope/ProviderConnectionTargetScopeDescriptor.php`
- `apps/platform/app/Services/Auth/TenantMembershipManager.php`
- `apps/platform/app/Services/Auth/ManagedEnvironmentAccessScopeResolver.php`
- `apps/platform/tests/Pest.php`
- targeted provider-connection, auth, RBAC, and browser tests that still depend on the old route or panel helper seams
- **Existing pattern(s) to extend**: current workspace-first provider-connection routing, existing target-scope normalizer and descriptor path, existing workspace-first access resolver path, and current shared test-support helper style in `tests/Pest.php`
- **Shared contract / presenter / builder / renderer to reuse**: `ProviderConnectionTargetScopeNormalizer`, `ProviderConnectionTargetScopeDescriptor`, `ProviderIdentityResolution`, `ProviderOperationStartGate`, existing workspace access resolver seams, and `setAdminPanelContext()`-style test support
- **Why the existing shared path is sufficient or insufficient**: the repo already has the right shared seams, but those seams still carry incomplete cutover behavior. Extending them is sufficient; building a new guard framework or a second helper layer would be wider than necessary.
- **Allowed deviation and why**: bounded provider-owned Microsoft profile detail may remain where current consent or diagnostics genuinely need it. No other deviation is allowed.
- **Consistency impact**: provider-connection routes, provider-core target-scope outputs, workspace-first access resolution, and test harness setup must all describe the same post-cutover truth so later enforcement can be declarative instead of compensating for runtime drift.
- **Review focus**: reviewers must verify that `287` completes only the named runtime and test-harness seams, does not introduce a global guard family, and leaves Spec `288` as the explicit follow-up for no-legacy enforcement.
## OperationRun UX Impact *(mandatory when the feature creates, queues, deduplicates, resumes, blocks, completes, or deep-links to an `OperationRun`; otherwise write `N/A - no OperationRun start or link semantics touched`)*
- **Touches OperationRun start/completion/link UX?**: yes
- **Shared OperationRun UX contract/layer reused**: `ProviderOperationStartGate`, existing `OperationRun` context resolution, and canonical operation links
- **Delegated start/completion UX behaviors**: existing queued, blocked, and run-link behavior remains on the shared provider-operation and OperationRun paths; this slice only completes the underlying target-scope and route truth they depend on
- **Local surface-owned behavior that remains**: provider-connection surfaces and existing launch points still own only the initiation inputs and local follow-up affordances
- **Queued DB-notification policy**: `N/A` - unchanged
- **Terminal notification path**: existing central lifecycle mechanism
- **Exception required?**: none
## Provider Boundary / Platform Core Check *(mandatory when the feature changes shared provider/platform seams, identity scope, governed-subject taxonomy, compare strategy selection, provider connection descriptors, or operator vocabulary that may leak provider-specific semantics into platform-core truth; otherwise write `N/A - no shared provider/platform boundary touched`)*
- **Shared provider/platform boundary touched?**: yes
- **Boundary classification**: mixed
- **Seams affected**: provider-connection route ownership, provider target-scope descriptors, provider identity resolution payloads, provider-backed run context, and managed-environment scoped provider launch surfaces
- **Neutral platform terms preserved or introduced**: `provider connection`, `target scope`, `scope kind`, `scope identifier`, `scope display name`, `provider profile`, `workspace`, and `managed environment`
- **Provider-specific semantics retained and why**: Microsoft tenant/profile identifiers, consent links, and Graph-specific diagnostics may remain nested provider-owned data because current-release support and consent workflows still require them
- **Why this does not deepen provider coupling accidentally**: the slice removes Microsoft-shaped assumptions from the shared platform-core contract and leaves provider detail only in explicitly provider-owned metadata
- **Follow-up path**: Spec `288` owns quality gates and no-legacy enforcement once these runtime seams are complete
## UI / Surface Guardrail Impact *(mandatory when operator-facing surfaces are changed; otherwise write `N/A`)*
| Surface / Change | Operator-facing surface change? | Native vs Custom | Shared-Family Relevance | State Layers Touched | Exception Needed? | Low-Impact / `N/A` Note |
|---|---|---|---|---|---|---|
| Provider connections route and launch path completion | yes | Native Filament resource plus existing route helpers | navigation entry points, record links, launch flow continuity | route, page, detail, URL/query | no | existing resource family only; no new operator-facing feature |
| Shared provider target-scope summaries and provider-backed run context | yes | Existing shared presenter or resolver path | provider summaries, operation launch context, supporting disclosure | page, modal, detail, run context | no | summary contract cleanup only; no UI copy expansion |
| Test-harness helper cutover | no | N/A | none | test support only | no | `N/A - repository workflow only` |
## Decision-First Surface Role *(mandatory when operator-facing surfaces are changed)*
| 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 |
|---|---|---|---|---|---|---|---|
| Provider connections route and launch path completion | Primary Decision Surface | Operator decides whether to open, manage, or launch from an existing provider connection | canonical provider-connection route, current managed-environment context, and existing record truth | full provider detail and downstream operation detail remain on existing surfaces | Primary because the route contract must stop forcing operators through legacy aliases | preserves the existing provider-connection workflow while removing route ambiguity | removes fallback redirects and stale path language from the live flow |
| Shared provider target-scope summaries and provider-backed run context | Secondary Context Surface | Operator confirms the connection or launch target before acting | neutral target-scope summary and current surface context | nested provider-owned profile detail and run evidence remain secondary | Secondary because it supports an existing decision surface rather than creating a new one | keeps current launch and summary flows intact while correcting the shared contract | removes the need to infer platform meaning from Microsoft-only fields |
## Audience-Aware Disclosure *(mandatory when operator-facing surfaces are changed)*
| 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 |
|---|---|---|---|---|---|---|---|
| Provider connections route and launch path completion | operator-MSP, support-platform | canonical provider-connection route and current environment context | existing provider status and authorization detail | nested provider-owned profile detail and raw diagnostics | `Open provider connection` or the existing primary launch action | raw/provider-owned detail remains secondary | the route and summary state the canonical scope once and do not add a second fallback path |
| Shared provider target-scope summaries and provider-backed run context | operator-MSP, support-platform | neutral target-scope summary and current launch scope | launch blockers and provider-operation follow-up stay on shared existing surfaces | provider-owned raw identifiers stay nested | existing primary launch or follow-up action | raw/provider-owned identifiers remain secondary | one shared target-scope summary is reused instead of page-local reinterpretations |
## UI/UX Surface Classification *(mandatory when operator-facing surfaces are changed)*
| 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 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Provider connections route and launch path completion | List / Detail / Integrations | CRUD / List-first Resource | Open the canonical provider-connection record or start the existing provider workflow | full-row open to the existing provider-connection detail surface | required | existing grouped secondary actions only | unchanged existing destructive actions only | `/admin/provider-connections` | `/admin/provider-connections/{record}` | workspace and managed-environment context | Provider connection | canonical route and current target scope | none |
| Shared provider target-scope summaries and provider-backed run context | Record / Detail / Action | Action-supporting detail | Confirm scope and continue on the existing provider flow | existing summary and launch affordance | n/a | existing secondary provider detail remains nested | none introduced by this slice | inherited current collection route | inherited current detail route | current workspace, current managed environment, target scope | Target scope | neutral shared scope summary | provider-owned nested detail only |
## Operator Surface Contract *(mandatory when operator-facing surfaces are changed)*
| 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 |
|---|---|---|---|---|---|---|---|---|---|---|
| Provider connections route and launch path completion | Workspace operator | Decide whether to inspect or use an existing provider connection on the canonical route | Existing provider-connection list and detail surfaces | Am I on the canonical provider-connection path for this environment and can I continue safely? | current environment context, canonical route, and existing provider-connection truth | provider-owned profile detail and lower-level diagnostics | lifecycle, authorization, target-scope truth | existing provider connection only | existing open, manage, and launch actions | unchanged existing dangerous actions only |
| Shared provider target-scope summaries and provider-backed run context | Workspace operator | Decide whether the current connection or launch target is the right scope | Existing launch and supporting summary surfaces | What exact target scope will this action use? | neutral shared target-scope summary and current managed-environment context | provider-owned identifiers and nested run detail | target-scope truth, launch readiness | none beyond the existing provider-backed mutation | existing launch action | none added by this slice |
## Proportionality Review *(mandatory when structural complexity is introduced)*
- **New source of truth?**: no
- **New persisted entity/table/artifact?**: no
- **New abstraction?**: no
- **New enum/state/reason family?**: no
- **New cross-domain UI framework/taxonomy?**: no
- **Current operator problem**: the quality-gates follow-up is blocked because the live runtime and test harness still carry unfinished cutover seams that enforcement should not have to complete.
- **Existing structure is insufficient because**: the repo already has the intended workspace-first and provider-neutral direction, but the remaining seams still encode old route aliases, Microsoft-shaped shared target-scope truth, copied environment-scope roles, and tenant-panel helper assumptions.
- **Narrowest correct implementation**: complete only the five named prerequisite seams and validate them with focused tests instead of introducing a new no-legacy framework or widening into adjacent product work.
- **Ownership cost**: bounded runtime seam cleanup, targeted helper migration, and focused regression coverage.
- **Alternative intentionally rejected**: leaving the runtime seams in place and moving straight to a guard suite. That would force the enforcement package to encode unfinished behavior instead of guarding the final truth.
- **Release truth**: current-release truth
### Compatibility posture
This feature assumes a pre-production environment.
Backward compatibility, legacy aliases kept for convenience, broad migration shims, and compatibility-specific test lanes are out of scope.
Canonical replacement is preferred over preservation.
## Testing / Lane / Runtime Impact *(mandatory for runtime behavior changes)*
- **Test purpose / classification**: Feature, Browser
- **Validation lane(s)**: fast-feedback, confidence, browser
- **Why this classification and these lanes are sufficient**: the package completes concrete runtime seams and the shared test harness, so focused feature coverage plus a narrow browser smoke are sufficient. A guard lane or full-suite baseline would exceed scope.
- **New or expanded test families**: targeted provider-connection route and scope tests, targeted workspace-first access persistence tests, targeted helper-cutover coverage, and one narrow browser smoke proving the canonical provider-connection launch path still works after helper and route completion
- **Fixture / helper cost impact**: moderate. The main cost is replacing the tenant-panel helper path with a post-cutover admin/workspace helper and updating the proof-command consumer tests `tests/Feature/Reviews/CustomerReviewWorkspaceLaunchLinksTest.php` and `tests/Feature/Rbac/TriageReviewStateAuthorizationTest.php`.
- **Heavy-family visibility / justification**: one narrow browser smoke only. No heavy-governance or broad browser family is justified.
- **Special surface test profile**: standard-native-filament, global-context-shell
- **Standard-native relief or required special coverage**: ordinary feature coverage is sufficient for most seams; one narrow browser smoke is required to prove the canonical provider-connection route and launch flow after the helper cutover
- **Reviewer handoff**: reviewers must verify that Filament remains v5 on Livewire v4, provider registration stays in `apps/platform/bootstrap/providers.php`, any touched provider-connection destructive actions retain confirmation and server authorization, no asset registration or deployment step is introduced, the proof commands stay targeted to the changed seams, and no new global guard family or full-suite baseline is introduced
- **Budget / baseline / trend impact**: contained feature-local increase only
- **Escalation needed**: document-in-feature
- **Active feature PR close-out entry**: RuntimePrerequisite
- **Planned validation commands**:
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && REPO_ROOT="$(git rev-parse --show-toplevel)" && (cd "$REPO_ROOT/apps/platform" && ./vendor/bin/sail artisan test --compact tests/Feature/ProviderConnections/LegacyRedirectTest.php tests/Feature/ProviderConnections/TenantlessListRouteTest.php tests/Feature/ProviderConnections/TenantlessListScopingTest.php tests/Feature/Auth/WorkspaceFirstManagedEnvironmentAccessTest.php tests/Feature/Rbac/ProviderConnectionWorkspaceFirstPolicyTest.php tests/Feature/Reviews/CustomerReviewWorkspaceLaunchLinksTest.php tests/Feature/Rbac/TriageReviewStateAuthorizationTest.php)`
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && REPO_ROOT="$(git rev-parse --show-toplevel)" && (cd "$REPO_ROOT/apps/platform" && ./vendor/bin/sail artisan test --compact tests/Browser/Spec281ProviderConnectionScopeSmokeTest.php tests/Browser/Spec285WorkspaceRbacEnvironmentAccessSmokeTest.php)`
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && REPO_ROOT="$(git rev-parse --show-toplevel)" && (cd "$REPO_ROOT/apps/platform" && ./vendor/bin/sail bin pint --dirty --format agent)`
## User Scenarios & Testing *(mandatory)*
### User Story 1 - Retire provider-connection legacy routes (Priority: P1)
As an operator, I want provider-connection entry points to resolve only through the canonical admin route family so the live flow no longer depends on legacy tenant-first redirects.
**Why this priority**: route retirement is the most visible unfinished prerequisite and it blocks later enforcement from distinguishing real regressions from known legacy fallbacks.
**Independent Test**: Can be fully tested by hitting the canonical provider-connection routes, the retired legacy aliases, and the current launch-point inventory in `apps/platform/app/Providers/Filament/AdminPanelProvider.php`, `apps/platform/app/Filament/Resources/TenantResource.php`, `apps/platform/app/Filament/Pages/TenantRequiredPermissions.php`, `apps/platform/app/Support/OperationRunLinks.php`, `apps/platform/app/Support/Providers/ProviderReasonTranslator.php`, and `apps/platform/app/Support/Verification/VerificationLinkBehavior.php`, then proving only the canonical path remains valid.
**Acceptance Scenarios**:
1. **Given** an operator opens a provider-connection launch point after the cutover, **When** the route resolves, **Then** it lands on the canonical `/admin/provider-connections...` path instead of a legacy tenant alias.
2. **Given** a legacy `/admin/tenants/{tenant:slug}/provider-connections` alias is requested after completion, **When** the request executes, **Then** it no longer acts as an accepted runtime fallback.
---
### User Story 2 - Neutralize provider target-scope core seams (Priority: P1)
As an operator and reviewer, I want shared provider target-scope and identity contracts to use platform-neutral fields so provider-backed runtime and follow-up do not depend on Microsoft-shaped core truth.
**Why this priority**: this is the runtime provider-boundary prerequisite that must be complete before a no-legacy guard package can enforce it.
**Independent Test**: Can be fully tested by exercising the provider target-scope descriptor, the shared provider-backed launch or run context, and the existing provider-connection summary surfaces without relying on Microsoft-only shared fields.
**Acceptance Scenarios**:
1. **Given** a provider connection is summarized on a shared surface, **When** the target scope is rendered, **Then** the shared contract uses neutral target-scope fields while provider-owned Microsoft detail stays nested.
2. **Given** a provider-backed run or start gate uses provider context, **When** it records the shared scope payload, **Then** it does not require Microsoft-only core keys such as `target_scope.entra_tenant_id` to express the shared truth.
---
### User Story 3 - Clean environment-scope role persistence (Priority: P1)
As a workspace owner or reviewer, I want environment scope persistence to narrow access only so workspace membership remains the single role-bearing authority.
**Why this priority**: workspace-first RBAC cannot be treated as complete while environment-scope persistence still mirrors or persists a second role-bearing truth.
**Independent Test**: Can be fully tested by creating workspace membership plus managed-environment scope combinations and proving authorization still derives role authority from workspace membership while environment scope only narrows visibility.
**Acceptance Scenarios**:
1. **Given** a workspace member is granted managed-environment scope, **When** the scope is persisted, **Then** it does not establish an independent role-bearing truth separate from workspace membership.
2. **Given** a workspace member loses access to one environment, **When** a scoped surface is resolved, **Then** the system treats that as a narrowing-only access outcome rather than a second role matrix.
---
### User Story 4 - Cut over tenant-panel test helpers (Priority: P2)
As a maintainer, I want the shared test harness to stop booting the retired tenant panel so targeted seam tests can prove the post-cutover runtime honestly.
**Why this priority**: the helper cutover is necessary to validate the runtime changes without relying on a panel context that the cutover is removing.
**Independent Test**: Can be fully tested by updating the shared helper, `apps/platform/tests/Feature/Reviews/CustomerReviewWorkspaceLaunchLinksTest.php`, and `apps/platform/tests/Feature/Rbac/TriageReviewStateAuthorizationTest.php`, then running the targeted seam tests and browser smoke without `setTenantPanelContext()`.
**Acceptance Scenarios**:
1. **Given** `CustomerReviewWorkspaceLaunchLinksTest.php` or `TriageReviewStateAuthorizationTest.php` needs managed-environment context after the cutover, **When** it boots test state, **Then** it uses the post-cutover admin/workspace helper path instead of `setTenantPanelContext()`.
2. **Given** the targeted browser or feature validation runs, **When** it exercises the changed seams, **Then** it passes without reintroducing the retired tenant panel as a test-only crutch.
### Edge Cases
- What happens when a legacy provider-connection alias is requested by a stale bookmark immediately after the canonical route retirement?
- How does the shared provider target-scope contract preserve provider-owned Microsoft profile detail without letting it leak back into platform-core truth?
- What happens when a workspace member remains entitled to the workspace but is narrowed away from one managed environment after persistence cleanup?
- How do targeted tests that still need managed-environment context keep working after the tenant-panel helper is removed?
## Requirements *(mandatory)*
**Constitution alignment (required):** This slice introduces no new Graph integration surface, no new queue workflow, no new persisted entity, and no new operator product flow. It completes the remaining runtime and test-harness seams that must be true before a separate enforcement spec can guard them.
**Constitution alignment (PROP-001 / ABSTR-001 / PERSIST-001 / STATE-001 / BLOAT-001):** The feature must stay completion-focused. It may update existing route, provider-core, access-scope, and test-support seams, but it must not introduce a new guard framework, new persistence, a new role family, or a new taxonomy layer.
**Constitution alignment (XCUT-001 / PROV-001):** The package must reuse existing provider-connection routing, target-scope normalization, access resolution, and test-support paths. It may finish their cutover behavior, but it must not create a parallel provider-core or enforcement subsystem.
### Functional Requirements
- **FR-001**: The package MUST retire the legacy provider-connection route family currently mounted below `/admin/tenants/{tenant:slug}/provider-connections` and leave the canonical provider-connection runtime on `/admin/provider-connections...`.
- **FR-002**: Canonical provider-connection deep links and the current launch-point inventory in `apps/platform/app/Providers/Filament/AdminPanelProvider.php`, `apps/platform/app/Filament/Resources/TenantResource.php`, `apps/platform/app/Filament/Pages/TenantRequiredPermissions.php`, `apps/platform/app/Support/OperationRunLinks.php`, `apps/platform/app/Support/Providers/ProviderReasonTranslator.php`, and `apps/platform/app/Support/Verification/VerificationLinkBehavior.php` MUST resolve through the post-cutover admin route family only.
- **FR-003**: Shared provider target-scope and identity contracts MUST stop depending on Microsoft-only core keys such as `entra_tenant_id`, `tenantContext`, or `target_scope.entra_tenant_id` to express platform-core truth.
- **FR-004**: Provider-owned Microsoft profile or consent details MAY remain nested where current-release diagnostics or consent workflows need them, but they MUST NOT remain the shared primary target-scope contract.
- **FR-005**: Workspace membership MUST remain the only role-bearing authority on the completed seams.
- **FR-006**: Environment-scope persistence MUST narrow visibility only and MUST NOT persist or restore an independent role-bearing truth.
- **FR-007**: Shared services or managers that currently copy workspace role values into environment-scope persistence MUST be completed to the narrowing-only contract.
- **FR-008**: `apps/platform/tests/Pest.php` MUST replace tenant-panel-era setup with a post-cutover admin or workspace context helper path suitable for the changed seams.
- **FR-009**: The targeted proof-command consumers `apps/platform/tests/Feature/Reviews/CustomerReviewWorkspaceLaunchLinksTest.php` and `apps/platform/tests/Feature/Rbac/TriageReviewStateAuthorizationTest.php` that still depend on `setTenantPanelContext()` or equivalent retired tenant-panel setup MUST be cut over as part of this slice.
- **FR-010**: Validation in `287` MUST remain targeted to the changed seams only and MUST NOT become a global no-legacy guard suite or a full-suite baseline.
- **FR-011**: Spec `288` MUST remain the follow-up package for quality gates and no-legacy enforcement; `287` must not absorb that work.
### Non-Functional Requirements
- **NFR-001**: Filament remains v5 on Livewire v4, and provider registration remains in `apps/platform/bootstrap/providers.php`.
- **NFR-002**: The package introduces no new asset registration and no new deployment step.
- **NFR-003**: Proof remains bounded to targeted feature tests, targeted browser validation, and formatting. No full-suite baseline is required in this slice.
- **NFR-004**: No new global guard family or repository-wide lint subsystem may be introduced under this spec.
- **NFR-005**: The package must remain reviewable as one bounded prerequisite-completion slice that directly precedes the Spec `288` enforcement package.

View File

@ -0,0 +1,224 @@
---
description: "Task list for Cutover Prerequisite Completion"
---
# Tasks: Cutover Prerequisite Completion
**Input**: Design documents from `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/287-cutover-prerequisite-completion/`
**Prerequisites**: `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/287-cutover-prerequisite-completion/plan.md` (required), `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/287-cutover-prerequisite-completion/spec.md` (required), `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/287-cutover-prerequisite-completion/checklists/requirements.md` (required), `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/287-cutover-prerequisite-completion/research.md`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/287-cutover-prerequisite-completion/data-model.md`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/287-cutover-prerequisite-completion/contracts/cutover-prerequisite-completion.logical.openapi.yaml`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/287-cutover-prerequisite-completion/quickstart.md`
**Review Artifact**: `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/287-cutover-prerequisite-completion/checklists/requirements.md` is the outcome-of-record for the review outcome class, workflow outcome, and test-governance outcome. If implementation expands into no-legacy guards, full-suite baselines, or adjacent feature work, update that artifact before continuing and stop when the work no longer fits `287`.
**Tests**: Required (Pest) for runtime and helper changes. Keep proof bounded to targeted `Feature` tests plus targeted `Browser` validation because this package completes prerequisite seams only.
**Operations**: No new `OperationRun`, queue family, remote workflow, or notification policy is introduced. `287` only completes the existing provider-backed run context and canonical route truth.
**RBAC**: Reuse the workspace-first access contract from Spec `285`; do not add a new role family, raw capability strings, or a second access overlay product.
**Shared Pattern Reuse**: Reuse `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/routes/web.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Support/Providers/TargetScope/ProviderConnectionTargetScopeNormalizer.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Support/Providers/TargetScope/ProviderConnectionTargetScopeDescriptor.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Services/Providers/ProviderIdentityResolver.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Services/Providers/ProviderIdentityResolution.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Services/Providers/PlatformProviderIdentityResolver.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Services/Providers/ProviderOperationStartGate.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Services/Auth/TenantMembershipManager.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Services/Auth/ManagedEnvironmentAccessScopeResolver.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/tests/Pest.php`, and the targeted feature/browser tests named below. Do not introduce a new guard subsystem or a full-suite wrapper under this spec.
**Filament / Panel Guardrails**: Filament remains v5 on Livewire v4. Provider registration remains unchanged in `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/bootstrap/providers.php`. No new panel, no new globally-searchable resource, and no asset-strategy change are allowed in this slice.
**Organization**: Tasks are grouped by the four runtime/test-harness prerequisite areas so route retirement, provider-core neutralization, access persistence cleanup, and helper cutover remain independently reviewable.
**Review Outcome**: `acceptable-special-case`
**Workflow Outcome**: `keep`
**Test-governance Outcome**: `keep`
## Test Governance Checklist
- [x] Lane assignment is named and is the narrowest sufficient proof for the changed behavior.
- [x] New or changed tests stay in targeted feature or browser coverage and do not become a guard family.
- [x] Shared helpers, fixtures, and context bootstrapping stay explicit and cheap by default.
- [x] Planned validation commands cover the changed seams without becoming a full-suite baseline.
- [x] Surface test profile stays explicit: `standard-native-filament` and `global-context-shell`.
- [x] The active package records that Spec `288` owns quality gates and no-legacy enforcement after this slice lands.
## Phase 1: Setup (Shared Context)
**Purpose**: Lock the bounded prerequisite-completion role, exact seam inventory, and targeted validation scope before runtime edits begin.
- [x] T001 Review `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/287-cutover-prerequisite-completion/spec.md`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/287-cutover-prerequisite-completion/plan.md`, and `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/287-cutover-prerequisite-completion/checklists/requirements.md` to confirm the package stays on prerequisite completion only
- [x] T002 [P] Review `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/287-cutover-prerequisite-completion/research.md`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/287-cutover-prerequisite-completion/data-model.md`, and `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/287-cutover-prerequisite-completion/contracts/cutover-prerequisite-completion.logical.openapi.yaml` to confirm the same seam categories, canonical replacements, and follow-up boundary to Spec `288` are pinned everywhere
- [x] T003 [P] Confirm the focused Sail/Pest validation commands in `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/287-cutover-prerequisite-completion/quickstart.md` and the current `apps/platform/tests/Feature/ProviderConnections/`, `apps/platform/tests/Feature/Auth/`, `apps/platform/tests/Feature/Rbac/`, and `apps/platform/tests/Browser/` directories
---
## Phase 2: Foundational (Blocking Prerequisites)
**Purpose**: Fix the exact completion inventory before user-story work begins and keep Spec `288` explicitly out of scope.
**Critical**: No user-story work should begin until this phase is complete.
- [x] T004 Audit the exact provider-connection legacy route seams across `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/routes/web.php` and the current launch-point inventory in `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Providers/Filament/AdminPanelProvider.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Resources/TenantResource.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Pages/TenantRequiredPermissions.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Support/OperationRunLinks.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Support/Providers/ProviderReasonTranslator.php`, and `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Support/Verification/VerificationLinkBehavior.php` so `287` retires only repo-real fallback paths
- [x] T005 [P] Audit the provider target-scope core seams across `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Services/Providers/ProviderConnectionResolver.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Services/Providers/ProviderIdentityResolver.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Services/Providers/ProviderIdentityResolution.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Services/Providers/PlatformProviderIdentityResolver.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Services/Providers/ProviderOperationStartGate.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Support/Providers/TargetScope/ProviderConnectionTargetScopeNormalizer.php`, and `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Support/Providers/TargetScope/ProviderConnectionTargetScopeDescriptor.php`
- [x] T006 [P] Audit the environment-scope role persistence and tenant-panel test-helper seams across `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Services/Auth/TenantMembershipManager.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Services/Auth/ManagedEnvironmentAccessScopeResolver.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/tests/Pest.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/tests/Feature/Reviews/CustomerReviewWorkspaceLaunchLinksTest.php`, and `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/tests/Feature/Rbac/TriageReviewStateAuthorizationTest.php`
- [x] T007 Confirm the scope boundary to Spec `288` remains explicit in the artifact package and that no guard-suite, full-suite, UI copy, package execution, guided operations, or provider-capability work is added here
**Checkpoint**: the runtime seam inventory and validation boundary are fixed before story work begins.
---
## Phase 3: User Story 1 - Retire provider-connection legacy routes (Priority: P1)
**Goal**: Make canonical provider-connection routing the only accepted runtime path.
**Independent Test**: hit the canonical provider-connection route family, retire the legacy alias family, and prove the launch-point inventory in `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Providers/Filament/AdminPanelProvider.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Resources/TenantResource.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Pages/TenantRequiredPermissions.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Support/OperationRunLinks.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Support/Providers/ProviderReasonTranslator.php`, and `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Support/Verification/VerificationLinkBehavior.php` resolves only through the canonical path.
### Tests for User Story 1
- [x] T008 [P] [US1] Extend `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/tests/Feature/ProviderConnections/LegacyRedirectTest.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/tests/Feature/ProviderConnections/TenantlessListRouteTest.php`, and `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/tests/Feature/ProviderConnections/TenantlessListScopingTest.php` so the canonical provider-connection route family is explicit
### Implementation for User Story 1
- [x] T009 [US1] Remove the remaining legacy provider-connection route family from `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/routes/web.php`
- [x] T010 [US1] Update the provider-connection launch-point builders in `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Providers/Filament/AdminPanelProvider.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Resources/TenantResource.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Pages/TenantRequiredPermissions.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Support/OperationRunLinks.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Support/Providers/ProviderReasonTranslator.php`, and `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Support/Verification/VerificationLinkBehavior.php` so they resolve through the canonical admin route family only
**Checkpoint**: User Story 1 is independently functional when legacy provider-connection aliases are gone and canonical routes are the only runtime path.
---
## Phase 4: User Story 2 - Neutralize provider target-scope core seams (Priority: P1)
**Goal**: Keep the shared provider-core contract provider-neutral while leaving Microsoft detail nested under provider-owned seams.
**Independent Test**: exercise the provider target-scope descriptor, shared provider identity path, and shared provider-backed run context without depending on Microsoft-only shared keys.
### Tests for User Story 2
- [x] T011 [P] [US2] Add or extend targeted provider-connection and provider-core tests under `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/tests/Feature/ProviderConnections/` so shared target-scope and identity outputs stop depending on Microsoft-only core keys
### Implementation for User Story 2
- [x] T012 [US2] Complete target-scope neutralization in `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Support/Providers/TargetScope/ProviderConnectionTargetScopeNormalizer.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Support/Providers/TargetScope/ProviderConnectionTargetScopeDescriptor.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Services/Providers/ProviderConnectionResolver.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Services/Providers/ProviderIdentityResolver.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Services/Providers/ProviderIdentityResolution.php`, and `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Services/Providers/PlatformProviderIdentityResolver.php`
- [x] T013 [US2] Update `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Services/Providers/ProviderOperationStartGate.php` and any directly affected shared provider context payloads so provider-backed run context uses the completed neutral contract while preserving provider-owned nested detail where needed
**Checkpoint**: User Story 2 is independently functional when shared provider target-scope and identity seams no longer depend on Microsoft-only core truth.
---
## Phase 5: User Story 3 - Clean environment-scope role persistence (Priority: P1)
**Goal**: Make workspace membership the only role-bearing truth and keep environment scope narrowing-only on the completed seams.
**Independent Test**: create workspace membership plus managed-environment scope combinations and prove authorization still derives role authority from workspace membership while environment scope narrows visibility only.
### Tests for User Story 3
- [x] T014 [P] [US3] Extend `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/tests/Feature/Auth/WorkspaceFirstManagedEnvironmentAccessTest.php` and `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/tests/Feature/Rbac/ProviderConnectionWorkspaceFirstPolicyTest.php` so copied role-bearing environment-scope persistence is no longer accepted on the changed seams
### Implementation for User Story 3
- [x] T015 [US3] Complete the narrowing-only access-scope cleanup in `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Services/Auth/TenantMembershipManager.php` and any directly affected access-scope resolver seam so workspace role truth is no longer mirrored as environment-scope role persistence
**Checkpoint**: User Story 3 is independently functional when workspace membership remains role-bearing and environment scope only narrows access on the completed seams.
---
## Phase 6: User Story 4 - Cut over tenant-panel test helpers (Priority: P2)
**Goal**: Remove the retired tenant-panel helper dependency from the shared test harness and the in-slice proof-command consumer tests.
**Independent Test**: replace `setTenantPanelContext()` on the shared helper path, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/tests/Feature/Reviews/CustomerReviewWorkspaceLaunchLinksTest.php`, and `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/tests/Feature/Rbac/TriageReviewStateAuthorizationTest.php`, then rerun the targeted seam validation without the retired panel context.
**Critical order**: complete T017 before T016. T016 is parallelizable only across the listed consumer files once the replacement helper exists.
### Tests for User Story 4
- [x] T016 [P] [US4] After T017 introduces the replacement helper, update `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/tests/Feature/Reviews/CustomerReviewWorkspaceLaunchLinksTest.php` and `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/tests/Feature/Rbac/TriageReviewStateAuthorizationTest.php` to prove the changed seams no longer require `setTenantPanelContext()`
### Implementation for User Story 4
- [x] T017 [US4] Replace the shared tenant-panel helper path in `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/tests/Pest.php` with a post-cutover admin or workspace context helper suitable for the changed seams
**Checkpoint**: User Story 4 is independently functional when the named targeted seam tests run without the retired tenant-panel helper.
---
## Phase 7: Polish & Cross-Cutting Validation
**Purpose**: Run the canonical targeted proof commands, format touched files, and keep Spec `288` as the explicit follow-up.
- [x] T018 Run `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && REPO_ROOT="$(git rev-parse --show-toplevel)" && (cd "$REPO_ROOT/apps/platform" && ./vendor/bin/sail artisan test --compact tests/Feature/ProviderConnections/LegacyRedirectTest.php tests/Feature/ProviderConnections/TenantlessListRouteTest.php tests/Feature/ProviderConnections/TenantlessListScopingTest.php tests/Feature/Auth/WorkspaceFirstManagedEnvironmentAccessTest.php tests/Feature/Rbac/ProviderConnectionWorkspaceFirstPolicyTest.php tests/Feature/Reviews/CustomerReviewWorkspaceLaunchLinksTest.php tests/Feature/Rbac/TriageReviewStateAuthorizationTest.php)` exactly as recorded in `spec.md`, `plan.md`, and `quickstart.md`
- [x] T019 Run `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && REPO_ROOT="$(git rev-parse --show-toplevel)" && (cd "$REPO_ROOT/apps/platform" && ./vendor/bin/sail artisan test --compact tests/Browser/Spec281ProviderConnectionScopeSmokeTest.php tests/Browser/Spec285WorkspaceRbacEnvironmentAccessSmokeTest.php)` exactly as recorded in `spec.md`, `plan.md`, and `quickstart.md`
- [x] T020 Run `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && REPO_ROOT="$(git rev-parse --show-toplevel)" && (cd "$REPO_ROOT/apps/platform" && ./vendor/bin/sail bin pint --dirty --format agent)`
- [x] T021 Review the touched runtime seams, helper updates, and the review artifact to confirm Filament remains on Livewire v4, provider registration still lives in `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/bootstrap/providers.php`, no asset registration or deployment-step drift was introduced, no guard suite, full-suite baseline, UI copy cleanup, package execution, guided operations, or provider capability expansion was absorbed, and Spec `288` remains the explicit follow-up
---
## Dependencies & Execution Order
### Phase Dependencies
- **Phase 1 (Setup)**: no dependencies; start immediately.
- **Phase 2 (Foundational)**: depends on Phase 1 and blocks all user-story work until the seam inventory and scope boundary are settled.
- **Phase 3 (US1)**: depends on Phase 2 and delivers the first independent prerequisite slice.
- **Phase 4 (US2)**: depends on Phase 2 and should follow US1 because provider route truth should settle before provider-core target-scope cleanup is validated through those surfaces.
- **Phase 5 (US3)**: depends on Phase 2 and should follow US2 because access persistence cleanup should consume the final provider-core and route baseline.
- **Phase 6 (US4)**: depends on Phases 3 through 5 and should follow them because the helper cutover must reflect the completed runtime seams.
- **Phase 7 (Polish)**: depends on all implemented stories.
### User Story Dependencies
- **US1 (P1)**: first independently testable increment once the seam inventory is settled.
- **US2 (P1)**: independently testable after Phase 2, but safer after US1 because provider summaries and launch points should already use the canonical route family.
- **US3 (P1)**: independently testable after Phase 2, but should merge after US2 because access persistence should validate against the completed provider-core truth.
- **US4 (P2)**: independently testable after Phase 2, but should merge after US1-US3 because the helper cutover must support the final runtime baseline rather than a moving target.
### Within Each User Story
- Extend or add the targeted tests first and make the current drift visible, unless the story defines an explicit helper-prerequisite step such as US4's T017 before T016.
- Complete the minimum runtime seam needed for that story.
- Re-run the narrowest relevant validation command after each story checkpoint before moving on.
---
## Parallel Execution Examples
### Phase 1
- T002 and T003 can run in parallel after T001 confirms the bounded package role.
### Phase 2
- T004, T005, and T006 can run in parallel because they inspect different seam families.
### User Story 1
- T008 can run while T009 and T010 are being prepared, but the runtime route cleanup should land as one coherent slice.
### User Story 2
- T011 can run in parallel with the seam audit, but T012 and T013 should land together because they define one shared provider-core contract.
### User Story 4
- T016 can run in parallel across `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/tests/Feature/Reviews/CustomerReviewWorkspaceLaunchLinksTest.php` and `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/tests/Feature/Rbac/TriageReviewStateAuthorizationTest.php` once T017 has defined the replacement helper path.
---
## Implementation Strategy
### Suggested MVP Scope
- MVP = **Phase 2 + US1**. The package starts delivering value once the legacy provider-connection route family is retired and the canonical path becomes real runtime truth.
### Incremental Delivery
1. Complete Phase 1 and Phase 2.
2. Deliver US1 and validate canonical route retirement.
3. Deliver US2 and validate provider target-scope core neutralization.
4. Deliver US3 and validate environment-scope persistence cleanup.
5. Deliver US4 and validate the helper cutover.
6. Finish with Phase 7 targeted validation, formatting, and scope review.
### Team Strategy
1. Keep Spec `288` explicitly out of implementation commits for this slice.
2. Land provider route and provider-core cleanup before helper migration so the test-support change reflects final runtime truth.
3. Serialize merges around `routes/web.php`, provider-core services, and `tests/Pest.php` because those are likely conflict hotspots.
---
## Explicit Follow-Ups / Out of Scope
- no-legacy guard suite and quality gates, which move to Spec `288`
- any full-suite baseline or budget recalibration work
- package execution or guided operations
- UI copy cleanup from Spec `286`
- provider capability expansion from Spec `283`
- broader repo-wide `setTenantPanelContext()` migration beyond `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/tests/Feature/Reviews/CustomerReviewWorkspaceLaunchLinksTest.php` and `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/tests/Feature/Rbac/TriageReviewStateAuthorizationTest.php`