Compare commits

...

3 Commits

Author SHA1 Message Date
603d509b8f cleanup: retire dead transitional residue (#270)
Some checks failed
Main Confidence / confidence (push) Failing after 58s
## Summary
- remove deprecated baseline profile status alias constants and keep baseline lifecycle semantics on the canonical enum path
- retire the dead tenant app-status badge/default-fixture residue from the active runtime support path
- add the `234-dead-transitional-residue` spec, plan, research, data-model, quickstart, checklist, and task artifacts plus focused regression assertions

## Validation
- not rerun during this PR creation step

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #270
2026-04-23 16:54:48 +00:00
6fdd45fb02 feat: surface stale active operation runs (#269)
Some checks failed
Main Confidence / confidence (push) Failing after 53s
## Summary
- keep stale active operation runs visible in the tenant progress overlay and polling state
- align tenant and canonical operation surfaces around the shared stale-active presentation contract
- add Spec 233 artifacts and clean the promoted-candidate backlog entries

## Validation
- browser smoke: `/admin/t/18000000-0000-4000-8000-000000000180` -> stale dashboard CTA -> `/admin/operations?tenant_id=7&activeTab=active_stale_attention&problemClass=active_stale_attention` -> `/admin/operations/15`
- verified healthy vs likely-stale tenant cards, canonical stale list row, and canonical run detail consistency

## Notes
- local smoke fixture seeded with one fresh and one stale running `baseline_compare` operation for browser validation
- Pest suite was not re-run in this session before opening this PR

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #269
2026-04-23 15:10:06 +00:00
2bf53f6337 Enforce operation run link contract (#268)
Some checks failed
Main Confidence / confidence (push) Failing after 44s
## Summary
- enforce shared operation run link generation across admin and system surfaces
- add guard coverage to block new raw operation route bypasses outside explicit exceptions
- harden Filament theme asset resolution so stale or wrong-stack hot files fall back to built assets

## Testing
- export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent
- export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/OpsUx/CanonicalViewRunLinksTest.php tests/Feature/Monitoring/OperationsDashboardDrillthroughTest.php tests/Feature/Filament/RecentOperationsSummaryWidgetTest.php tests/Feature/Filament/InventoryCoverageRunContinuityTest.php tests/Feature/ReviewPack/ReviewPackResourceTest.php tests/Feature/144/CanonicalOperationViewerDeepLinkTrustTest.php tests/Feature/078/RelatedLinksOnDetailTest.php tests/Feature/RunAuthorizationTenantIsolationTest.php tests/Feature/System/Spec195/SystemDirectoryResidualSurfaceTest.php tests/Feature/System/Spec113/AuthorizationSemanticsTest.php tests/Feature/Guards/OperationRunLinkContractGuardTest.php tests/Unit/Filament/PanelThemeAssetTest.php

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #268
2026-04-23 13:09:53 +00:00
62 changed files with 4383 additions and 202 deletions

View File

@ -240,6 +240,12 @@ ## Active Technologies
- Filesystem only (`specs/226-astrodeck-inventory-planning/*`) (226-astrodeck-inventory-planning)
- PHP 8.4.15, Laravel 12, Filament v5, Livewire v4, Blade + `App\Models\Finding`, `App\Filament\Resources\FindingResource`, `App\Services\Findings\FindingWorkflowService`, `App\Services\Baselines\BaselineAutoCloseService`, `App\Services\EntraAdminRoles\EntraAdminRolesFindingGenerator`, `App\Services\PermissionPosture\PermissionPostureFindingGenerator`, `App\Jobs\CompareBaselineToTenantJob`, `App\Filament\Pages\Reviews\ReviewRegister`, `App\Filament\Resources\TenantReviewResource`, `BadgeCatalog`, `BadgeRenderer`, `AuditLog` metadata via `AuditLogger` (231-finding-outcome-taxonomy)
- PostgreSQL via existing `findings`, `finding_exceptions`, `tenant_reviews`, `stored_reports`, and audit-log tables; no schema changes planned (231-finding-outcome-taxonomy)
- PHP 8.4.15, Laravel 12, Filament v5, Livewire v4 + Filament Resources/Pages/Widgets, Pest v4, `App\Support\OperationRunLinks`, `App\Support\System\SystemOperationRunLinks`, `App\Support\Navigation\CanonicalNavigationContext`, `App\Support\Navigation\RelatedNavigationResolver`, existing workspace and tenant authorization helpers (232-operation-run-link-contract)
- PostgreSQL-backed existing `operation_runs`, `tenants`, and `workspaces` records plus current session-backed canonical navigation state; no new persistence (232-operation-run-link-contract)
- PHP 8.4.15, Laravel 12, Filament v5, Livewire v4 + Filament widgets/resources/pages, Pest v4, `App\Models\OperationRun`, `App\Support\Operations\OperationRunFreshnessState`, `App\Services\Operations\OperationLifecycleReconciler`, `App\Support\OpsUx\OperationUxPresenter`, `App\Support\OpsUx\ActiveRuns`, `App\Support\Badges\BadgeCatalog` / `BadgeRenderer`, `App\Support\Workspaces\WorkspaceOverviewBuilder`, `App\Support\OperationRunLinks` (233-stale-run-visibility)
- Existing PostgreSQL `operation_runs` records and current session/query-backed monitoring navigation state; no new persistence (233-stale-run-visibility)
- PHP 8.4.15, Laravel 12, Filament v5, Livewire v4 + `App\Models\BaselineProfile`, `App\Support\Baselines\BaselineProfileStatus`, `App\Support\Badges\BadgeCatalog`, `App\Support\Badges\BadgeDomain`, `Database\Factories\TenantFactory`, `App\Console\Commands\SeedBackupHealthBrowserFixture`, existing tenant-truth and baseline-profile Pest tests (234-dead-transitional-residue)
- Existing PostgreSQL `baseline_profiles` and `tenants` tables; no new persistence and no schema migration in this slice (234-dead-transitional-residue)
- PHP 8.4.15 (feat/005-bulk-operations)
@ -274,9 +280,9 @@ ## Code Style
PHP 8.4.15: Follow standard conventions
## Recent Changes
- 231-finding-outcome-taxonomy: Added PHP 8.4.15, Laravel 12, Filament v5, Livewire v4, Blade + `App\Models\Finding`, `App\Filament\Resources\FindingResource`, `App\Services\Findings\FindingWorkflowService`, `App\Services\Baselines\BaselineAutoCloseService`, `App\Services\EntraAdminRoles\EntraAdminRolesFindingGenerator`, `App\Services\PermissionPosture\PermissionPostureFindingGenerator`, `App\Jobs\CompareBaselineToTenantJob`, `App\Filament\Pages\Reviews\ReviewRegister`, `App\Filament\Resources\TenantReviewResource`, `BadgeCatalog`, `BadgeRenderer`, `AuditLog` metadata via `AuditLogger`
- 226-astrodeck-inventory-planning: Added Markdown artifacts + Astro 6.0.0 + TypeScript 5.9 context for source discovery + Repository spec workflow (`.specify`), Astro website source tree under `apps/website/src`, existing component taxonomy (`primitives`, `content`, `sections`, `layout`)
- 225-assignment-hygiene: Added PHP 8.4.15, Laravel 12, Filament v5, Livewire v4, Blade + `Finding`, `FindingResource`, `MyFindingsInbox`, `FindingsIntakeQueue`, `WorkspaceOverviewBuilder`, `EnsureFilamentTenantSelected`, `FindingWorkflowService`, `AuditLog`, `TenantMembership`, Filament page and table primitives
- 234-dead-transitional-residue: Added PHP 8.4.15, Laravel 12, Filament v5, Livewire v4 + `App\Models\BaselineProfile`, `App\Support\Baselines\BaselineProfileStatus`, `App\Support\Badges\BadgeCatalog`, `App\Support\Badges\BadgeDomain`, `Database\Factories\TenantFactory`, `App\Console\Commands\SeedBackupHealthBrowserFixture`, existing tenant-truth and baseline-profile Pest tests
- 233-stale-run-visibility: Added PHP 8.4.15, Laravel 12, Filament v5, Livewire v4 + Filament widgets/resources/pages, Pest v4, `App\Models\OperationRun`, `App\Support\Operations\OperationRunFreshnessState`, `App\Services\Operations\OperationLifecycleReconciler`, `App\Support\OpsUx\OperationUxPresenter`, `App\Support\OpsUx\ActiveRuns`, `App\Support\Badges\BadgeCatalog` / `BadgeRenderer`, `App\Support\Workspaces\WorkspaceOverviewBuilder`, `App\Support\OperationRunLinks`
- 232-operation-run-link-contract: Added PHP 8.4.15, Laravel 12, Filament v5, Livewire v4 + Filament Resources/Pages/Widgets, Pest v4, `App\Support\OperationRunLinks`, `App\Support\System\SystemOperationRunLinks`, `App\Support\Navigation\CanonicalNavigationContext`, `App\Support\Navigation\RelatedNavigationResolver`, existing workspace and tenant authorization helpers
<!-- MANUAL ADDITIONS START -->
### Pre-production compatibility check

View File

@ -67,7 +67,6 @@ public function handle(): int
'name' => (string) ($scenarioConfig['tenant_name'] ?? 'Spec 180 Blocked Backup Tenant'),
'tenant_id' => $tenantRouteKey,
'app_certificate_thumbprint' => null,
'app_status' => 'ok',
'app_notes' => null,
'status' => Tenant::STATUS_ACTIVE,
'environment' => 'dev',

View File

@ -20,6 +20,7 @@
use App\Support\Badges\TagBadgeDomain;
use App\Support\Inventory\TenantCoverageTruth;
use App\Support\Inventory\TenantCoverageTruthResolver;
use App\Support\OperationRunLinks;
use App\Support\Ui\ActionSurface\ActionSurfaceDeclaration;
use App\Support\Ui\ActionSurface\ActionSurfaceDefaults;
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceProfile;
@ -535,7 +536,7 @@ public function basisRunSummary(): array
: 'The coverage basis is current, but your role cannot open the cited run detail.',
'badgeLabel' => $badge->label,
'badgeColor' => $badge->color,
'runUrl' => $canViewRun ? route('admin.operations.view', ['run' => (int) $truth->basisRun->getKey()]) : null,
'runUrl' => $canViewRun ? OperationRunLinks::view($truth->basisRun, $tenant) : null,
'historyUrl' => $canViewRun ? $this->inventorySyncHistoryUrl($tenant) : null,
'inventoryItemsUrl' => InventoryItemResource::getUrl('index', panel: 'tenant', tenant: $tenant),
];
@ -560,13 +561,6 @@ protected function coverageTruth(): ?TenantCoverageTruth
private function inventorySyncHistoryUrl(Tenant $tenant): string
{
return route('admin.operations.index', [
'tenant_id' => (int) $tenant->getKey(),
'tableFilters' => [
'type' => [
'value' => 'inventory_sync',
],
],
]);
return OperationRunLinks::index($tenant, operationType: 'inventory_sync');
}
}

View File

@ -110,14 +110,14 @@ protected function getHeaderActions(): array
$actions[] = Action::make('operate_hub_back_to_operations')
->label('Back to Operations')
->color('gray')
->url(fn (): string => route('admin.operations.index'));
->url(fn (): string => OperationRunLinks::index());
}
if ($activeTenant instanceof Tenant) {
$actions[] = Action::make('operate_hub_show_all_operations')
->label('Show all operations')
->color('gray')
->url(fn (): string => route('admin.operations.index'));
->url(fn (): string => OperationRunLinks::index());
}
$actions[] = Action::make('refresh')
@ -126,7 +126,7 @@ protected function getHeaderActions(): array
->color('primary')
->url(fn (): string => isset($this->run)
? OperationRunLinks::tenantlessView($this->run, $navigationContext)
: route('admin.operations.index'));
: OperationRunLinks::index());
if (! isset($this->run)) {
return $actions;

View File

@ -17,6 +17,7 @@
use App\Support\Badges\TagBadgeRenderer;
use App\Support\Filament\FilterOptionCatalog;
use App\Support\Inventory\InventoryPolicyTypeMeta;
use App\Support\OperationRunLinks;
use App\Support\Ui\ActionSurface\ActionSurfaceDeclaration;
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceInspectAffordance;
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceProfile;
@ -148,7 +149,13 @@ public static function infolist(Schema $schema): Schema
return null;
}
return route('admin.operations.view', ['run' => (int) $record->last_seen_operation_run_id]);
$tenant = $record->tenant;
if ($tenant instanceof Tenant) {
return OperationRunLinks::view((int) $record->last_seen_operation_run_id, $tenant);
}
return OperationRunLinks::tenantlessView((int) $record->last_seen_operation_run_id);
})
->openUrlInNewTab(),
TextEntry::make('support_restore')

View File

@ -13,6 +13,7 @@
use App\Support\Badges\BadgeCatalog;
use App\Support\Badges\BadgeDomain;
use App\Support\Badges\BadgeRenderer;
use App\Support\OperationRunLinks;
use App\Support\OpsUx\OperationUxPresenter;
use App\Support\Rbac\UiEnforcement;
use App\Support\ReviewPackStatus;
@ -199,9 +200,19 @@ public static function infolist(Schema $schema): Schema
->placeholder('—'),
TextEntry::make('operationRun.id')
->label('Operation')
->url(fn (ReviewPack $record): ?string => $record->operation_run_id
? route('admin.operations.view', ['run' => (int) $record->operation_run_id])
: null)
->url(function (ReviewPack $record): ?string {
if (! $record->operation_run_id) {
return null;
}
$tenant = $record->tenant;
if ($tenant instanceof Tenant) {
return OperationRunLinks::view((int) $record->operation_run_id, $tenant);
}
return OperationRunLinks::tenantlessView((int) $record->operation_run_id);
})
->openUrlInNewTab()
->placeholder('—'),
TextEntry::make('fingerprint')->label('Fingerprint')->copyable()->placeholder('—'),

View File

@ -41,7 +41,7 @@ protected function getViewData(): array
return [
'tenant' => null,
'runs' => collect(),
'operationsIndexUrl' => route('admin.operations.index'),
'operationsIndexUrl' => OperationRunLinks::index(),
'operationsIndexLabel' => OperationRunLinks::openCollectionLabel(),
'operationsIndexDescription' => OperationRunLinks::collectionScopeDescription(),
];
@ -68,7 +68,7 @@ protected function getViewData(): array
return [
'tenant' => $tenant,
'runs' => $runs,
'operationsIndexUrl' => route('admin.operations.index'),
'operationsIndexUrl' => OperationRunLinks::index($tenant),
'operationsIndexLabel' => OperationRunLinks::openCollectionLabel(),
'operationsIndexDescription' => OperationRunLinks::collectionScopeDescription(),
];

View File

@ -86,7 +86,7 @@ public function refreshRuns(): void
$query = OperationRun::query()
->where('tenant_id', $tenantId)
->healthyActive()
->active()
->orderByDesc('created_at');
$activeCount = (clone $query)->count();

View File

@ -20,21 +20,6 @@ class BaselineProfile extends Model
{
use HasFactory;
/**
* @deprecated Use BaselineProfileStatus::Draft instead.
*/
public const string STATUS_DRAFT = 'draft';
/**
* @deprecated Use BaselineProfileStatus::Active instead.
*/
public const string STATUS_ACTIVE = 'active';
/**
* @deprecated Use BaselineProfileStatus::Archived instead.
*/
public const string STATUS_ARCHIVED = 'archived';
/** @var list<string> */
protected $fillable = [
'workspace_id',

View File

@ -38,7 +38,6 @@ final class BadgeCatalog
BadgeDomain::BooleanEnabled->value => Domains\BooleanEnabledBadge::class,
BadgeDomain::BooleanHasErrors->value => Domains\BooleanHasErrorsBadge::class,
BadgeDomain::TenantStatus->value => Domains\TenantStatusBadge::class,
BadgeDomain::TenantAppStatus->value => Domains\TenantAppStatusBadge::class,
BadgeDomain::TenantRbacStatus->value => Domains\TenantRbacStatusBadge::class,
BadgeDomain::TenantPermissionStatus->value => Domains\TenantPermissionStatusBadge::class,
BadgeDomain::PolicySnapshotMode->value => Domains\PolicySnapshotModeBadge::class,

View File

@ -29,7 +29,6 @@ enum BadgeDomain: string
case BooleanEnabled = 'boolean_enabled';
case BooleanHasErrors = 'boolean_has_errors';
case TenantStatus = 'tenant_status';
case TenantAppStatus = 'tenant_app_status';
case TenantRbacStatus = 'tenant_rbac_status';
case TenantPermissionStatus = 'tenant_permission_status';
case PolicySnapshotMode = 'policy_snapshot_mode';

View File

@ -1,24 +0,0 @@
<?php
namespace App\Support\Badges\Domains;
use App\Support\Badges\BadgeCatalog;
use App\Support\Badges\BadgeMapper;
use App\Support\Badges\BadgeSpec;
final class TenantAppStatusBadge implements BadgeMapper
{
public function spec(mixed $value): BadgeSpec
{
$state = BadgeCatalog::normalizeState($value);
return match ($state) {
'ok' => new BadgeSpec('OK', 'success', 'heroicon-m-check-circle'),
'configured' => new BadgeSpec('Configured', 'success', 'heroicon-m-check-circle'),
'pending' => new BadgeSpec('Pending', 'warning', 'heroicon-m-clock'),
'requires_consent', 'consent_required' => new BadgeSpec('Consent required', 'warning', 'heroicon-m-exclamation-triangle'),
'error' => new BadgeSpec('Error', 'danger', 'heroicon-m-x-circle'),
default => BadgeSpec::unknown(),
};
}
}

View File

@ -6,19 +6,89 @@
class PanelThemeAsset
{
/**
* @var array<string, bool>
*/
private static array $hotAssetReachability = [];
public static function resolve(string $entry): ?string
{
if (app()->runningUnitTests()) {
return static::resolveFromManifest($entry);
}
if (is_file(public_path('hot'))) {
if (static::shouldUseHotAsset($entry)) {
return Vite::asset($entry);
}
return static::resolveFromManifest($entry);
}
private static function shouldUseHotAsset(string $entry): bool
{
$hotFile = public_path('hot');
if (! is_file($hotFile)) {
return false;
}
$hotUrl = trim((string) file_get_contents($hotFile));
if ($hotUrl === '') {
return false;
}
$assetUrl = Vite::asset($entry);
if ($assetUrl === '') {
return false;
}
if (array_key_exists($assetUrl, static::$hotAssetReachability)) {
return static::$hotAssetReachability[$assetUrl];
}
$parts = parse_url($assetUrl);
if (! is_array($parts)) {
return static::$hotAssetReachability[$assetUrl] = false;
}
$host = $parts['host'] ?? null;
if (! is_string($host) || $host === '') {
return static::$hotAssetReachability[$assetUrl] = false;
}
$scheme = $parts['scheme'] ?? 'http';
$port = $parts['port'] ?? ($scheme === 'https' ? 443 : 80);
$transport = $scheme === 'https' ? 'ssl://' : '';
$connection = @fsockopen($transport.$host, $port, $errorNumber, $errorMessage, 0.2);
if (! is_resource($connection)) {
return static::$hotAssetReachability[$assetUrl] = false;
}
$path = ($parts['path'] ?? '/').(isset($parts['query']) ? '?'.$parts['query'] : '');
$hostHeader = isset($parts['port']) ? $host.':'.$port : $host;
stream_set_timeout($connection, 0, 200000);
fwrite(
$connection,
"HEAD {$path} HTTP/1.1\r\nHost: {$hostHeader}\r\nConnection: close\r\n\r\n",
);
$statusLine = fgets($connection);
fclose($connection);
if (! is_string($statusLine)) {
return static::$hotAssetReachability[$assetUrl] = false;
}
return static::$hotAssetReachability[$assetUrl] = preg_match('/^HTTP\/\d\.\d\s+[23]\d\d\b/', $statusLine) === 1;
}
private static function resolveFromManifest(string $entry): ?string
{
$manifest = public_path('build/manifest.json');

View File

@ -202,7 +202,7 @@ public function auditTargetLink(AuditLog $record): ?array
->whereKey($resourceId)
->where('workspace_id', (int) $workspace->getKey())
->exists()
? ['label' => OperationRunLinks::openLabel(), 'url' => route('admin.operations.view', ['run' => $resourceId])]
? ['label' => OperationRunLinks::openLabel(), 'url' => OperationRunLinks::tenantlessView($resourceId)]
: null,
'baseline_profile' => $workspace instanceof Workspace
&& $this->workspaceCapabilityResolver->isMember($user, $workspace)

View File

@ -81,6 +81,7 @@ public static function index(
?string $activeTab = null,
bool $allTenants = false,
?string $problemClass = null,
?string $operationType = null,
): string {
$parameters = $context?->toQuery() ?? [];
@ -106,6 +107,10 @@ public static function index(
}
}
if (is_string($operationType) && $operationType !== '') {
$parameters['tableFilters']['type']['value'] = $operationType;
}
return route('admin.operations.index', $parameters);
}

View File

@ -22,7 +22,7 @@ public static function existForTenantId(?int $tenantId): bool
return OperationRun::query()
->where('tenant_id', $tenantId)
->healthyActive()
->active()
->exists();
}

View File

@ -42,7 +42,6 @@ public function definition(): array
'app_client_id' => fake()->uuid(),
'app_client_secret' => null, // Skip encryption in tests
'app_certificate_thumbprint' => null,
'app_status' => 'ok',
'app_notes' => null,
'status' => 'active',
'environment' => 'other',

View File

@ -1,6 +1,8 @@
@php($runs = $runs ?? collect())
@php($overflowCount = (int) ($overflowCount ?? 0))
@php($tenant = $tenant ?? null)
@php
$runs = $runs ?? collect();
$overflowCount = (int) ($overflowCount ?? 0);
$tenant = $tenant ?? null;
@endphp
{{-- Cleanup is delegated to the shared poller helper, which uses teardownObserver and new MutationObserver. --}}
@ -16,6 +18,17 @@
@if($runs->isNotEmpty())
<div class="fixed bottom-4 right-4 z-[999999] w-96 space-y-2" style="pointer-events: auto;">
@foreach ($runs->take(5) as $run)
@php
$statusSpec = \App\Support\Badges\BadgeRenderer::spec(
\App\Support\Badges\BadgeDomain::OperationRunStatus,
[
'status' => (string) $run->status,
'freshness_state' => $run->freshnessState()->value,
],
);
$lifecycleAttention = \App\Support\OpsUx\OperationUxPresenter::lifecycleAttentionSummary($run);
$guidance = \App\Support\OpsUx\OperationUxPresenter::surfaceGuidance($run);
@endphp
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl border-2 border-primary-500 dark:border-primary-400 p-4 transition-all animate-in slide-in-from-right duration-300"
wire:key="run-{{ $run->id }}">
<div class="flex items-start justify-between gap-4">
@ -30,6 +43,21 @@
Running {{ ($run->started_at ?? $run->created_at)?->diffForHumans(null, true, true) }}
@endif
</p>
<div class="mt-2 flex flex-wrap items-center gap-2">
<x-filament::badge :color="$statusSpec->color" size="sm">
{{ $statusSpec->label }}
</x-filament::badge>
@if ($lifecycleAttention)
<span class="inline-flex items-center rounded-full border border-warning-200 bg-warning-50 px-2 py-0.5 text-xs font-medium text-warning-800 dark:border-warning-600/40 dark:bg-warning-500/10 dark:text-warning-100">
{{ $lifecycleAttention }}
</span>
@endif
</div>
@if ($guidance)
<p class="mt-2 text-xs leading-5 text-gray-600 dark:text-gray-300">
{{ $guidance }}
</p>
@endif
</div>
@if ($tenant)

View File

@ -7,6 +7,7 @@
use App\Models\OperationRun;
use App\Models\RestoreRun;
use App\Models\Tenant;
use App\Support\OperationRunLinks;
use App\Support\Workspaces\WorkspaceContext;
use Filament\Facades\Filament;
use Illuminate\Foundation\Testing\RefreshDatabase;
@ -63,7 +64,7 @@ public function test_shows_only_generic_links_for_tenantless_runs_on_canonical_d
->get(route('admin.operations.view', ['run' => (int) $run->getKey()]))
->assertOk()
->assertSee('Operations')
->assertSee(route('admin.operations.index'), false)
->assertSee(OperationRunLinks::index(), false)
->assertDontSee('View restore run');
}

View File

@ -75,6 +75,34 @@ public function test_trusts_notification_style_run_links_with_no_selected_tenant
->assertSee('Canonical workspace view');
}
public function test_uses_canonical_collection_link_for_default_back_and_show_all_fallbacks(): void
{
$runTenant = Tenant::factory()->create();
[$user, $runTenant] = createUserWithTenant(tenant: $runTenant, role: 'owner');
$otherTenant = Tenant::factory()->create([
'workspace_id' => (int) $runTenant->workspace_id,
]);
createUserWithTenant(tenant: $otherTenant, user: $user, role: 'owner');
$run = OperationRun::factory()->create([
'workspace_id' => (int) $runTenant->workspace_id,
'tenant_id' => (int) $runTenant->getKey(),
'type' => 'inventory_sync',
]);
Filament::setTenant($otherTenant, true);
$this->actingAs($user)
->withSession([WorkspaceContext::SESSION_KEY => (int) $runTenant->workspace_id])
->get(OperationRunLinks::tenantlessView($run))
->assertOk()
->assertSee('Back to Operations')
->assertSee('Show all operations')
->assertSee(OperationRunLinks::index(), false);
}
public function test_trusts_verification_surface_run_links_with_no_selected_tenant_context(): void
{
$tenant = Tenant::factory()->create();

View File

@ -43,6 +43,10 @@
it('archives baseline profiles for authorized workspace members', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
expect(defined(BaselineProfile::class.'::STATUS_DRAFT'))->toBeFalse()
->and(defined(BaselineProfile::class.'::STATUS_ACTIVE'))->toBeFalse()
->and(defined(BaselineProfile::class.'::STATUS_ARCHIVED'))->toBeFalse();
$profile = BaselineProfile::factory()->active()->create([
'workspace_id' => (int) $tenant->workspace_id,
]);

View File

@ -3,6 +3,8 @@
declare(strict_types=1);
use App\Filament\Resources\BaselineProfileResource;
use App\Models\BaselineProfile;
use App\Support\Baselines\BaselineProfileStatus;
use Filament\Facades\Filament;
it('keeps baseline profiles out of tenant panel registration and tenant navigation URLs', function (): void {
@ -23,6 +25,11 @@
it('keeps baseline profile urls workspace-owned even when a tenant context exists', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
$profile = BaselineProfile::factory()->create([
'workspace_id' => (int) $tenant->workspace_id,
'status' => BaselineProfileStatus::Archived->value,
]);
$this->actingAs($user)
->withSession([\App\Support\Workspaces\WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]);
@ -32,5 +39,8 @@
expect($workspaceUrl)->not->toContain("/admin/t/{$tenant->external_id}/baseline-profiles");
$this->get($workspaceUrl)->assertOk();
$this->get(BaselineProfileResource::getUrl('view', ['record' => $profile], panel: 'admin'))->assertOk();
$this->get("/admin/t/{$tenant->external_id}/baseline-profiles")->assertNotFound();
expect($profile->fresh()->status)->toBe(BaselineProfileStatus::Archived);
});

View File

@ -14,6 +14,8 @@
it('filters baseline profiles by status inside the current workspace', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
expect(defined(BaselineProfile::class.'::STATUS_ACTIVE'))->toBeFalse();
$active = BaselineProfile::factory()->active()->create([
'workspace_id' => (int) $tenant->workspace_id,
]);

View File

@ -7,6 +7,7 @@
use App\Filament\Resources\BaselineProfileResource\Pages\EditBaselineProfile;
use App\Filament\Resources\BaselineProfileResource\Pages\ViewBaselineProfile;
use App\Models\BaselineProfile;
use App\Support\Baselines\BaselineProfileStatus;
use App\Support\Governance\GovernanceSubjectTaxonomyRegistry;
use App\Support\Workspaces\WorkspaceContext;
use Illuminate\Validation\ValidationException;
@ -45,7 +46,7 @@
expect($profile->scope_jsonb)->toBe([
'policy_types' => ['deviceConfiguration'],
'foundation_types' => ['assignmentFilter'],
]);
])->and($profile->status)->toBe(BaselineProfileStatus::Draft);
expect($profile->canonicalScopeJsonb())->toBe([
'version' => 2,
@ -83,7 +84,7 @@
'name' => 'Legacy baseline profile',
'description' => null,
'version_label' => null,
'status' => 'active',
'status' => BaselineProfileStatus::Active->value,
'capture_mode' => 'opportunistic',
'scope_jsonb' => json_encode([
'policy_types' => [],
@ -178,7 +179,7 @@
'name' => 'Legacy lineage profile',
'description' => null,
'version_label' => null,
'status' => 'active',
'status' => BaselineProfileStatus::Active->value,
'capture_mode' => 'opportunistic',
'scope_jsonb' => json_encode([
'policy_types' => ['deviceConfiguration'],
@ -224,4 +225,4 @@
]))->toThrow(ValidationException::class, 'Filters are not supported');
expect(BaselineProfile::query()->where('name', 'Invalid filtered baseline')->exists())->toBeFalse();
});
});

View File

@ -4,9 +4,11 @@
use App\Filament\Pages\InventoryCoverage;
use App\Filament\Resources\InventoryItemResource;
use App\Models\InventoryItem;
use App\Models\OperationRun;
use App\Models\Tenant;
use App\Support\Inventory\InventoryCoverage as InventoryCoveragePayload;
use App\Support\OperationRunLinks;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
@ -40,21 +42,14 @@ function seedCoverageBasisRun(Tenant $tenant): OperationRun
$run = seedCoverageBasisRun($tenant);
$historyUrl = route('admin.operations.index', [
'tenant_id' => (int) $tenant->getKey(),
'tableFilters' => [
'type' => [
'value' => 'inventory_sync',
],
],
]);
$historyUrl = OperationRunLinks::index($tenant, operationType: 'inventory_sync');
$this->actingAs($user)
->get(InventoryCoverage::getUrl(tenant: $tenant))
->assertOk()
->assertSee('Latest coverage-bearing sync completed')
->assertSee('Open basis run')
->assertSee(route('admin.operations.view', ['run' => (int) $run->getKey()]), false)
->assertSee(OperationRunLinks::view($run, $tenant), false)
->assertSee($historyUrl, false)
->assertSee('Review the cited inventory sync to inspect provider or permission issues in detail.');
});
@ -78,6 +73,26 @@ function seedCoverageBasisRun(Tenant $tenant): OperationRun
->assertDontSee('Open basis run');
});
it('shows the last inventory sync as a canonical admin operation detail link', function (): void {
$tenant = Tenant::factory()->create();
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner');
$run = OperationRun::factory()->forTenant($tenant)->create([
'type' => 'inventory_sync',
]);
$item = InventoryItem::factory()->create([
'tenant_id' => (int) $tenant->getKey(),
'last_seen_operation_run_id' => (int) $run->getKey(),
]);
$this->actingAs($user)
->get(InventoryItemResource::getUrl('view', ['record' => $item], tenant: $tenant))
->assertOk()
->assertSee('Last inventory sync')
->assertSee(OperationRunLinks::view($run, $tenant), false);
});
it('keeps the no-basis fallback explicit on the inventory items list', function (): void {
$tenant = Tenant::factory()->create();
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner');

View File

@ -16,7 +16,7 @@
$this->actingAs($user);
OperationRun::factory()->create([
$run = OperationRun::factory()->create([
'workspace_id' => (int) $tenant->workspace_id,
'tenant_id' => (int) $tenant->getKey(),
'type' => 'provider.connection.check',
@ -32,6 +32,8 @@
->assertSee('Open operation')
->assertSee(OperationRunLinks::openCollectionLabel())
->assertSee(OperationRunLinks::collectionScopeDescription())
->assertSee(OperationRunLinks::index($tenant), false)
->assertSee(OperationRunLinks::tenantlessView($run), false)
->assertSee('No action needed.')
->assertDontSee('No operations yet.');
});

View File

@ -25,6 +25,8 @@
role: 'owner',
);
expect($tenant->fresh()->app_status)->toBe('consent_required');
$this->actingAs($user);
Filament::setTenant($tenant, true);
@ -33,11 +35,14 @@
->assertSee('Lifecycle summary')
->assertSee('This tenant is still onboarding. It remains visible on management and review surfaces, but it is not selectable as active context until onboarding completes.')
->assertDontSee('App status')
->assertDontSee('Consent required')
->assertSee('RBAC status')
->assertSee('Failed');
});
it('keeps referenced tenant lifecycle context separate from run status in the tenantless operations viewer', function (): void {
expect(array_key_exists('app_status', Tenant::factory()->onboarding()->raw()))->toBeFalse();
$tenant = Tenant::factory()->onboarding()->create([
'name' => 'Viewer Separation Tenant',
]);

View File

@ -38,6 +38,8 @@
'verification_status' => ProviderVerificationStatus::Unknown->value,
]);
expect($tenant->fresh()->app_status)->toBe('ok');
$this->actingAs($user);
session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id);
@ -61,6 +63,17 @@
->and($visibleColumnNames)->not->toContain('provider_connection_state');
});
it('keeps legacy app status as opt-in test setup instead of a factory default', function (): void {
expect(array_key_exists('app_status', Tenant::factory()->raw()))->toBeFalse();
$tenant = Tenant::factory()->create([
'name' => 'Explicit Historical App Status Tenant',
'app_status' => 'error',
]);
expect($tenant->fresh()->app_status)->toBe('error');
});
it('keeps lifecycle and rbac separate while leading the provider summary with consent and verification', function (): void {
$tenant = Tenant::factory()->create([
'status' => Tenant::STATUS_ONBOARDING,
@ -86,6 +99,8 @@
'verification_status' => ProviderVerificationStatus::Blocked->value,
]);
expect($tenant->fresh()->app_status)->toBe('consent_required');
$this->actingAs($user);
session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id);
@ -97,6 +112,7 @@
->assertSee('RBAC status')
->assertSee('Failed')
->assertDontSee('App status')
->assertDontSee('Consent required')
->assertSee('Truth Cleanup Connection')
->assertSee('Lifecycle')
->assertSee('Disabled')

View File

@ -0,0 +1,160 @@
<?php
declare(strict_types=1);
use Tests\Support\OpsUx\SourceFileScanner;
/**
* @return array<string, string>
*/
function operationRunLinkContractIncludePaths(): array
{
$root = SourceFileScanner::projectRoot();
return [
'tenant_recent_operations_summary' => $root.'/app/Filament/Widgets/Tenant/RecentOperationsSummary.php',
'inventory_coverage' => $root.'/app/Filament/Pages/InventoryCoverage.php',
'inventory_item_resource' => $root.'/app/Filament/Resources/InventoryItemResource.php',
'review_pack_resource' => $root.'/app/Filament/Resources/ReviewPackResource.php',
'tenantless_operation_run_viewer' => $root.'/app/Filament/Pages/Operations/TenantlessOperationRunViewer.php',
'related_navigation_resolver' => $root.'/app/Support/Navigation/RelatedNavigationResolver.php',
'system_directory_tenant' => $root.'/app/Filament/System/Pages/Directory/ViewTenant.php',
'system_directory_workspace' => $root.'/app/Filament/System/Pages/Directory/ViewWorkspace.php',
'system_ops_runs' => $root.'/app/Filament/System/Pages/Ops/Runs.php',
'system_ops_view_run' => $root.'/app/Filament/System/Pages/Ops/ViewRun.php',
'admin_panel_provider' => $root.'/app/Providers/Filament/AdminPanelProvider.php',
'tenant_panel_provider' => $root.'/app/Providers/Filament/TenantPanelProvider.php',
'ensure_filament_tenant_selected' => $root.'/app/Support/Middleware/EnsureFilamentTenantSelected.php',
'clear_tenant_context_controller' => $root.'/app/Http/Controllers/ClearTenantContextController.php',
'operation_run_url_delegate' => $root.'/app/Support/OpsUx/OperationRunUrl.php',
];
}
/**
* @return array<string, string>
*/
function operationRunLinkContractAllowlist(): array
{
$paths = operationRunLinkContractIncludePaths();
return [
$paths['admin_panel_provider'] => 'Admin panel navigation is bootstrapping infrastructure and intentionally links to the canonical collection route before request-scoped navigation context exists.',
$paths['tenant_panel_provider'] => 'Tenant panel navigation is bootstrapping infrastructure and intentionally links to the canonical collection route before tenant-specific helper context is owned by the source surface.',
$paths['ensure_filament_tenant_selected'] => 'Tenant-selection middleware owns redirect/navigation fallback infrastructure and must not fabricate source-surface navigation context.',
$paths['clear_tenant_context_controller'] => 'Clear-tenant redirects preserve an explicit redirect contract and cannot depend on UI helper context.',
];
}
/**
* @param array<string, string> $paths
* @param array<string, string> $allowlist
* @return list<array{file: string, line: int, snippet: string, expectedHelper: string, reason: string}>
*/
function operationRunLinkContractViolations(array $paths, array $allowlist = []): array
{
$patterns = [
[
'pattern' => '/route\(\s*[\'"]admin\.operations\.index[\'"]/',
'expectedHelper' => 'OperationRunLinks::index(...)',
'reason' => 'Raw admin operations collection route assembly bypasses the canonical admin link helper.',
],
[
'pattern' => '/route\(\s*[\'"]admin\.operations\.view[\'"]/',
'expectedHelper' => 'OperationRunLinks::view(...) or OperationRunLinks::tenantlessView(...)',
'reason' => 'Raw admin operation detail route assembly bypasses the canonical admin link helper.',
],
[
'pattern' => '/[\'"]\/system\/ops\/runs(?:\/[^\'"]*)?[\'"]/',
'expectedHelper' => 'SystemOperationRunLinks::index() or SystemOperationRunLinks::view(...)',
'reason' => 'Direct system operations path assembly bypasses the canonical system link helper.',
],
[
'pattern' => '/\b(?:Runs|ViewRun)::getUrl\(/',
'expectedHelper' => 'SystemOperationRunLinks::index() or SystemOperationRunLinks::view(...)',
'reason' => 'Direct system operations page URL generation belongs behind the canonical system link helper.',
],
];
$violations = [];
foreach ($paths as $path) {
if (array_key_exists($path, $allowlist)) {
continue;
}
$source = SourceFileScanner::read($path);
$lines = preg_split('/\R/', $source) ?: [];
foreach ($lines as $index => $line) {
foreach ($patterns as $pattern) {
if (preg_match($pattern['pattern'], $line) !== 1) {
continue;
}
$violations[] = [
'file' => SourceFileScanner::relativePath($path),
'line' => $index + 1,
'snippet' => SourceFileScanner::snippet($source, $index + 1),
'expectedHelper' => $pattern['expectedHelper'],
'reason' => $pattern['reason'],
];
}
}
}
return $violations;
}
it('keeps covered operation run link producers on canonical helper families', function (): void {
$paths = operationRunLinkContractIncludePaths();
$allowlist = operationRunLinkContractAllowlist();
$violations = operationRunLinkContractViolations($paths, $allowlist);
expect($violations)->toBeEmpty();
})->group('surface-guard');
it('keeps the operation run link exception boundary explicit and infrastructure-owned', function (): void {
$allowlist = operationRunLinkContractAllowlist();
expect(array_keys($allowlist))->toHaveCount(4);
foreach ($allowlist as $reason) {
expect($reason)
->not->toBe('')
->not->toContain('convenience');
}
foreach (array_keys($allowlist) as $path) {
expect(SourceFileScanner::read($path))->toContain("route('admin.operations.index')");
}
})->group('surface-guard');
it('reports actionable file and snippet output for a representative raw bypass', function (): void {
$probePath = storage_path('framework/testing/OperationRunLinkContractProbe.php');
if (! is_dir(dirname($probePath))) {
mkdir(dirname($probePath), 0777, true);
}
file_put_contents($probePath, <<<'PHP'
<?php
return route('admin.operations.view', ['run' => 123]);
PHP);
try {
$violations = operationRunLinkContractViolations([
'probe' => $probePath,
]);
} finally {
@unlink($probePath);
}
expect($violations)->toHaveCount(1)
->and($violations[0]['file'])->toContain('OperationRunLinkContractProbe.php')
->and($violations[0]['line'])->toBe(3)
->and($violations[0]['snippet'])->toContain("route('admin.operations.view'")
->and($violations[0]['expectedHelper'])->toContain('OperationRunLinks::view')
->and($violations[0]['reason'])->toContain('bypasses the canonical admin link helper');
})->group('surface-guard');

View File

@ -76,7 +76,7 @@
->assertDontSee('Inventory sync');
})->group('ops-ux');
it('does not show likely stale runs in the progress overlay and stops polling when only stale runs remain', function () {
it('shows likely stale runs in the progress overlay and keeps polling when only stale runs remain', function () {
[$user, $tenant] = createUserWithTenant(role: 'owner');
$this->actingAs($user);
Filament::setTenant($tenant, true);
@ -94,8 +94,10 @@
Livewire::actingAs($user)
->test(BulkOperationProgress::class)
->call('refreshRuns')
->assertSet('hasActiveRuns', false)
->assertDontSee('Inventory sync');
->assertSet('hasActiveRuns', true)
->assertSee('Inventory sync')
->assertSee('Likely stale')
->assertSee('This operation is past its lifecycle window.');
})->group('ops-ux');
it('registers Alpine cleanup for the Ops UX poller to avoid stale listeners across re-renders', function () {

View File

@ -5,6 +5,8 @@
use App\Models\OperationRun;
use App\Models\Tenant;
use App\Support\OperationRunLinks;
use App\Support\OpsUx\OperationRunUrl;
use App\Support\System\SystemOperationRunLinks;
use Illuminate\Support\Facades\File;
it('routes all OperationRun view links through OperationRunLinks', function (): void {
@ -82,3 +84,38 @@
'problemClass' => OperationRun::PROBLEM_CLASS_TERMINAL_FOLLOW_UP,
]));
})->group('ops-ux');
it('preserves helper-owned operation type filters on canonical operations collection links', function (): void {
$tenant = Tenant::factory()->create();
expect(OperationRunLinks::index($tenant, operationType: 'inventory_sync'))
->toBe(route('admin.operations.index', [
'tenant_id' => (int) $tenant->getKey(),
'tableFilters' => [
'type' => [
'value' => 'inventory_sync',
],
],
]));
})->group('ops-ux');
it('keeps the thin operation URL delegate on the canonical admin helpers', function (): void {
$tenant = Tenant::factory()->create();
$run = OperationRun::factory()->for($tenant)->create();
expect(OperationRunUrl::view($run, $tenant))
->toBe(OperationRunLinks::view($run, $tenant))
->and(OperationRunUrl::index($tenant))
->toBe(OperationRunLinks::index($tenant));
})->group('ops-ux');
it('resolves system operation links through the canonical system helper family', function (): void {
$run = OperationRun::factory()->create();
expect(SystemOperationRunLinks::index())
->toBe(\App\Filament\System\Pages\Ops\Runs::getUrl(panel: 'system'))
->and(SystemOperationRunLinks::view($run))
->toBe(\App\Filament\System\Pages\Ops\ViewRun::getUrl(['run' => (int) $run->getKey()], panel: 'system'))
->and(SystemOperationRunLinks::view((int) $run->getKey()))
->toBe(\App\Filament\System\Pages\Ops\ViewRun::getUrl(['run' => (int) $run->getKey()], panel: 'system'));
})->group('ops-ux');

View File

@ -46,7 +46,7 @@
expect($runs->pluck('user_id')->all())->toContain($otherUser->id);
})->group('ops-ux');
it('suppresses stale backup set update runs from the progress widget', function (string $operationType): void {
it('keeps stale backup set update runs visible in the progress widget', function (string $operationType): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
$this->actingAs($user);
Filament::setTenant($tenant, true);
@ -67,8 +67,9 @@
->call('refreshRuns');
expect($component->get('runs'))->toBeInstanceOf(Collection::class)
->and($component->get('runs'))->toHaveCount(0)
->and($component->get('hasActiveRuns'))->toBeFalse();
->and($component->get('runs'))->toHaveCount(1)
->and($component->get('runs')->first()->freshnessState()->value)->toBe('likely_stale')
->and($component->get('hasActiveRuns'))->toBeTrue();
})->with([
'backup set update' => 'backup_set.update',
])->group('ops-ux');

View File

@ -10,10 +10,22 @@
$this->actingAs($user);
Filament::setTenant($tenant, true);
OperationRun::factory()->count(7)->create([
OperationRun::factory()->count(4)->create([
'tenant_id' => $tenant->id,
'workspace_id' => (int) $tenant->workspace_id,
'type' => 'inventory_sync',
'status' => 'queued',
'outcome' => 'pending',
'created_at' => now(),
]);
OperationRun::factory()->count(3)->create([
'tenant_id' => $tenant->id,
'workspace_id' => (int) $tenant->workspace_id,
'type' => 'inventory_sync',
'status' => 'queued',
'outcome' => 'pending',
'created_at' => now()->subHour(),
]);
$component = Livewire::actingAs($user)
@ -22,4 +34,6 @@
expect($component->get('runs'))->toHaveCount(6);
expect($component->get('overflowCount'))->toBe(2);
expect($component->get('runs')->map(fn (OperationRun $run): string => $run->freshnessState()->value)->unique()->values()->all())
->toEqualCanonicalizing(['fresh_active', 'likely_stale']);
})->group('ops-ux');

View File

@ -15,6 +15,7 @@
use App\Services\ReviewPackService;
use App\Support\Auth\UiTooltips;
use App\Support\Evidence\EvidenceSnapshotStatus;
use App\Support\OperationRunLinks;
use App\Support\ReviewPackStatus;
use Filament\Actions\ActionGroup;
use Filament\Facades\Filament;
@ -320,12 +321,16 @@ function seedReviewPackEvidence(Tenant $tenant): EvidenceSnapshot
$tenant = Tenant::factory()->create();
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner');
$snapshot = seedReviewPackEvidence($tenant);
$run = OperationRun::factory()->forTenant($tenant)->create([
'type' => 'tenant.review_pack.generate',
]);
$pack = ReviewPack::factory()->ready()->create([
'tenant_id' => (int) $tenant->getKey(),
'workspace_id' => (int) $tenant->workspace_id,
'initiated_by_user_id' => (int) $user->getKey(),
'evidence_snapshot_id' => (int) $snapshot->getKey(),
'operation_run_id' => (int) $run->getKey(),
'summary' => [
'finding_count' => 5,
'report_count' => 2,
@ -352,6 +357,7 @@ function seedReviewPackEvidence(Tenant $tenant): EvidenceSnapshot
->assertDontSee('Artifact truth')
->assertSee('Publishable')
->assertSee('#'.$snapshot->getKey())
->assertSee(OperationRunLinks::view($run, $tenant), false)
->assertSee('resolved');
});

View File

@ -2,9 +2,11 @@
declare(strict_types=1);
use App\Models\OperationRun;
use App\Models\PlatformUser;
use App\Models\User;
use App\Support\Auth\PlatformCapabilities;
use App\Support\System\SystemOperationRunLinks;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
@ -35,6 +37,46 @@
'/system/ops/runs',
]);
it('returns 404 when a tenant session accesses a system operation detail route', function () {
$user = User::factory()->create();
$run = OperationRun::factory()->create();
$this->actingAs($user)
->get(SystemOperationRunLinks::view($run))
->assertNotFound();
});
it('returns 403 when a platform user lacks operations capability on system operation detail', function () {
$platformUser = PlatformUser::factory()->create([
'capabilities' => [
PlatformCapabilities::ACCESS_SYSTEM_PANEL,
],
'is_active' => true,
]);
$run = OperationRun::factory()->create();
$this->actingAs($platformUser, 'platform')
->get(SystemOperationRunLinks::view($run))
->assertForbidden();
});
it('returns 200 on system operation detail when a platform user has operations capability', function () {
$platformUser = PlatformUser::factory()->create([
'capabilities' => [
PlatformCapabilities::ACCESS_SYSTEM_PANEL,
PlatformCapabilities::OPERATIONS_VIEW,
],
'is_active' => true,
]);
$run = OperationRun::factory()->create();
$this->actingAs($platformUser, 'platform')
->get(SystemOperationRunLinks::view($run))
->assertSuccessful();
});
it('returns 200 when a platform user has the required capability', function () {
$platformUser = PlatformUser::factory()->create([
'capabilities' => [

View File

@ -48,3 +48,14 @@
expect(BadgeCatalog::mapper(BadgeDomain::BooleanEnabled))->not->toBeNull()
->and($domainValues)->not->toContain('provider_connection.status', 'provider_connection.health');
});
it('keeps retired tenant app-status out of the central catalog while active tenant domains remain registered', function (): void {
$domainValues = collect(BadgeDomain::cases())
->map(fn (BadgeDomain $domain): string => $domain->value)
->all();
expect($domainValues)->not->toContain('tenant_app_status')
->and(BadgeCatalog::mapper(BadgeDomain::TenantStatus))->not->toBeNull()
->and(BadgeCatalog::mapper(BadgeDomain::TenantRbacStatus))->not->toBeNull()
->and(BadgeCatalog::mapper(BadgeDomain::TenantPermissionStatus))->not->toBeNull();
});

View File

@ -23,18 +23,15 @@
expect($error->color)->toBe('danger');
});
it('maps tenant app status values to legacy diagnostic badge semantics', function (): void {
$ok = BadgeCatalog::spec(BadgeDomain::TenantAppStatus, 'ok');
expect($ok->label)->toBe('OK');
expect($ok->color)->toBe('success');
it('does not expose retired app status as active tenant badge semantics', function (): void {
$domainValues = collect(BadgeDomain::cases())
->map(fn (BadgeDomain $domain): string => $domain->value)
->all();
$consentRequired = BadgeCatalog::spec(BadgeDomain::TenantAppStatus, 'consent_required');
expect($consentRequired->label)->toBe('Consent required');
expect($consentRequired->color)->toBe('warning');
$error = BadgeCatalog::spec(BadgeDomain::TenantAppStatus, 'error');
expect($error->label)->toBe('Error');
expect($error->color)->toBe('danger');
expect($domainValues)->not->toContain('tenant_app_status')
->and(BadgeCatalog::mapper(BadgeDomain::TenantStatus))->not->toBeNull()
->and(BadgeCatalog::mapper(BadgeDomain::TenantRbacStatus))->not->toBeNull()
->and(BadgeCatalog::mapper(BadgeDomain::TenantPermissionStatus))->not->toBeNull();
});
it('maps tenant RBAC status values to canonical badge semantics', function (): void {

View File

@ -5,11 +5,13 @@
beforeEach(function (): void {
$this->originalPublicPath = public_path();
$this->originalEnvironment = app()->environment();
$this->temporaryPublicPath = null;
});
afterEach(function (): void {
app()->usePublicPath($this->originalPublicPath);
app()->instance('env', $this->originalEnvironment);
if (is_string($this->temporaryPublicPath) && File::isDirectory($this->temporaryPublicPath)) {
File::deleteDirectory($this->temporaryPublicPath);
@ -69,6 +71,27 @@ function useTemporaryPublicPath(): string
->not->toContain(':5173');
});
it('falls back to the built manifest asset when the Vite hot server is unreachable', function (): void {
$publicPath = useTemporaryPublicPath();
app()->instance('env', 'local');
File::ensureDirectoryExists($publicPath.'/build');
File::put($publicPath.'/hot', 'http://127.0.0.1:1');
File::put(
$publicPath.'/build/manifest.json',
json_encode([
'resources/css/filament/admin/theme.css' => [
'file' => 'assets/theme-test.css',
],
], JSON_THROW_ON_ERROR),
);
expect(PanelThemeAsset::resolve('resources/css/filament/admin/theme.css'))
->toEndWith('/build/assets/theme-test.css')
->not->toContain(':1');
});
it('returns null when the build manifest contains invalid json', function (): void {
$publicPath = useTemporaryPublicPath();

View File

@ -5,7 +5,7 @@ # Spec Candidates
>
> **Flow**: Inbox → Qualified → Planned → Spec created → moved to `Promoted to Spec`
**Last reviewed**: 2026-04-22 (promoted `Findings Notifications & Escalation v1` to Spec 224, promoted `Findings Notification Presentation Convergence` to Spec 230, added three architecture contract-enforcement candidates from the 2026-04-22 drift audit, added the repository cleanup strand from the strict read-only legacy audit, reframed the compliance-control foundation candidate into a framework-neutral canonical control catalog foundation, and aligned the control-library candidates to the S1/S2/S3 layering language)
**Last reviewed**: 2026-04-23 (promoted `Operation Run Active-State Visibility & Stale Escalation` to Spec 233, cleaned already-promoted entries for Specs 225, 231, and 232 out of `Qualified`, and kept the remaining contract-enforcement and workflow candidates aligned with the current spec stream)
---
@ -47,7 +47,11 @@ ## Promoted to Spec
- Findings Operator Inbox v1 → Spec 221 (`findings-operator-inbox`)
- Findings Intake & Team Queue v1 -> Spec 222 (`findings-intake-team-queue`)
- Findings Notifications & Escalation v1 → Spec 224 (`findings-notifications-escalation`)
- Assignment Hygiene & Stale Work Detection → Spec 225 (`assignment-hygiene`)
- Findings Notification Presentation Convergence → Spec 230 (`findings-notification-convergence`)
- Finding Outcome Taxonomy & Verification Semantics → Spec 231 (`finding-outcome-taxonomy`)
- Operation Run Link Contract Enforcement → Spec 232 (`operation-run-link-contract`)
- Operation Run Active-State Visibility & Stale Escalation → Spec 233 (`stale-run-visibility`)
- Provider-Backed Action Preflight and Dispatch Gate Unification → Spec 216 (`provider-dispatch-gate`)
- Record Page Header Discipline & Contextual Navigation → Spec 192 (`record-header-discipline`)
- Monitoring Surface Action Hierarchy & Workbench Semantics → Spec 193 (`monitoring-action-hierarchy`)
@ -167,81 +171,8 @@ ### Baseline Capture Truthful Outcomes and Upstream Guardrails
>
> **Why these are not one spec:** Each candidate has a different implementation surface, different stakeholders, and different shippability boundary. The taxonomy is a cross-cutting decision document. Reason code translation touches reason-code artifacts and notification builders. Spec 158 defines the richer artifact truth engine. The Operator Explanation Layer defines the shared interpretation semantics and explanation patterns. Governance operator outcome compression is a UI-information-architecture adoption slice across governance artifact surfaces. Humanized diagnostic summaries are an adoption slice for governance run-detail pages. Gate unification touches provider dispatch and notification plumbing across ~20 services. Merging them would create an unshippable monolith. Keeping them sequenced preserves independent delivery while still converging on one operator language.
### Operation Run Active-State Visibility & Stale Escalation
- **Type**: hardening
- **Source**: product/operator visibility analysis 2026-03-24; operation-run lifecycle and stale-state communication review
- **Vehicle**: new standalone candidate
- **Problem**: TenantPilot already has the core lifecycle foundations for `OperationRun` records: canonical run modelling, workspace-level run viewing, per-type lifecycle policies, freshness and stale detection, overdue-run reconciliation, terminal notifications, and tenant-local active-run hints. The gap is no longer primarily lifecycle logic. The gap is that the same lifecycle truth is not communicated with enough consistency and urgency across the operator surfaces that matter. A run can be past its expected lifecycle or likely stuck while still looking like normal active work on tenant-local cards or dashboard attention surfaces. Operators then have to drill into the canonical run viewer to learn that the run is no longer healthy, which weakens monitoring trust and makes hanging work look deceptively normal.
- **Why it matters**: This is an observability and operator-trust problem in a core platform layer, not visual polish. If `queued` or `running` remains visually neutral after lifecycle expectations have been exceeded, operators receive false reassurance, support burden rises, queue or worker issues are discovered later, and the product trains users that active-state surfaces are not trustworthy without manual drill-down. As TenantPilot pushes more governance, review, drift, and evidence workflows through `OperationRun`, stale active work must never read as healthy progress anywhere in the product.
- **Proposed direction**:
- Reuse the existing lifecycle, freshness, and reconciliation truth to define one **cross-surface active-state presentation contract** that distinguishes at least: `active / normal`, `active / past expected lifecycle`, `stale / likely stuck`, and `terminal / no longer active`
- Upgrade **tenant-local active-run and progress cards** so stale or past-lifecycle runs are visibly and linguistically different from healthy active work instead of reading as neutral `Queued • 1d` or `Running • 45m`
- Upgrade **tenant dashboard and attention surfaces** so they distinguish between healthy activity, activity that needs attention, and activity that is likely stale or hanging
- Upgrade the **workspace operations list / monitoring views** so problematic active runs become scanable at row level instead of being discoverable only through subtle secondary text or by opening each run
- Preserve the **workspace-level canonical run viewer** as the authoritative diagnostic surface, while ensuring compact and summary surfaces do not contradict it
- Apply a **same meaning, different density** rule: tenant cards, dashboard signals, list rows, and run detail may vary in information density, but not in lifecycle meaning or operator implication
- **Core product principles**:
- Execution lifecycle, freshness, and operator attention are related but not identical dimensions
- Compact surfaces may compress information, but must not downplay stale or hanging work
- The workspace-level run viewer remains canonical; this candidate improves visibility, not source-of-truth ownership
- Stale or past-lifecycle work must not look like healthy progress anywhere
- **Candidate requirements**:
- **R1 Cross-surface lifecycle visibility**: all relevant active-run surfaces can distinguish at least normal active, past-lifecycle active, stale/likely stuck, and terminal states
- **R2 Tenant active-run escalation**: tenant-local active-run and progress cards visibly and linguistically escalate stale or past-lifecycle work
- **R3 Dashboard attention separation**: dashboard and attention surfaces distinguish healthy activity from concerning active work
- **R4 Operations-list scanability**: the workspace operations list makes problematic active runs quickly identifiable without requiring row-by-row interpretation or drill-in
- **R5 Canonical viewer preservation**: the workspace-level run viewer remains the detailed and authoritative truth surface
- **R6 No hidden contradiction**: a run that is clearly stale or lifecycle-problematic on the detail page must not appear as ordinary active work on tenant or monitoring surfaces
- **R7 Existing lifecycle logic reuse**: the candidate reuses current freshness, lifecycle, and reconciliation semantics instead of introducing parallel UI-only heuristics
- **R8 No new backend lifecycle semantics unless necessary**: new status values or model-level lifecycle semantics are out unless the current semantics cannot carry the presentation contract cleanly
- **Scope boundaries**:
- **In scope**: tenant-local active-run cards, tenant dashboard activity and attention surfaces, workspace operations list and monitoring surfaces, shared lifecycle presentation contract for active-state visibility, copy and visual semantics needed to distinguish healthy active work from stale active work
- **Out of scope**: retry, cancel, force-fail, or reconcile-now operator actions; queue or worker architecture changes; new scheduler or timeout engines; new notification channels; a full operations-hub redesign; cross-workspace fleet monitoring; introducing new `OperationRun` status values unless existing semantics are proven insufficient
- **Acceptance points**:
- An active run outside its lifecycle expectation is visibly distinct from healthy active work on tenant-local progress cards
- Tenant dashboard and attention surfaces clearly represent the difference between healthy activity and active work that needs attention
- The workspace operations list makes stale or problematic active runs quickly scanable
- No surface shows a run as stale/problematic while another still presents it as normal active work
- The canonical workspace-level run viewer remains the most detailed lifecycle and diagnosis surface
- Existing lifecycle and freshness logic is reused rather than duplicated into local UI-only state rules
- No retry, cancel, or force-fail intervention actions are introduced by this candidate
- Fresh active runs do not regress into false escalation
- Tenant and workspace scoping remain correct; no cross-tenant leakage appears in cards or monitoring views
- Regression coverage includes fresh and stale active runs across tenant and workspace surfaces
- **Suggested test matrix**:
- queued run within expected lifecycle
- queued run well past expected lifecycle
- running run within expected lifecycle
- running run well past expected lifecycle
- run becomes terminal while an operator navigates between tenant and run-detail surfaces
- stale state on detail surface remains semantically stale on tenant and monitoring surfaces
- fresh active runs do not escalate falsely
- tenant-scoped surfaces never show another tenant's runs
- operations list clearly surfaces problematic active runs for fast scan
- **Dependencies**: existing operation-run lifecycle policy and stale detection foundations, canonical run viewer work, tenant-local active-run surfaces, operations monitoring list surfaces
- **Related specs / candidates**: Spec 114 (system console / operations monitoring foundations), Spec 144 (canonical operation viewer context decoupling), Spec 149 (queued execution reauthorization and lifecycle continuity), Spec 161 (operator-explanation-layer), Spec 216 (Provider-Backed Action Preflight and Dispatch Gate Unification)
- **Strategic sequencing**: Best treated before any broader operator intervention-actions spec. First make stale or hanging work visibly truthful across all existing surfaces; only then add retry / force-fail / reconcile-now UX on top of a coherent active-state language.
- **Priority**: high
> Architecture contract-enforcement cluster: these candidates come from the targeted repository drift audit on 2026-04-22. Most are intentionally narrower than naming, presentation, or IA cleanup work. The shared contracts already exist; the gap is that they are not yet treated as mandatory on every platform-owned path. The operation-type candidate is the deliberate exception because the audit found two active competing semantic contracts, not just a missing guardrail.
### Operation Run Link Contract Enforcement
- **Type**: hardening / contract enforcement
- **Source**: targeted repository architecture/pattern-drift audit 2026-04-22; canonical operation-link drift review
- **Problem**: TenantPilot already has a real canonical navigation contract in `OperationRunLinks` and `SystemOperationRunLinks`, but platform-owned UI and shared layers can still build `OperationRun` links through raw `route('admin.operations...')` calls. The same navigation class is therefore emitted through two parallel paths, including in shared navigation layers that otherwise already know the canonical link contract.
- **Why it matters**: This is not a missing-helper problem. It is a shared-contract bypass on a cross-cutting operator path. If it keeps spreading, tenant/workspace context, filter continuity, deep-link stability, and future operations IA changes all become more expensive because each surface can choose raw routes instead of the contract.
- **Proposed direction**:
- inventory platform-owned `OperationRun` collection/detail link producers and classify legitimate infrastructure exceptions
- move platform-owned UI and shared navigation layers to `OperationRunLinks` or `SystemOperationRunLinks`
- make collection/detail/context semantics part of the helper contract rather than repeated local route assembly
- add a lightweight guardrail that catches new raw `route('admin.operations...')` calls outside an explicit allowlist
- **Explicit non-goals**: Not a new operations IA, not a redesign of the operations pages, not a broad routing refactor for the whole repo, and not a change to `OperationRun` page content.
- **Boundary with Operation Run Active-State Visibility & Stale Escalation**: Active-state visibility owns lifecycle communication on compact and detail surfaces. This candidate owns canonical link generation and context continuity.
- **Boundary with Operator Presentation & Lifecycle Action Hardening**: Presentation hardening owns labels, badges, and action-visibility conventions. This candidate owns deep-link and collection-link contract enforcement.
- **Dependencies**: `OperationRunLinks`, `SystemOperationRunLinks`, canonical operations pages, and any repository signal or review guardrail infrastructure introduced by Spec 201.
- **Strategic sequencing**: First of this cluster. The leverage is high because the shared contract already exists and the surface area is concrete.
- **Priority**: high
### Canonical Operation Type Source of Truth
- **Type**: hardening / source-of-truth decision
- **Source**: strict read-only legacy / compatibility audit 2026-04-22; operation-type drift review
@ -432,30 +363,6 @@ ### Tenant Operational Readiness & Status Truth Hierarchy
> Findings execution layer cluster: complementary to existing Spec 154 (`finding-risk-acceptance`). Keep these split so prioritization can pull workflow semantics, operator work surfaces, alerts, external handoff, and later portfolio operating slices independently instead of collapsing them into one oversized "Findings v2" spec.
### Assignment Hygiene & Stale Work Detection
- **Type**: workflow hardening / operations hygiene
- **Source**: findings execution layer candidate pack 2026-04-17; assignment lifecycle hygiene gap analysis
- **Problem**: Assignments can silently rot when memberships change, assignees lose access, or findings remain stuck in `in_progress` indefinitely.
- **Why it matters**: Stale and orphaned assignments erode trust in queues and create hidden backlog. Hygiene reporting is a prerequisite for later auto-reassign logic.
- **Proposed direction**: Detect orphaned assignments, invalid or inactive assignees, and stale `in_progress` work; provide an admin/operator hygiene report; define what counts as stale versus active; stop short of automatic redistribution in v1.
- **Explicit non-goals**: Full reassignment workflows, automatic load distribution, and absence management.
- **Dependencies**: Tenant membership / RBAC model, scheduler or job layer, ownership semantics, open status logic.
- **Roadmap fit**: Findings Workflow v2 hardening.
- **Strategic sequencing**: Shortly after ownership semantics, ideally alongside or immediately after notifications.
- **Priority**: high
### Finding Outcome Taxonomy & Verification Semantics
- **Type**: workflow semantics / reporting hardening
- **Source**: findings execution layer candidate pack 2026-04-17; status/outcome reporting gap analysis
- **Problem**: Resolve and close reasoning is too free-form, and the product does not cleanly separate operator-resolved from system-verified or confirmed-cleared states.
- **Why it matters**: Reporting, audit, reopening logic, and governance review packs need structured outcomes rather than ad hoc prose. Otherwise outcome meaning drifts between operators and surfaces.
- **Proposed direction**: Define structured reason codes for resolve, close, and reopen transitions; distinguish resolved, verified or confirmed cleared, closed, false positive, duplicate, and no-longer-applicable semantics; make reporting and filter UI consume the taxonomy instead of relying on free text.
- **Explicit non-goals**: Comments, full narrative case notes, and complex multi-reason models.
- **Dependencies**: Finding status transitions, audit payloads, reporting and filter UI.
- **Roadmap fit**: Findings Workflow v2 hardening and downstream review/reporting quality.
- **Strategic sequencing**: After the first operator work surfaces unless reporting pressure pulls it earlier.
- **Priority**: high
### Finding Comments & Decision Log v1
- **Type**: collaboration / audit depth
- **Source**: findings execution layer candidate pack 2026-04-17; operator handoff and context gap analysis

View File

@ -0,0 +1,36 @@
# Specification Quality Checklist: Operation Run Link Contract Enforcement
**Purpose**: Validate specification completeness and quality before proceeding to planning
**Created**: 2026-04-23
**Feature**: [spec.md](/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/232-operation-run-link-contract/spec.md)
## Content Quality
- [x] No implementation details (languages, frameworks, APIs)
- [x] Focused on user value and business needs
- [x] Written for non-technical stakeholders
- [x] All mandatory sections completed
## Requirement Completeness
- [x] No [NEEDS CLARIFICATION] markers remain
- [x] Requirements are testable and unambiguous
- [x] Success criteria are measurable
- [x] Success criteria are technology-agnostic (no implementation details)
- [x] All acceptance scenarios are defined
- [x] Edge cases are identified
- [x] Scope is clearly bounded
- [x] Dependencies and assumptions identified
## Feature Readiness
- [x] All functional requirements have clear acceptance criteria
- [x] User scenarios cover primary flows
- [x] Feature meets measurable outcomes defined in Success Criteria
- [x] No implementation details leak into specification
## Notes
- Validation pass 1 completed on 2026-04-23.
- The spec stays intentionally narrow: existing helper families remain the contract, and the feature only standardizes adoption plus a bounded allowlist guard.
- A few requirement lines necessarily name existing shared contract classes and canonical routes because the subject of the spec is contract enforcement on those existing platform surfaces. The spec avoids prescribing implementation structure beyond reuse of the already-shipped canonical paths.

View File

@ -0,0 +1,380 @@
openapi: 3.1.0
info:
title: Operation Run Link Contract Enforcement
version: 1.0.0
summary: Logical internal contract for Spec 232 canonical admin and system operation-run links plus bounded guard enforcement.
description: |
This contract documents the internal helper-owned URL semantics that Spec 232 enforces.
It is intentionally logical rather than a public HTTP API because the feature reuses
existing Filament pages, helper families, and route ownership instead of introducing
a new controller namespace.
servers:
- url: https://logical.internal
description: Non-routable placeholder used to describe internal repository contracts.
paths:
/internal/operation-run-links/admin/collection:
post:
summary: Build the canonical admin operations collection URL for a covered source surface.
operationId: buildAdminOperationCollectionLink
x-not-public-http: true
requestBody:
required: true
content:
application/vnd.tenantpilot.admin-operation-collection-input+json:
schema:
$ref: '#/components/schemas/AdminOperationCollectionLinkInput'
responses:
'200':
description: Canonical admin collection link emitted through `OperationRunLinks::index(...)`.
content:
application/vnd.tenantpilot.operation-link+json:
schema:
$ref: '#/components/schemas/CanonicalOperationLink'
/internal/operation-run-links/admin/detail:
post:
summary: Build the canonical admin operation detail URL for a covered source surface.
operationId: buildAdminOperationDetailLink
x-not-public-http: true
requestBody:
required: true
content:
application/vnd.tenantpilot.admin-operation-detail-input+json:
schema:
$ref: '#/components/schemas/AdminOperationDetailLinkInput'
responses:
'200':
description: Canonical admin detail link emitted through `OperationRunLinks::view(...)` or `tenantlessView(...)`.
content:
application/vnd.tenantpilot.operation-link+json:
schema:
$ref: '#/components/schemas/CanonicalOperationLink'
/internal/operation-run-links/system/collection:
post:
summary: Build the canonical system operations collection URL for a covered system source surface.
operationId: buildSystemOperationCollectionLink
x-not-public-http: true
requestBody:
required: true
content:
application/vnd.tenantpilot.system-operation-collection-input+json:
schema:
$ref: '#/components/schemas/SystemOperationCollectionLinkInput'
responses:
'200':
description: Canonical system collection link emitted through `SystemOperationRunLinks::index()`.
content:
application/vnd.tenantpilot.operation-link+json:
schema:
$ref: '#/components/schemas/CanonicalOperationLink'
/internal/operation-run-links/system/detail:
post:
summary: Build the canonical system operation detail URL for a covered system source surface.
operationId: buildSystemOperationDetailLink
x-not-public-http: true
requestBody:
required: true
content:
application/vnd.tenantpilot.system-operation-detail-input+json:
schema:
$ref: '#/components/schemas/SystemOperationDetailLinkInput'
responses:
'200':
description: Canonical system detail link emitted through `SystemOperationRunLinks::view(...)`.
content:
application/vnd.tenantpilot.operation-link+json:
schema:
$ref: '#/components/schemas/CanonicalOperationLink'
/internal/guards/operation-run-link-contract/check:
post:
summary: Scan the bounded app-side source surface for raw operation-run link bypasses.
operationId: checkOperationRunLinkContract
x-not-public-http: true
requestBody:
required: true
content:
application/vnd.tenantpilot.operation-run-link-guard-input+json:
schema:
$ref: '#/components/schemas/OperationRunLinkGuardCheck'
responses:
'200':
description: Guard completed and found no violations inside the declared boundary.
content:
application/vnd.tenantpilot.operation-run-link-guard-report+json:
schema:
$ref: '#/components/schemas/OperationRunLinkGuardReport'
'422':
description: Guard found one or more raw bypasses outside the allowlist.
content:
application/vnd.tenantpilot.operation-run-link-guard-report+json:
schema:
$ref: '#/components/schemas/OperationRunLinkGuardReport'
/admin/operations:
get:
summary: Existing canonical admin operations collection route.
operationId: openAdminOperationsCollection
responses:
'200':
description: Admin monitoring collection renders for an entitled workspace operator.
content:
text/html:
schema:
type: string
'403':
description: Actor is in scope but lacks the required capability.
'404':
description: Actor is not entitled to the workspace or tenant-bound records referenced by the current context.
/admin/operations/{run}:
get:
summary: Existing canonical admin operation detail route.
operationId: openAdminOperationDetail
parameters:
- name: run
in: path
required: true
schema:
type: integer
responses:
'200':
description: Canonical admin run detail renders for an entitled operator.
content:
text/html:
schema:
type: string
'403':
description: Actor is a member in scope but lacks current capability.
'404':
description: Actor cannot access the run because of workspace, tenant, or plane isolation.
/system/ops/runs:
get:
summary: Existing canonical system operations collection route.
operationId: openSystemOperationsCollection
responses:
'200':
description: System monitoring collection renders for an entitled platform user.
content:
text/html:
schema:
type: string
'403':
description: Platform user lacks the required system operations capability.
'404':
description: Actor is not entitled to the system plane.
/system/ops/runs/{run}:
get:
summary: Existing canonical system operation detail route.
operationId: openSystemOperationDetail
parameters:
- name: run
in: path
required: true
schema:
type: integer
responses:
'200':
description: Canonical system run detail renders for an entitled platform user.
content:
text/html:
schema:
type: string
'403':
description: Platform user lacks the required system operations capability.
'404':
description: Actor is not entitled to the system plane or the run is not visible there.
components:
schemas:
OperationPlane:
type: string
enum:
- admin
- system
OperationLinkKind:
type: string
enum:
- collection
- detail
CoveredSurfaceKey:
type: string
description: Stable identifier for the source surface that emits the canonical link.
CanonicalNavigationContextInput:
type: object
description: |
Opaque helper-owned navigation context payload passed through the admin helper family
when a source surface needs canonical back-link or query continuity.
additionalProperties: true
AdminOperationCollectionLinkInput:
type: object
required:
- surfaceKey
properties:
surfaceKey:
$ref: '#/components/schemas/CoveredSurfaceKey'
tenantId:
type: integer
tenantExternalId:
type: string
navigationContext:
$ref: '#/components/schemas/CanonicalNavigationContextInput'
activeTab:
type: string
problemClass:
type: string
operationType:
type: string
description: Optional helper-owned operations table type filter, used by inventory coverage history links.
allTenants:
type: boolean
default: false
description: Canonical input for `OperationRunLinks::index(...)`.
AdminOperationDetailLinkInput:
type: object
required:
- surfaceKey
- runId
properties:
surfaceKey:
$ref: '#/components/schemas/CoveredSurfaceKey'
runId:
type: integer
navigationContext:
$ref: '#/components/schemas/CanonicalNavigationContextInput'
description: Canonical input for `OperationRunLinks::view(...)` or `tenantlessView(...)`.
SystemOperationCollectionLinkInput:
type: object
required:
- surfaceKey
properties:
surfaceKey:
$ref: '#/components/schemas/CoveredSurfaceKey'
description: Canonical input for `SystemOperationRunLinks::index()`.
SystemOperationDetailLinkInput:
type: object
required:
- surfaceKey
- runId
properties:
surfaceKey:
$ref: '#/components/schemas/CoveredSurfaceKey'
runId:
type: integer
description: Canonical input for `SystemOperationRunLinks::view(...)`.
CanonicalOperationLink:
type: object
required:
- label
- url
- plane
- kind
- canonicalNoun
properties:
label:
type: string
url:
type: string
plane:
$ref: '#/components/schemas/OperationPlane'
kind:
$ref: '#/components/schemas/OperationLinkKind'
canonicalNoun:
type: string
example: Operation
preservedQueryKeys:
type: array
items:
type: string
description: Helper-owned operator-facing link output for canonical operations destinations.
OperationRunLinkGuardCheck:
type: object
required:
- includePaths
- allowlistedPaths
- forbiddenPatterns
properties:
includePaths:
type: array
items:
type: string
examples:
- - app/Filament/Widgets/Tenant/RecentOperationsSummary.php
- app/Filament/Pages/InventoryCoverage.php
- app/Filament/Resources/InventoryItemResource.php
- app/Filament/Resources/ReviewPackResource.php
- app/Filament/Pages/Operations/TenantlessOperationRunViewer.php
- app/Support/Navigation/RelatedNavigationResolver.php
- app/Filament/System/Pages/Directory/ViewTenant.php
- app/Filament/System/Pages/Directory/ViewWorkspace.php
- app/Filament/System/Pages/Ops/Runs.php
- app/Filament/System/Pages/Ops/ViewRun.php
- app/Providers/Filament/AdminPanelProvider.php
- app/Providers/Filament/TenantPanelProvider.php
- app/Support/Middleware/EnsureFilamentTenantSelected.php
- app/Http/Controllers/ClearTenantContextController.php
- app/Support/OpsUx/OperationRunUrl.php
allowlistedPaths:
type: array
items:
type: string
examples:
- - app/Providers/Filament/AdminPanelProvider.php
- app/Providers/Filament/TenantPanelProvider.php
- app/Support/Middleware/EnsureFilamentTenantSelected.php
- app/Http/Controllers/ClearTenantContextController.php
forbiddenPatterns:
type: array
items:
type: string
examples:
- - "route('admin.operations.index'"
- "route('admin.operations.view'"
- "/system/ops/runs"
- "Runs::getUrl("
- "ViewRun::getUrl("
acceptedDelegates:
type: array
items:
type: string
examples:
- - app/Support/OpsUx/OperationRunUrl.php
description: Declares the bounded source surface and explicit exceptions for the guard.
OperationRunLinkGuardViolation:
type: object
required:
- filePath
- line
- snippet
- reason
properties:
filePath:
type: string
line:
type: integer
snippet:
type: string
expectedHelper:
type: string
reason:
type: string
description: Actionable failure output for one raw bypass.
OperationRunLinkGuardReport:
type: object
required:
- scannedPaths
- allowlistedPaths
- violations
properties:
scannedPaths:
type: array
items:
type: string
allowlistedPaths:
type: array
items:
type: string
acceptedDelegates:
type: array
items:
type: string
violations:
type: array
items:
$ref: '#/components/schemas/OperationRunLinkGuardViolation'
description: Report shape returned by the bounded guard check.

View File

@ -0,0 +1,199 @@
# Data Model: Operation Run Link Contract Enforcement
## Overview
This feature introduces no new persisted business entity. Existing `OperationRun` records, workspace and tenant authorization truth, and canonical operations destination pages remain authoritative. The new work is a derived link-generation and guard contract over those existing records and helper families.
## Existing Persistent Entities
### OperationRun
**Purpose**: Canonical runtime and monitoring truth for operation collection and detail destinations.
**Key fields used by this feature**:
- `id`
- `workspace_id`
- `tenant_id`
- `type`
- `status`
- `outcome`
- `context`
**Rules relevant to this feature**:
- Admin-plane and system-plane detail links resolve to existing canonical monitoring surfaces; the feature does not add a new route family.
- Tenant-bound runs remain subject to destination-side entitlement checks even when the source link carries canonical tenant continuity.
- The feature changes how source surfaces build URLs, not how `OperationRun` lifecycle truth is persisted.
### Tenant
**Purpose**: Existing tenant scope and entitlement anchor for admin-plane collection continuity and tenant-bound run inspection.
**Key fields used by this feature**:
- `id`
- `external_id`
- `workspace_id`
- `name`
**Rules relevant to this feature**:
- Admin-plane collection links may preserve entitled tenant context only through helper-supported parameters.
- Detail links never create a tenant-prefixed duplicate route; tenant relevance is enforced at the destination against the run itself.
### Workspace
**Purpose**: Existing workspace isolation boundary for canonical admin monitoring routes.
**Key fields used by this feature**:
- `id`
- membership and capability truth via existing authorization helpers
**Rules relevant to this feature**:
- Non-members remain `404` on canonical admin monitoring routes.
- The feature does not add any new workspace-scoped persistence or copied navigation records.
## Derived Models
### AdminOperationCollectionLinkInput
**Purpose**: Canonical input model for helper-owned admin collection links.
**Fields**:
- `surfaceKey`
- `tenantId` or `tenantExternalId` when the source surface owns entitled tenant continuity
- `navigationContext`
- `activeTab`
- `problemClass`
- `allTenants`
**Validation rules**:
- Tenant context is included only when the source surface already owns an entitled tenant.
- `activeTab`, `problemClass`, and `allTenants` remain limited to current helper-supported semantics.
- Collection URLs are always emitted by `OperationRunLinks::index(...)`.
### AdminOperationDetailLinkInput
**Purpose**: Canonical input model for helper-owned admin detail links.
**Fields**:
- `surfaceKey`
- `runId`
- `navigationContext`
**Validation rules**:
- Detail links are emitted only through `OperationRunLinks::view(...)` or `OperationRunLinks::tenantlessView(...)`.
- No source surface may mint a tenant-prefixed or surface-local duplicate detail route.
### SystemOperationCollectionLinkInput
**Purpose**: Canonical input model for helper-owned system collection links.
**Fields**:
- `surfaceKey`
**Validation rules**:
- Collection links are emitted only through `SystemOperationRunLinks::index()`.
- System-plane collection links never fall back to admin-plane monitoring.
### SystemOperationDetailLinkInput
**Purpose**: Canonical input model for helper-owned system detail links.
**Fields**:
- `surfaceKey`
- `runId`
**Validation rules**:
- Detail links are emitted only through `SystemOperationRunLinks::view(...)`.
- System-plane detail links never fall back to admin-plane monitoring.
### CoveredLinkProducer
**Purpose**: Planning and guard model for every app-side source that emits an `OperationRun` collection or detail link.
**Fields**:
- `surfaceKey`
- `filePath`
- `plane` (`admin`, `system`)
- `linkKind` (`collection`, `detail`, `both`)
- `contractState` (`migrated`, `verified_helper_backed`, `allowlisted_exception`)
- `justification`
**State transitions**:
- `raw_bypass` -> `migrated`
- `raw_bypass` -> `allowlisted_exception`
- `existing_helper_path` -> `verified_helper_backed`
- `thin_delegate` -> `verified_helper_backed`
**Rules**:
- Every first-slice producer must end in either `migrated`, `verified_helper_backed`, or `allowlisted_exception`.
- `allowlisted_exception` is valid only for infrastructure or redirect code that should not absorb UI-context dependencies.
- `verified_helper_backed` is valid for already-converged system producers and thin delegates that forward directly to the canonical helper family.
### OperationRunLinkGuardReport
**Purpose**: Derived failure output for the bounded regression guard.
**Fields**:
- `scannedPaths[]`
- `allowlistedPaths[]`
- `acceptedDelegates[]`
- `violations[]`
### GuardViolation
**Purpose**: Actionable output for a newly detected raw bypass.
**Fields**:
- `filePath`
- `line`
- `snippet`
- `expectedHelper` (optional)
- `reason`
**Rules**:
- When present, `expectedHelper` points to a concrete replacement path such as `OperationRunLinks::index(...)`, `OperationRunLinks::view(...)`, or `SystemOperationRunLinks::view(...)`.
- Violations are limited to the declared guard boundary and must not report tests or helper implementations themselves.
## Consumer Matrix
| Producer | Plane | Link kinds | Target state |
|----------|-------|------------|--------------|
| `RecentOperationsSummary` | admin | collection | migrated |
| `InventoryCoverage` | admin | collection + detail | migrated |
| `InventoryItemResource` | admin | detail | migrated |
| `ReviewPackResource` | admin | detail | migrated |
| `TenantlessOperationRunViewer` | admin | collection fallbacks | migrated |
| `RelatedNavigationResolver` | admin | detail | migrated |
| `AdminPanelProvider` | admin | collection nav shortcut | allowlisted exception |
| `TenantPanelProvider` | admin | collection nav shortcut | allowlisted exception |
| `EnsureFilamentTenantSelected` | admin | collection redirect shortcut | allowlisted exception |
| `ClearTenantContextController` | admin | collection redirect fallback | allowlisted exception |
| `ViewTenant` | system | collection + detail | verified helper-backed |
| `ViewWorkspace` | system | collection + detail | verified helper-backed |
| `Runs` | system | collection + detail | verified helper-backed |
| `ViewRun` | system | collection + detail | verified helper-backed |
## Persistence Boundaries
- No new table, enum-backed state, cache record, or presentation-only persistence is introduced.
- The producer inventory and allowlist are repository-level planning and guard artifacts, not product-domain records.
- Canonical navigation context remains derived request state owned by existing helper and navigation abstractions.

View File

@ -0,0 +1,230 @@
# Implementation Plan: Operation Run Link Contract Enforcement
**Branch**: `232-operation-run-link-contract` | **Date**: 2026-04-23 | **Spec**: [spec.md](./spec.md)
**Input**: Feature specification from `/specs/232-operation-run-link-contract/spec.md`
## Summary
Enforce one canonical contract for platform-owned `OperationRun` collection and detail links by migrating confirmed raw admin-plane producers to `OperationRunLinks`, preserving and regression-protecting the existing system-plane helper path through `SystemOperationRunLinks`, recording only the narrow infrastructure exceptions that cannot safely depend on the helper family, and adding one bounded regression guard that blocks new raw bypasses inside the declared UI and shared-navigation boundary.
## Technical Context
**Language/Version**: PHP 8.4.15, Laravel 12, Filament v5, Livewire v4
**Primary Dependencies**: Filament Resources/Pages/Widgets, Pest v4, `App\Support\OperationRunLinks`, `App\Support\System\SystemOperationRunLinks`, `App\Support\Navigation\CanonicalNavigationContext`, `App\Support\Navigation\RelatedNavigationResolver`, existing workspace and tenant authorization helpers
**Storage**: PostgreSQL-backed existing `operation_runs`, `tenants`, and `workspaces` records plus current session-backed canonical navigation state; no new persistence
**Testing**: Focused Pest feature tests for canonical link behavior, representative admin drill-throughs, admin and system authorization semantics, and one bounded guard test
**Validation Lanes**: `fast-feedback`, `confidence`
**Target Platform**: Laravel admin web application running in Sail Linux containers, with admin plane at `/admin` and platform plane at `/system`
**Project Type**: Monorepo with one Laravel runtime in `apps/platform` and spec artifacts at repository root
**Performance Goals**: Link generation remains helper-owned string construction with no new remote work, no new persisted navigation state, and no broadened destination queries beyond existing canonical helper semantics
**Constraints**: No new operations route family, no second link presenter stack, no compatibility shim layer, no weakening of `404` versus `403` semantics, and no repo-wide guard broadening beyond platform-owned UI and shared navigation code in this slice
**Scale/Scope**: Migrate confirmed raw admin-plane producers in widgets, pages, resources, and shared navigation; validate already-helper-backed system-plane producers; keep bootstrapping and redirect exceptions explicit and narrow
## Filament v5 Implementation Contract
- **Livewire v4.0+ compliance**: Preserved. The feature stays inside existing Filament v5 and Livewire v4 primitives and does not introduce legacy Livewire v3 patterns.
- **Provider registration location**: Unchanged. Panel providers remain registered in `bootstrap/providers.php`, not `bootstrap/app.php`.
- **Global search coverage**:
- `InventoryItemResource` remains compatible with global search expectations because it already exposes a `view` page.
- `ReviewPackResource` keeps global search disabled via `$isGloballySearchable = false` while still exposing a `view` page for direct navigation.
- The affected pages, widgets, and shared navigation helpers do not introduce or change any additional global-search surface.
- **Destructive actions**: No new destructive actions are introduced. Existing destructive or destructive-like actions on operations surfaces remain governed by their current `Action::make(...)->action(...)` definitions, and existing system run-detail actions already retain `->requiresConfirmation()`.
- **Asset strategy**: No new panel-only or shared assets are planned. Deployment expectations remain unchanged, including `cd apps/platform && php artisan filament:assets` only when registered Filament assets change.
- **Testing plan**: Prove the feature with focused feature coverage on canonical admin links, representative dashboard and shared-resolver drill-throughs, explicit admin `404`/`403` authorization preservation, the new bounded guard, and system-plane continuity plus authorization semantics. No browser or heavy-governance family is needed for this plan.
## UI / Surface Guardrail Plan
- **Guardrail scope**: Changed admin monitoring collection/detail link producers, shared related-navigation builders, helper-owned URL-query continuity, and system-plane regression protection for canonical operations links
- **Native vs custom classification summary**: Mixed shared-family change using native Filament resources/pages/widgets plus existing shared link helpers
- **Shared-family relevance**: Navigation, action links, related links, deep links, and canonical monitoring entry points
- **State layers in scope**: `page`, `detail`, and helper-owned `URL-query` continuity
- **Handling modes by drift class or surface**: Hard-stop for new raw bypasses inside the declared guard boundary; review-mandatory for explicit infrastructure exceptions
- **Repository-signal treatment**: Review-mandatory because the feature adds a bounded repo guard and a named exception list
- **Special surface test profiles**: `standard-native-filament`, `monitoring-state-page`
- **Required tests or manual smoke**: `functional-core`, `state-contract`, `manual-smoke`
- **Exception path and spread control**: One named exception boundary for bootstrapping, middleware, and redirect surfaces that cannot safely depend on runtime navigation context; each retained path must be file-specific and justified
- **Active feature PR close-out entry**: `Guardrail`
## Shared Pattern & System Fit
- **Cross-cutting feature marker**: yes
- **Systems touched**: `OperationRunLinks`, `SystemOperationRunLinks`, `CanonicalNavigationContext`, `RecentOperationsSummary`, `InventoryCoverage`, `InventoryItemResource`, `ReviewPackResource`, `TenantlessOperationRunViewer`, `RelatedNavigationResolver`, panel navigation providers, tenant-selection middleware, and clear-tenant-context redirect behavior
- **Shared abstractions reused**: `App\Support\OperationRunLinks`, `App\Support\System\SystemOperationRunLinks`, `App\Support\Navigation\CanonicalNavigationContext`; the existing `App\Support\OpsUx\OperationRunUrl` wrapper remains acceptable where it simply delegates to `OperationRunLinks`
- **New abstraction introduced? why?**: none; the only new structure is a test-local allowlist boundary for legitimate raw producers
- **Why the existing abstraction was sufficient or insufficient**: The helper families already encode canonical labels, admin/system plane selection, entitled tenant continuity, and canonical query semantics. The only current-release gap is adoption and enforcement on specific producers that still assemble routes locally.
- **Bounded deviation / spread control**: Infrastructure-only exceptions are explicit, file-scoped, and non-precedential. UI surfaces, shared navigation, and related-link builders stay on the helper path.
## Constitution Check
*GATE: Passed before Phase 0 research. Re-checked after Phase 1 design: still passed with one bounded guard and no new persisted truth.*
| Gate | Status | Plan Notes |
|------|--------|------------|
| Inventory-first / read-write separation | PASS | The feature changes link generation only. It introduces no new writes, no new restore or remediation flow, and no new persisted artifact. |
| RBAC, workspace isolation, tenant isolation | PASS | Admin-plane destinations keep workspace and tenant entitlement checks; system-plane destinations remain platform-only; cross-plane access stays `404`. |
| Run observability / Ops-UX | PASS | Existing operations pages remain the canonical monitoring destination, but the feature does not start, mutate, or reclassify `OperationRun` lifecycle behavior. |
| Shared pattern first | PASS | The plan converges on `OperationRunLinks` and `SystemOperationRunLinks` instead of introducing a second link presenter or navigation framework. |
| Proportionality / no premature abstraction | PASS | The change is confined to migrating confirmed producers plus one bounded guard allowlist. No new registry, resolver family, or persisted truth is introduced. |
| UI semantics / Filament-native discipline | PASS | Existing Filament pages, resources, widgets, and navigation items remain intact; only helper-owned URLs change. No ad-hoc UI replacement or new action chrome is introduced. |
| Test governance | PASS | Proof stays in focused feature lanes with a narrow guard boundary. No browser lane or heavy-governance promotion is required for this slice. |
## Test Governance Check
- **Test purpose / classification by changed surface**: `Feature` for link continuity, representative admin drill-throughs, authorization-path preservation, and the bounded guard
- **Affected validation lanes**: `fast-feedback`, `confidence`
- **Why this lane mix is the narrowest sufficient proof**: The business truth is helper-owned URL generation, correct plane and scope continuity, and bounded prevention of new raw bypasses. Those behaviors are fully provable with targeted feature tests and one repo-guard test; browser coverage would add cost without validating additional business logic.
- **Narrowest proving command(s)**:
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent`
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/OpsUx/CanonicalViewRunLinksTest.php tests/Feature/Monitoring/OperationsDashboardDrillthroughTest.php tests/Feature/Filament/RecentOperationsSummaryWidgetTest.php tests/Feature/Filament/InventoryCoverageRunContinuityTest.php tests/Feature/ReviewPack/ReviewPackResourceTest.php tests/Feature/144/CanonicalOperationViewerDeepLinkTrustTest.php tests/Feature/078/RelatedLinksOnDetailTest.php tests/Feature/RunAuthorizationTenantIsolationTest.php tests/Feature/System/Spec195/SystemDirectoryResidualSurfaceTest.php tests/Feature/System/Spec113/AuthorizationSemanticsTest.php tests/Feature/Guards/OperationRunLinkContractGuardTest.php`
- **Fixture / helper / factory / seed / context cost risks**: Minimal. Existing `OperationRun` factories, workspace membership helpers, tenant fixtures, and platform-user fixtures are sufficient.
- **Expensive defaults or shared helper growth introduced?**: No. The guard allowlist remains opt-in and local to this feature; no new global test helper is required.
- **Heavy-family additions, promotions, or visibility changes**: none
- **Surface-class relief / special coverage rule**: Standard native-Filament relief plus the existing `monitoring-state-page` profile for canonical operations destinations
- **Closing validation and reviewer handoff**: Re-run `pint`, then the focused test command above. Reviewers should verify that each migrated source now uses the correct helper family, that remaining raw producers sit only in the named exception boundary, and that both admin and system authorization semantics are unchanged.
- **Budget / baseline / trend follow-up**: none
- **Review-stop questions**: Did the guard accidentally absorb tests or non-operator infrastructure? Did any admin-plane producer still hand-assemble `admin.operations` URLs? Did any system-plane producer start bypassing `SystemOperationRunLinks`? Did any exception remain convenience-based instead of infrastructure-based?
- **Escalation path**: `document-in-feature`
- **Active feature PR close-out entry**: `Guardrail`
- **Why no dedicated follow-up spec is needed**: This is current-release contract enforcement around an existing shared interaction family. Only a later desire for repo-wide routing policy would justify a separate governance spec.
## Project Structure
### Documentation (this feature)
```text
specs/232-operation-run-link-contract/
├── plan.md
├── research.md
├── data-model.md
├── quickstart.md
├── contracts/
│ └── operation-run-link-contract.logical.openapi.yaml
└── tasks.md
```
### Source Code (repository root)
```text
apps/platform/
├── app/
│ ├── Filament/
│ │ ├── Pages/
│ │ │ ├── InventoryCoverage.php
│ │ │ └── Operations/TenantlessOperationRunViewer.php
│ │ ├── Resources/
│ │ │ ├── InventoryItemResource.php
│ │ │ └── ReviewPackResource.php
│ │ └── Widgets/Tenant/RecentOperationsSummary.php
│ ├── Http/Controllers/ClearTenantContextController.php
│ ├── Providers/Filament/
│ │ ├── AdminPanelProvider.php
│ │ └── TenantPanelProvider.php
│ └── Support/
│ ├── Middleware/EnsureFilamentTenantSelected.php
│ ├── Navigation/
│ │ ├── CanonicalNavigationContext.php
│ │ └── RelatedNavigationResolver.php
│ ├── OperationRunLinks.php
│ ├── OpsUx/OperationRunUrl.php
│ └── System/SystemOperationRunLinks.php
└── tests/
└── Feature/
├── 078/RelatedLinksOnDetailTest.php
├── 144/CanonicalOperationViewerDeepLinkTrustTest.php
├── Filament/
│ ├── InventoryCoverageRunContinuityTest.php
│ └── RecentOperationsSummaryWidgetTest.php
├── Guards/OperationRunLinkContractGuardTest.php
├── Monitoring/OperationsDashboardDrillthroughTest.php
├── OpsUx/CanonicalViewRunLinksTest.php
├── ReviewPack/ReviewPackResourceTest.php
├── RunAuthorizationTenantIsolationTest.php
└── System/
├── Spec113/AuthorizationSemanticsTest.php
└── Spec195/SystemDirectoryResidualSurfaceTest.php
```
**Structure Decision**: Single Laravel application inside the monorepo. Runtime changes stay inside `apps/platform`, while planning artifacts remain under `specs/232-operation-run-link-contract`.
## Complexity Tracking
No constitutional violation is planned. One bounded review artifact is tracked explicitly because the feature adds an allowlisted guard boundary.
| Violation | Why Needed | Simpler Alternative Rejected Because |
|-----------|------------|-------------------------------------|
| BLOAT-001 bounded exception inventory | The guard needs a small explicit exception list so bootstrapping and redirect code does not have to fake helper usage or fabricate runtime context it does not own. | A raw repo-wide ban without named exceptions would create false positives, blur the operator-facing boundary, and push the feature into heavy-governance scope. |
## Proportionality Review
- **Current operator problem**: Platform-owned admin and system surfaces still have parallel ways to open the same canonical operations destinations, which creates plane and scope drift and keeps the next contributor free to reintroduce raw route assembly.
- **Existing structure is insufficient because**: The helper families already exist, but they are optional in practice. Without a bounded enforcement slice, the repository keeps two competing link-generation paths.
- **Narrowest correct implementation**: Migrate the confirmed raw admin producers, preserve the existing system helper path, record the few legitimate infrastructure exceptions, and add one route-bounded guard that fails on new bypasses.
- **Ownership cost created**: Small ongoing maintenance of the exception list and targeted regression coverage for a shared interaction family.
- **Alternative intentionally rejected**: A repo-wide route-string ban or a new navigation presenter stack. Both would be broader than the current operator problem and would add governance or architecture cost the release does not need.
- **Release truth**: Current-release contract enforcement and cleanup.
## Phase 0 Research Summary
- Reuse `OperationRunLinks` for every admin-plane producer that opens `/admin/operations` or `/admin/operations/{run}`; do not create a second admin helper or presenter.
- Treat the currently confirmed raw admin-plane producers as first-slice migration targets: `RecentOperationsSummary`, `InventoryCoverage`, `InventoryItemResource`, `ReviewPackResource`, `TenantlessOperationRunViewer`, and `RelatedNavigationResolver`.
- Keep the initial explicit exception boundary narrow and infrastructure-only: panel providers, tenant-selection middleware, and clear-tenant-context redirects may stay raw if helper adoption would fabricate the wrong runtime context.
- Preserve `SystemOperationRunLinks` as the canonical system-plane path. Current system widgets and pages are already largely converged, so the first slice focuses on regression prevention rather than inventing synthetic migration work.
- Treat `OperationRunUrl` as an acceptable thin delegating seam because it forwards directly to `OperationRunLinks` and does not create parallel routing truth.
- Keep the guard route-bounded to platform-owned UI and shared navigation code under `apps/platform/app`; do not scan tests, helpers themselves, or unrelated infrastructure.
## Phase 1 Design Summary
- `data-model.md` documents the feature as a derived contract over existing `OperationRun`, tenant, workspace, and canonical navigation truth plus a bounded producer inventory and explicit exception model.
- `contracts/operation-run-link-contract.logical.openapi.yaml` defines the internal logical contract for canonical admin/system collection and detail links plus the bounded guard request and result shape.
- `quickstart.md` provides the focused validation path for representative admin drill-throughs, system-plane continuity, allowlisted exceptions, and authorization semantics.
## Implementation Close-Out Inventory
- **Migrated admin producers**: `RecentOperationsSummary` collection links now use `OperationRunLinks::index(...)`; `InventoryCoverage` basis/detail and inventory-sync history links now use `OperationRunLinks::view(...)` and helper-owned type filtering; `InventoryItemResource` and `ReviewPackResource` detail links now use `OperationRunLinks::view(...)` or `tenantlessView(...)`; `TenantlessOperationRunViewer` default collection fallbacks now use `OperationRunLinks::index()`; `RelatedNavigationResolver` operation-run audit target links now use `OperationRunLinks::tenantlessView(...)`.
- **Verified system producers**: `ViewTenant`, `ViewWorkspace`, `Runs`, and `ViewRun` remain on `SystemOperationRunLinks::index()` and `SystemOperationRunLinks::view(...)` with no admin-plane fallback.
- **Accepted delegate**: `App\Support\OpsUx\OperationRunUrl` remains a thin delegate to `OperationRunLinks` and is explicitly covered by helper-contract tests.
- **Allowlisted infrastructure exceptions**: `AdminPanelProvider`, `TenantPanelProvider`, `EnsureFilamentTenantSelected`, and `ClearTenantContextController` retain raw `admin.operations.index` routes because they own panel bootstrapping or redirect behavior rather than source-surface drill-through context.
- **Guard boundary**: `OperationRunLinkContractGuardTest` scans the migrated admin producers, verified system producers, accepted delegate, and four explicit infrastructure exceptions. It blocks raw `admin.operations.index`, raw `admin.operations.view`, direct `/system/ops/runs` paths, and direct `Runs::getUrl(...)` / `ViewRun::getUrl(...)` use outside the allowlist.
- **Test-governance disposition**: `document-in-feature`. The cost is contained to a focused feature guard and representative feature coverage; no follow-up spec or heavy-governance lane is needed.
## Phase 1 Agent Context Update
- Run `.specify/scripts/bash/update-agent-context.sh copilot` after the plan, research, data model, quickstart, and contract artifacts are written.
- The update must preserve manual additions between generated markers and add only the new technology and change notes relevant to Spec 232.
- The generated agent-context file is supporting output for the planning workflow, not a reason to widen the feature scope.
## Implementation Strategy
1. **Inventory and classify producers**
- Freeze the first-slice inventory of raw admin-plane link producers and distinguish between migration targets, verified helper-backed system producers, and explicit infrastructure exceptions.
2. **Migrate direct admin collection and detail links**
- Replace raw `route('admin.operations.index')` and `route('admin.operations.view')` usage in widgets, pages, and resources with `OperationRunLinks` methods.
- Preserve canonical tenant continuity, problem-class filters, active-tab semantics, and current operator-facing labels through the helper contract only.
3. **Normalize shared related-navigation and back-link paths**
- Refactor `RelatedNavigationResolver` and `TenantlessOperationRunViewer` to consume canonical helper methods rather than local route assembly.
- Keep back-link context helper-owned when `CanonicalNavigationContext` is present and degrade cleanly to the canonical admin collection when it is absent.
4. **Retain only justified infrastructure exceptions**
- Keep panel navigation providers, tenant-selection middleware, and clear-tenant-context redirects as the narrow allowlisted exception set for this slice.
- Record each allowlisted exception with a reason tied to bootstrapping or redirect ownership, not convenience.
5. **Protect the already-converged system plane**
- Audit verified helper-backed system pages and widgets and keep them on `SystemOperationRunLinks`, applying only minimal cleanup if a direct page URL slips through.
- Use the guard and authorization tests to prove that no platform-facing system producer regresses to admin-plane or direct page URL assembly.
6. **Add bounded regression coverage**
- Add one guard test that scans only the declared app-side boundary and fails with file-plus-snippet output on representative bypasses.
- Extend or update focused feature tests so admin-plane continuity, explicit admin `404`/`403` preservation, system-plane continuity, and negative authorization behavior remain explicit.
## Risks and Mitigations
- **False-positive guard scope**: A broad scan would catch tests or infrastructure code. Mitigation: keep the boundary on platform-owned UI and shared navigation files only and maintain a file-scoped allowlist for legitimate exceptions.
- **Tenant continuity drift**: Replacing raw URLs could accidentally drop canonical filters or navigation context. Mitigation: route collection and detail links through existing helper parameters and keep representative continuity tests in scope.
- **Back-link regression on run detail**: `TenantlessOperationRunViewer` currently mixes raw fallbacks with helper-owned detail refresh behavior. Mitigation: migrate both the back-to-operations and show-all-operations fallbacks in the same slice so behavior stays coherent.
- **Over-scoping into routing cleanup**: It is easy to turn this into a general route-string purge. Mitigation: keep the feature limited to `OperationRun` collection and detail link producers plus their bounded exception list.
## Post-Design Re-check
Phase 0 and Phase 1 outputs resolve the planning questions without introducing a new routing framework, new persisted navigation truth, or a repo-wide governance lane change. The plan remains constitution-compliant, helper-first, and ready for `/speckit.tasks`.

View File

@ -0,0 +1,97 @@
# Quickstart: Operation Run Link Contract Enforcement
## Prerequisites
1. Start the local platform stack.
```bash
export PATH="/bin:/usr/bin:/usr/local/bin:$PATH"
cd apps/platform && ./vendor/bin/sail up -d
```
2. Work with:
- one workspace operator who can access canonical admin monitoring,
- one entitled tenant with recent `OperationRun` records,
- one second tenant that the operator must not be able to inspect, and
- one platform user who can access `/system/ops/runs`.
3. Remember that this feature changes link generation only. No frontend asset build should be required unless unrelated platform assets changed.
## Automated Validation
Run formatting and the narrowest proving suites for this feature:
```bash
export PATH="/bin:/usr/bin:/usr/local/bin:$PATH"
cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent
export PATH="/bin:/usr/bin:/usr/local/bin:$PATH"
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/OpsUx/CanonicalViewRunLinksTest.php tests/Feature/Monitoring/OperationsDashboardDrillthroughTest.php tests/Feature/Filament/RecentOperationsSummaryWidgetTest.php tests/Feature/Filament/InventoryCoverageRunContinuityTest.php tests/Feature/ReviewPack/ReviewPackResourceTest.php tests/Feature/144/CanonicalOperationViewerDeepLinkTrustTest.php tests/Feature/078/RelatedLinksOnDetailTest.php tests/Feature/RunAuthorizationTenantIsolationTest.php tests/Feature/System/Spec195/SystemDirectoryResidualSurfaceTest.php tests/Feature/System/Spec113/AuthorizationSemanticsTest.php tests/Feature/Guards/OperationRunLinkContractGuardTest.php
```
## Final Guard Boundary
The implemented guard is bounded to the first-slice source surfaces and explicit infrastructure exceptions:
- **Migrated admin producers**: `RecentOperationsSummary`, `InventoryCoverage`, `InventoryItemResource`, `ReviewPackResource`, `TenantlessOperationRunViewer`, and `RelatedNavigationResolver`.
- **Verified system producers**: `ViewTenant`, `ViewWorkspace`, `Runs`, and `ViewRun`, all continuing through `SystemOperationRunLinks`.
- **Accepted thin delegate**: `App\Support\OpsUx\OperationRunUrl`, which forwards to `OperationRunLinks`.
- **Allowlisted infrastructure exceptions**: `AdminPanelProvider`, `TenantPanelProvider`, `EnsureFilamentTenantSelected`, and `ClearTenantContextController`.
- **Forbidden bypasses inside the boundary**: raw `route('admin.operations.index')`, raw `route('admin.operations.view')`, direct `/system/ops/runs` strings, and direct `Runs::getUrl(...)` or `ViewRun::getUrl(...)` outside `SystemOperationRunLinks`.
## Manual Validation Flow
### 1. Validate tenant-aware admin collection continuity
1. Open a tenant-facing surface that exposes the recent-operations summary or an inventory coverage follow-up link.
2. Follow the `Open operations` or equivalent history link.
3. Confirm the destination stays on `/admin/operations` and preserves only helper-supported tenant or filter continuity.
4. Confirm the page does not invent a tenant-prefixed duplicate operations route.
### 2. Validate canonical admin detail links from representative resource surfaces
1. Open one inventory item with a `last_seen_operation_run_id`.
2. Follow the `Last inventory sync` link.
3. Open one review pack with an associated `operation_run_id`.
4. Confirm both links open canonical admin run detail, not a surface-local route or raw fallback URL.
### 3. Validate shared related-navigation and back-link behavior
1. Open a surface that renders an `operation_run` related link through `RelatedNavigationResolver`.
2. Confirm the helper-generated label and URL match canonical admin run detail behavior.
3. Open `TenantlessOperationRunViewer` through a source without an explicit back-link context.
4. Confirm `Back to Operations` and `Show all operations` land on the canonical admin collection helper path.
### 4. Validate system-plane continuity
1. Open a system-plane widget or directory page with run drill-through.
2. Follow collection and detail links into monitoring.
3. Confirm the destination stays on `/system/ops/runs` or `/system/ops/runs/{run}` and does not fall back to `/admin/operations`.
### 5. Validate authorization semantics stayed unchanged
1. As a workspace member who is not entitled to a foreign tenant, request a canonical admin detail URL for that tenants run.
2. Confirm the response remains `404`.
3. As a non-platform user, request a system-plane operations URL.
4. Confirm the response remains `404`.
5. As an entitled actor missing the relevant capability, confirm current destination behavior still yields `403` where the route already distinguishes membership from capability denial.
### 6. Validate the explicit exception boundary
1. Confirm that navigation boot, middleware, and clear-tenant redirect behavior still function after the cleanup.
2. Review the named allowlist entries and verify each remaining raw producer is infrastructure-owned rather than convenience-owned.
3. Confirm no new operator-facing page, widget, or related-navigation builder remains on raw `admin.operations.*` assembly outside the allowlist.
### 7. Validate the guardrail
1. Use a temporary local probe or test fixture to simulate one representative raw `route('admin.operations.view', ...)` bypass inside the declared guard boundary without committing it.
2. Run the guard test.
3. Confirm it fails with actionable file and snippet output.
4. Replace the bypass with the canonical helper or move it into an explicitly justified exception and confirm the guard passes again.
## Reviewer Notes
- The feature stays Livewire v4.0+ compatible and does not change provider registration in `bootstrap/providers.php`.
- No new global-search surface is introduced; `InventoryItemResource` already has a view page and `ReviewPackResource` remains non-searchable.
- No destructive action or new asset behavior is introduced.
- The contract boundary is intentionally narrow: platform-owned UI and shared navigation code only.

View File

@ -0,0 +1,67 @@
# Research: Operation Run Link Contract Enforcement
## Decision 1: Reuse the existing helper families instead of introducing a new link presenter
**Decision**: Treat `App\Support\OperationRunLinks` as the single admin-plane contract and `App\Support\System\SystemOperationRunLinks` as the single system-plane contract for all in-scope `OperationRun` collection and detail links.
**Rationale**: The existing helper families already own canonical nouns, labels, plane separation, and admin query semantics. The current defect is incomplete adoption, not missing capability. Reusing those helpers satisfies XCUT-001 with the smallest possible change.
**Alternatives considered**:
- Create a new cross-plane navigation presenter or registry. Rejected because the repository already has concrete helper ownership for the exact routes this feature targets.
- Fix each raw producer locally without naming a shared contract. Rejected because it would not stop the next contributor from reintroducing raw route assembly.
## Decision 2: Scope the first migration wave to the confirmed raw admin-plane producers
**Decision**: The first implementation slice migrates the currently confirmed raw admin-plane producers in `RecentOperationsSummary`, `InventoryCoverage`, `InventoryItemResource`, `ReviewPackResource`, `TenantlessOperationRunViewer`, and `RelatedNavigationResolver`.
**Rationale**: A targeted scan of `apps/platform/app` shows these files still assemble `admin.operations.index` or `admin.operations.view` links directly inside platform-owned UI or shared navigation code. They represent the smallest set of real producers needed to satisfy FR-232-003, FR-232-014, and FR-232-015.
**Alternatives considered**:
- Full opportunistic cleanup of every `admin.operations.*` string in the repository. Rejected because tests and legitimate infrastructure redirects would expand the slice into heavy-governance work.
- Migrate only one or two surfaces. Rejected because the drift is already spread across widget, page, resource, and shared-resolver layers.
## Decision 3: Keep the initial allowlist narrow and infrastructure-only
**Decision**: Seed the explicit exception boundary with bootstrapping or redirect-owned raw producers such as `AdminPanelProvider`, `TenantPanelProvider`, `EnsureFilamentTenantSelected`, and `ClearTenantContextController`, and require file-specific justification for each retained exception.
**Rationale**: These producers sit in navigation boot, middleware, or redirect code where runtime canonical navigation context is not the owning abstraction. Forcing helper usage there can fabricate the wrong dependency shape or hide a deliberate redirect contract.
**Alternatives considered**:
- Ban all raw route usage with no exceptions. Rejected because it would create false positives in infrastructure code and push the feature into a broader governance lane.
- Allow convenience-based exceptions. Rejected because convenience is exactly what created the current drift.
## Decision 4: Treat the system plane as contract lock-in, not broad migration work
**Decision**: Preserve `SystemOperationRunLinks` as the canonical system-plane path and focus the system portion of the feature on regression protection and authorization proof rather than manufactured cleanup work.
**Rationale**: The app-side scan shows current system widgets and pages already use `SystemOperationRunLinks` consistently. The feature should document and protect that convergence instead of inflating the slice with unnecessary system refactors.
**Alternatives considered**:
- Expand the scope into general system routing cleanup. Rejected because the current-release problem is not missing on the system plane in app code.
- Ignore the system plane entirely. Rejected because FR-232-002, FR-232-008, and FR-232-016 still require explicit system-plane continuity protection.
## Decision 5: The guard stays route-bounded and app-side only
**Decision**: The new guard scans only the declared platform-owned UI and shared-navigation boundary in `apps/platform/app` and fails on raw `admin.operations.*` route assembly or direct system operations page URLs outside the explicit allowlist.
**Rationale**: The proving purpose is preventing new operator-facing bypasses, not banning route literals everywhere in the repository. A route-bounded guard keeps the feature in `fast-feedback + confidence` instead of escalating it into a structural repo-governance family.
**Alternatives considered**:
- Whole-repo scanning including tests and unrelated infrastructure. Rejected because it would create noisy failures and require a much broader allowlist.
- No guard at all. Rejected because the cleanup would then be a one-time repair with no contract enforcement.
## Decision 6: Keep thin delegating wrappers out of the violation set
**Decision**: Treat `App\Support\OpsUx\OperationRunUrl` as an acceptable thin seam because it delegates directly to `OperationRunLinks` and does not introduce parallel route truth.
**Rationale**: The spec targets raw bypass of the canonical helper families, not every intermediate call site that already converges on them. Counting a delegating wrapper as a violation would add churn without improving contract safety.
**Alternatives considered**:
- Force all wrapper users to switch to direct helper calls in the same slice. Rejected because the wrapper is not the source of drift and does not block enforcement of the raw-route contract.
- Allow arbitrary wrappers. Rejected because only pure delegating seams remain acceptable; a wrapper that adds its own routing logic would reintroduce the same problem.

View File

@ -0,0 +1,302 @@
# Feature Specification: Operation Run Link Contract Enforcement
**Feature Branch**: `232-operation-run-link-contract`
**Created**: 2026-04-23
**Status**: Draft
**Input**: User description: "schau in spec-candidate und roadmap rein und wähle das nächste sinnvolle spec"
## Spec Candidate Check *(mandatory — SPEC-GATE-001)*
- **Problem**: Platform-owned admin and system surfaces still emit `OperationRun` collection and detail links through raw route assembly instead of the shared helper families, so the same canonical operations destinations have two parallel generation paths.
- **Today's failure**: Operators can reach the right pages through URLs that ignore tenant-prefilter or plane semantics, while contributors can keep adding raw operation routes in shared UI code without tripping a contract failure.
- **User-visible improvement**: Operation links behave consistently across dashboards, resources, monitoring pages, and shared related-navigation so operators land in the canonical destination with the expected plane and scope continuity.
- **Smallest enterprise-capable version**: Inventory all platform-owned operation collection/detail link producers, migrate raw admin-plane producers to `OperationRunLinks`, verify already-helper-backed system producers stay on `SystemOperationRunLinks`, record the narrow allowlisted exceptions, and add one automated guard that blocks new raw bypasses.
- **Explicit non-goals**: No operations IA redesign, no page-content rewrite, no status-semantics change, no new tenant-specific operations route family, and no platform-wide routing cleanup beyond `OperationRun` collection/detail links.
- **Permanent complexity imported**: One explicit allowlist for legitimate infrastructure-only exceptions, narrow regression coverage for admin-plane and system-plane link producers, and small helper-contract extensions only if current canonical context parameters are incomplete.
- **Why now**: Governance & Architecture Hardening is the active roadmap block, and the 2026-04-22 drift audit identified this as the first high-leverage contract-enforcement slice before more operations surfaces or navigation retrofits land.
- **Why not local**: The drift exists across shared navigation and multiple platform-owned surfaces. A one-off fix on one widget or resource would leave the same contract bypass available everywhere else.
- **Approval class**: Cleanup
- **Red flags triggered**: Cross-cutting interaction-class scope and repository guardrail addition. Defense: the helper families already exist, the feature only makes them mandatory on platform-owned paths, and the guard stays bounded to explicit allowlisted exceptions instead of introducing a new framework.
- **Score**: Nutzen: 2 | Dringlichkeit: 2 | Scope: 2 | Komplexität: 2 | Produktnähe: 1 | Wiederverwendung: 2 | **Gesamt: 11/12**
- **Decision**: approve
## Spec Scope Fields *(mandatory)*
- **Scope**: workspace + canonical-view + system
- **Primary Routes**:
- `/admin/operations`
- `/admin/operations/{run}`
- `/system/ops/runs`
- `/system/ops/runs/{run}`
- Existing platform-owned source surfaces that drill into those canonical destinations, including tenant recency widgets, inventory coverage follow-up links, review-pack operation links, panel navigation shortcuts, and shared related-navigation builders
- **Data Ownership**:
- No new persisted entity or ownership model is introduced.
- `operation_runs` remain the existing operational source of truth, tenant-owned in the admin plane with workspace-scoped canonical viewing rules and platform-plane visibility through existing system monitoring surfaces.
- Workspace- or system-page filter state remains derived navigation state only; this feature does not persist copied link state or introduce a second run-navigation record.
- **RBAC**:
- Admin-plane monitoring links continue to require workspace membership; tenant-linked destinations continue to enforce tenant entitlement and existing operations-view capabilities server-side.
- System-plane monitoring links continue to require authenticated platform users with existing system operations access.
- Cross-plane access remains deny-as-not-found; this feature does not introduce new capabilities or raw capability strings.
For canonical-view specs, the spec MUST define:
- **Default filter behavior when tenant-context is active**: Admin-plane collection links may preserve the active entitled tenant through the canonical `OperationRunLinks` collection contract. Admin-plane run detail remains record-authoritative and may carry canonical navigation context, but must not invent a tenant-specific duplicate detail route. System-plane links never inherit tenant-context filters from `/admin`.
- **Explicit entitlement checks preventing cross-tenant leakage**: Helper-generated links may carry only entitled tenant/query context; destination pages re-check workspace membership, tenant entitlement, and plane-specific access before rendering. A link from a covered surface must never make a foreign-tenant run or system route newly observable.
## Cross-Cutting / Shared Pattern Reuse *(mandatory when the feature touches notifications, status messaging, action links, header actions, dashboard signals/cards, alerts, navigation entry points, 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)**: navigation, action links, related links, deep links
- **Systems touched**: admin monitoring surfaces, system operations surfaces, shared related-navigation builders, panel shortcuts, and tenant/workspace drill-through links from recency and evidence-oriented surfaces
- **Existing pattern(s) to extend**: canonical operation link helpers, canonical navigation context propagation, and existing operations row-click/detail conventions
- **Shared contract / presenter / builder / renderer to reuse**: `App\Support\OperationRunLinks`, `App\Support\System\SystemOperationRunLinks`, and `App\Support\Navigation\CanonicalNavigationContext`
- **Why the existing shared path is sufficient or insufficient**: The shared path is already sufficient for collection/detail labels, tenant-prefilter continuity, active-tab/problem-class query semantics, and plane separation. The current gap is not missing capability but incomplete adoption where raw `route('admin.operations...')` or direct system page URLs still bypass the contract.
- **Allowed deviation and why**: Bounded infrastructure-only exceptions may remain when the producer cannot depend on the helper family without introducing the wrong runtime dependency or hiding a deliberate redirect contract. Every retained exception must be explicit and allowlisted.
- **Consistency impact**: `Operations` / `Operation` nouns, `Open operations` / `Open operation` labels, admin versus system plane routing, tenant-prefilter continuity, canonical back-link behavior, and shared related-link structure must stay aligned across all covered producers.
- **Review focus**: Reviewers must verify that new or changed platform-owned operation links use the correct helper family for their plane and that no new raw collection/detail route assembly appears outside the explicit allowlist.
## UI / Surface Guardrail Impact *(mandatory when operator-facing surfaces are changed; otherwise write `N/A`)*
Use this section to classify UI and surface risk once. If the feature does
not change an operator-facing surface, write `N/A - no operator-facing surface
change` here and do not invent duplicate prose in the downstream surface tables.
| Surface / Change | Operator-facing surface change? | Native vs Custom | Shared-Family Relevance | State Layers Touched | Exception Needed? | Low-Impact / `N/A` Note |
|---|---|---|---|---|---|---|
| Admin monitoring collection/detail link contract enforcement | yes | Native Filament + shared helpers | navigation / related links | table, detail header, linked widgets | no | Destination pages stay the same; only link generation is standardized |
| System operations collection/detail link contract enforcement | yes | Native Filament + shared helpers | navigation / related links | table, detail header | no | Destination pages stay the same; plane separation becomes explicit |
| Shared operation drill-through links from tenant/workspace surfaces | yes | Native Filament widgets/resources + shared helpers | navigation | widget, table cell, infolist/detail supporting links | no | Low-impact UI change; same destinations, stricter continuity semantics |
## Decision-First Surface Role *(mandatory when operator-facing surfaces are changed)*
If this feature adds or materially changes an operator-facing surface,
fill out one row per affected surface. This role is orthogonal to the
Action Surface Class / Surface Type below. Reuse the exact surface names
and classifications from the UI / Surface Guardrail Impact section above.
| 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 |
|---|---|---|---|---|---|---|---|
| Admin monitoring collection and run detail | Secondary Context Surface | Open canonical monitoring after a tenant/workspace surface signals follow-up | Correct destination family, preserved tenant scope when valid, stable canonical operation identity | Full run diagnostics, raw context, related artifacts | Secondary because the decision to inspect happens on the source surface; monitoring remains the canonical context surface for follow-up and diagnosis | Follows existing dashboard/resource-to-monitoring workflow instead of creating duplicate run pages | Removes operator guesswork about which operations page and which scope a link should open |
| System operations collection and run detail | Secondary Context Surface | Open platform-plane run monitoring from system runbooks or system registries | Correct system destination, preserved platform plane, stable run identity | Full system run diagnostics and runbook context | Secondary because it supports platform triage after a runbook or system list already framed the operator task | Follows existing `/system` triage workflow and keeps system links out of `/admin` | Removes plane confusion and manual route reconstruction for platform operators |
## UI/UX Surface Classification *(mandatory when operator-facing surfaces are changed)*
If this feature adds or materially changes an operator-facing list, detail, queue, audit, config, or report surface,
fill out one row per affected surface. Declare the broad Action Surface
Class first, then the detailed Surface Type. Keep this table in sync
with the Decision-First Surface Role section above and avoid renaming the
same surface a second time.
| 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 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Admin monitoring collection | Read-only Registry / Report | Canonical monitoring registry | Open operation detail or inspect filtered run history | Full-row click or canonical helper-generated open link | required | Header/context filters and source-surface link affordances only | No new destructive placement; existing detail-only interventions remain unchanged | `/admin/operations` | `/admin/operations/{run}` | Workspace scope, optional entitled tenant filter, canonical navigation context | Operations / Operation | The operator can tell they are entering canonical admin monitoring and whether tenant context was preserved | none |
| Admin operation run detail | Canonical detail | Diagnostic run detail | Inspect one run without losing canonical plane semantics | Direct route-resolved detail page | forbidden | Related links and back-navigation remain secondary in the header/body | No new destructive actions in this feature | `/admin/operations` | `/admin/operations/{run}` | Workspace scope, entitled tenant context if present, run identity | Operations / Operation | The canonical run identity and plane are obvious without interpreting source-surface hacks | none |
| System operations collection | Read-only Registry / Report | Platform monitoring registry | Open system run detail | Full-row click or canonical helper-generated open link | required | Header filters or runbook follow-up only | Existing system interventions remain separately governed | `/system/ops/runs` | `/system/ops/runs/{run}` | Platform scope only | Operations / Operation | The operator can tell they are staying in the system plane | none |
| System run detail | Detail / Decision | Platform run triage detail | Inspect one platform run in the correct plane | Direct route-resolved detail page | forbidden | Header/context links only | Existing system-detail interventions remain separately governed | `/system/ops/runs` | `/system/ops/runs/{run}` | Platform scope, run identity, system context | Operations / Operation | The canonical system-run destination is explicit and not silently downgraded to admin monitoring | none |
## Operator Surface Contract *(mandatory when operator-facing surfaces are changed)*
If this feature adds a new operator-facing page or materially refactors
one, fill out one row per affected page/surface. The contract MUST show
how one governance case or operator task becomes decidable without
unnecessary cross-page reconstruction.
| 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 |
|---|---|---|---|---|---|---|---|---|---|---|
| Admin monitoring collection and run detail | Workspace operator | Follow a source-surface signal into canonical operation history or one canonical run detail | Registry + detail | Did I land on the right canonical operations surface with the right scope, and can I inspect the run from here? | Canonical collection/detail destination, run identity, workspace scope, entitled tenant continuity where applicable | Raw context payloads, error traces, and extended related links | lifecycle, outcome, problem class, scope continuity | No new mutation in this slice | Open operation, Open operations | None added by this feature |
| System operations collection and run detail | Platform operator | Follow runbook or system registry context into platform-plane run history or one system run detail | Registry + detail | Am I staying in the system plane, and am I opening the correct run history/detail surface? | Canonical system destination, run identity, platform scope | Raw run context, failure payloads, runbook lineage | lifecycle, outcome, platform scope | No new mutation in this slice | Open operation, Open operations | None added by this feature |
## 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**: Covered surfaces can bypass the existing canonical operation-link contract, which makes plane and scope continuity drift across the same operations destinations.
- **Existing structure is insufficient because**: The helpers exist but are not enforced. Without one explicit enforcement slice and guardrail, every new platform-owned surface can keep choosing local route assembly.
- **Narrowest correct implementation**: Reuse the existing helper families, migrate all in-scope producers, and add one bounded allowlist guard instead of inventing a broader routing framework or new navigation model.
- **Ownership cost**: Small ongoing allowlist review, targeted regression maintenance, and occasional helper extensions when new canonical query semantics are introduced.
- **Alternative intentionally rejected**: A one-off cleanup of currently known raw routes without a guardrail was rejected because it would not stop the same drift from reappearing on the next surface.
- **Release truth**: current-release contract enforcement and cleanup, not future-platform preparation
### Compatibility posture
This feature assumes a pre-production environment.
Backward compatibility, legacy aliases, migration shims, historical fixtures, and compatibility-specific tests are out of scope unless explicitly required by this spec.
Canonical replacement is preferred over preservation.
## Testing / Lane / Runtime Impact *(mandatory for runtime behavior changes)*
For docs-only or template-only changes, state concise `N/A` or `none`. For runtime- or test-affecting work, classification MUST follow the proving purpose of the change rather than the file path or folder name.
- **Test purpose / classification**: Feature
- **Validation lane(s)**: fast-feedback + confidence
- **Why this classification and these lanes are sufficient**: This change is proved by end-to-end URL generation and plane/scope outcomes on real admin/system surfaces, not by isolated string helpers alone. Fast-feedback covers representative admin/system surfaces and the guard. Confidence reruns the broader operations link-contract coverage already present in the monitoring family.
- **New or expanded test families**: Extend existing canonical run-link coverage for admin and system surfaces; add one focused guard test that blocks new raw platform-owned operation routes outside the allowlist.
- **Fixture / helper cost impact**: Minimal. Existing `OperationRun` factories, workspace/tenant membership helpers, and platform-user fixtures are sufficient.
- **Heavy-family visibility / justification**: none
- **Special surface test profile**: monitoring-state-page
- **Standard-native relief or required special coverage**: Requires shared-link contract coverage on representative widgets/resources plus one repository guard check; no browser or heavy-governance suite is needed.
- **Reviewer handoff**: Reviewers must confirm that each migrated producer emits helper-generated URLs for the correct plane, that allowlisted exceptions are explicitly justified, and that the guard pattern cannot be trivially bypassed.
- **Budget / baseline / trend impact**: none
- **Escalation needed**: none
- **Active feature PR close-out entry**: Guardrail
- **Planned validation commands**:
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent`
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/OpsUx/CanonicalViewRunLinksTest.php tests/Feature/Monitoring/OperationsDashboardDrillthroughTest.php tests/Feature/Filament/RecentOperationsSummaryWidgetTest.php tests/Feature/Filament/InventoryCoverageRunContinuityTest.php tests/Feature/ReviewPack/ReviewPackResourceTest.php tests/Feature/144/CanonicalOperationViewerDeepLinkTrustTest.php tests/Feature/078/RelatedLinksOnDetailTest.php tests/Feature/RunAuthorizationTenantIsolationTest.php tests/Feature/System/Spec195/SystemDirectoryResidualSurfaceTest.php tests/Feature/System/Spec113/AuthorizationSemanticsTest.php tests/Feature/Guards/OperationRunLinkContractGuardTest.php`
## User Scenarios & Testing *(mandatory)*
### User Story 1 - Follow Admin Operations Links Consistently (Priority: P1)
As a workspace operator, I want platform-owned admin surfaces to open canonical operations collection/detail links through one shared contract so that tenant-prefilter and run-detail continuity are consistent.
**Why this priority**: This is the direct operator-facing path. If admin-plane drill-through links remain inconsistent, the canonical monitoring experience stays fragile even when the destination pages themselves are correct.
**Independent Test**: Can be fully tested by opening operations links from covered tenant/workspace surfaces such as recency widgets, inventory coverage, and review-pack detail and confirming that the links land on the canonical admin collection/detail destinations with the expected scope continuity.
**Acceptance Scenarios**:
1. **Given** an active entitled tenant context, **When** an operator opens operations from a covered admin-plane surface, **Then** the link resolves to `/admin/operations` or `/admin/operations/{run}` with the expected tenant/context continuity and without inventing a duplicate route family.
2. **Given** no tenant context is active, **When** a covered admin-plane surface links to operations, **Then** it opens workspace-wide admin monitoring rather than inventing or leaking tenant scope.
3. **Given** a run belongs to a tenant the actor is not entitled to inspect, **When** the actor requests the destination, **Then** the destination still resolves as deny-as-not-found.
---
### User Story 2 - Keep System-Plane Run Links in the System Plane (Priority: P1)
As a platform operator, I want system run lists and runbooks to open canonical system operations URLs so that platform monitoring never silently routes me through the admin plane.
**Why this priority**: Plane correctness is a core trust requirement. A system operator following a run link to the wrong plane is both confusing and authorization-sensitive.
**Independent Test**: Can be fully tested by opening run history and run detail from covered `/system` surfaces and confirming that the destination remains `/system/ops/runs` or `/system/ops/runs/{run}` for platform users while tenant/admin users still cannot access those routes.
**Acceptance Scenarios**:
1. **Given** a system-plane list or follow-up link, **When** the platform operator opens a run, **Then** the URL is `/system/ops/runs/{run}` rather than an admin-plane monitoring route.
2. **Given** a system-plane collection link, **When** the platform operator opens history, **Then** the URL is `/system/ops/runs`.
3. **Given** a tenant/admin user session, **When** that user requests a system-plane destination, **Then** the response remains deny-as-not-found.
---
### User Story 3 - Prevent New Raw Operation-Link Bypasses (Priority: P2)
As a maintainer, I want a guardrail that fails when platform-owned UI introduces new raw operation routes outside allowlisted exceptions so that the contract stays enforced after cleanup.
**Why this priority**: The cleanup only holds if the next contributor cannot quietly reintroduce the same drift on a new surface.
**Independent Test**: Can be fully tested by introducing a representative raw route bypass in a covered area, observing the guard fail, then moving the same producer to the helper family or explicit allowlist and observing the guard pass.
**Acceptance Scenarios**:
1. **Given** a new platform-owned UI producer assembles `route('admin.operations...')` or direct system page URLs outside the allowlist, **When** the guard runs, **Then** the build fails with an actionable message.
2. **Given** an infrastructure-only exception is explicitly allowlisted, **When** the guard runs, **Then** it passes without forcing fake helper usage.
3. **Given** a new covered surface needs an additional canonical query semantic, **When** the contributor extends the helper family, **Then** the surface adopts the helper rather than adding a second local route pattern.
### Edge Cases
- A source surface has stale or absent tenant context; the admin collection link must degrade to workspace-wide canonical monitoring rather than carrying invalid tenant state.
- A source surface wants to prefilter by problem class or active tab; query semantics must be emitted through the helper contract only, not handwritten per surface.
- A run detail link is built from a tenant surface but the run belongs to a tenant the current actor can no longer access; opening the destination must still fail as `404`.
- A boot-time panel item or controller redirect cannot safely depend on view-context objects; if it remains raw, it must be explicitly allowlisted and justified.
- System-plane and admin-plane links for the same run ID must never resolve through the wrong plane by helper accident or local override.
## Requirements *(mandatory)*
**Constitution alignment (required):** This feature introduces no Microsoft Graph calls, no new mutation workflow, and no new `OperationRun` type. It standardizes link generation to already-shipped canonical operations destinations and must therefore make tenant isolation, plane separation, shared-link reuse, and regression coverage explicit.
**Constitution alignment (PROP-001 / ABSTR-001 / PERSIST-001 / STATE-001 / BLOAT-001):** This feature does not introduce new persistence, a new abstraction family, or new states. It deliberately reuses the existing helper families and adds only the narrowest enforcement mechanism needed to stop repeated drift on a current-release operator path.
**Constitution alignment (XCUT-001):** This is a cross-cutting navigation and action-link feature. The shared path is `OperationRunLinks` for the admin plane and `SystemOperationRunLinks` for the system plane, with `CanonicalNavigationContext` carrying canonical admin query state where needed. Any retained raw-route exception must be explicit, justified, and reviewable.
**Constitution alignment (TEST-GOV-001):** The proving purpose is runtime link behavior on real admin/system surfaces plus one repository guard. Feature tests and a focused guard are the narrowest sufficient proof. Existing factories and membership fixtures remain enough, and no heavy-governance or browser family is justified.
**Constitution alignment (OPS-UX):** Not applicable. This feature does not create, start, or mutate `OperationRun` records. Existing operations destinations remain the canonical Ops-UX surfaces and keep their current service-owned lifecycle semantics.
**Constitution alignment (RBAC-UX):** The feature spans the admin `/admin` plane and the platform `/system` plane. Cross-plane access remains `404`. Admin-plane destinations continue to enforce workspace membership first and tenant entitlement when the run is tenant-bound. System-plane destinations continue to enforce platform-user access only. The feature must add at least one positive and one negative authorization-path regression covering the affected link destinations.
**Constitution alignment (OPS-EX-AUTH-001):** Not applicable. No authentication handshake behavior changes.
**Constitution alignment (BADGE-001):** Not applicable. No badge semantics are introduced or changed.
**Constitution alignment (UI-FIL-001):** The affected surfaces remain native Filament pages, widgets, and resources. No local replacement markup is needed. Semantic emphasis stays in existing tables, related-link areas, and header affordances while the destination URLs are delegated to the shared helper families.
**Constitution alignment (UI-NAMING-001):** The target object is the canonical operation destination. Operator verbs remain `Open operations`, `Open operation`, and `View in Operations` where those labels are already defined by the helper family. Source/domain disambiguation is only plane-based: admin-plane links open admin monitoring, system-plane links open system monitoring.
**Constitution alignment (DECIDE-001):** The affected surfaces are Secondary Context Surfaces. Their responsibility is not to create a new decision queue but to preserve a calm, predictable path from source surfaces into canonical monitoring without forcing operators to reconstruct plane or scope by hand.
**Constitution alignment (UI-CONST-001 / UI-SURF-001 / ACTSURF-001 / UI-HARD-001 / UI-EX-001 / UI-REVIEW-001 / HDR-001):** This feature does not add new visible row or header actions. It preserves one primary inspect/open model per affected surface, keeps pure navigation in row click or supporting related links, leaves destructive actions where existing destinations already govern them, and does not introduce a second operations route family.
**Constitution alignment (ACTSURF-001 - action hierarchy):** No new action hierarchy is introduced. Navigation remains separate from mutation, existing detail-only interventions remain where they are, and the feature does not turn route cleanup into a new action chrome pattern.
**Constitution alignment (OPSURF-001):** The default-visible operator experience remains operator-first because the change is to destination continuity, not content density. Raw route tokens and local path assembly must not leak into surface-specific logic. Diagnostics remain on the destination detail pages, not in the link producers.
**Constitution alignment (UI-SEM-001 / LAYER-001 / TEST-TRUTH-001):** No new UI-semantic or presenter layer is introduced. Direct helper reuse is preferred over another interpretation layer, and tests focus on business consequences: correct plane, correct destination, correct scope continuity, and blocked drift.
**Constitution alignment (Filament Action Surfaces):** The Action Surface Contract remains satisfied. Each affected surface keeps exactly one primary inspect/open model, redundant `View` actions are not introduced by this feature, empty placeholder groups remain forbidden, and destructive actions remain governed by the existing destination pages. `UI-FIL-001` is satisfied with no exception.
**Constitution alignment (UX-001 — Layout & Information Architecture):** Existing operations pages, widgets, and resources keep their current layouts, infolists, table search/sort/filter behavior, and empty-state structure. No layout exemption is needed because this slice changes destination generation only.
### Functional Requirements
- **FR-232-001**: The system MUST treat `App\Support\OperationRunLinks` as the canonical admin-plane generator for platform-owned links to `/admin/operations` and `/admin/operations/{run}`.
- **FR-232-002**: The system MUST treat `App\Support\System\SystemOperationRunLinks` as the canonical system-plane generator for platform-owned links to `/system/ops/runs` and `/system/ops/runs/{run}`.
- **FR-232-003**: The first implementation slice MUST inventory all platform-owned collection/detail link producers for `OperationRun` destinations and classify each producer as migrated, verified helper-backed, or explicit exception.
- **FR-232-004**: Covered admin-plane producers MUST stop assembling raw `route('admin.operations.index')` or `route('admin.operations.view')` URLs locally.
- **FR-232-005**: Covered system-plane producers MUST remain on `SystemOperationRunLinks`, and any direct system operations page URL assembly outside explicit allowlisted infrastructure-only cases MUST be removed.
- **FR-232-006**: Admin-plane collection links originating from tenant-aware surfaces MUST preserve only valid canonical context supported by `OperationRunLinks`, including entitled tenant prefilter, active tab, problem class, and canonical navigation context when applicable.
- **FR-232-007**: Admin-plane run-detail links MUST use the canonical admin detail helper and MUST NOT invent tenant-prefixed or surface-specific duplicate detail routes.
- **FR-232-008**: System-plane links MUST never route platform operators to admin-plane monitoring pages as the default destination for system operations history or detail.
- **FR-232-009**: The helper contract MUST remain the only source for the canonical operator-facing nouns and open labels used by covered admin-plane operation links.
- **FR-232-010**: Existing admin-plane and system-plane destination pages MUST keep their current authorization and plane semantics; this feature MUST NOT widen access or weaken `404` versus `403` behavior.
- **FR-232-011**: Explicit exceptions to helper-family reuse MUST be documented, narrowly scoped, and justified by infrastructure or bootstrapping constraints rather than convenience.
- **FR-232-012**: The repository MUST provide one automated guard that fails when new platform-owned operation collection/detail links bypass the canonical helper families outside the explicit allowlist.
- **FR-232-013**: The guard MUST target platform-owned UI and shared navigation layers only and MUST NOT force helper use inside the helper classes themselves, unrelated tests, or non-operator infrastructure code outside the declared boundary.
- **FR-232-014**: Shared navigation builders that produce `operation_run` links, including audit- or related-navigation paths in scope, MUST use the canonical helper family for the destination plane instead of local route assembly.
- **FR-232-015**: Representative tenant/workspace source surfaces in scope, including at least one tenant recency surface, one evidence- or review-oriented surface, and one shared resolver path, MUST be migrated in the first rollout.
- **FR-232-016**: Regression coverage MUST prove correct admin-plane destination continuity, correct system-plane destination continuity, and guard failure on a representative bypass.
- **FR-232-017**: The feature MUST NOT introduce a new operations collection/detail route family, a second link presenter stack, or a separate tenant-local operations page.
## UI Action Matrix *(mandatory when Filament is changed)*
If this feature adds/modifies any Filament Resource / RelationManager / Page, fill out the matrix below.
For each surface, list the exact action labels, whether they are destructive (confirmation? typed confirmation?),
RBAC gating (capability + enforcement helper), whether the mutation writes an audit log, and any exemption or exception used.
| Surface | Location | Header Actions | Inspect Affordance (List/Table) | Row Actions (max 2 visible) | Bulk Actions (grouped) | Empty-State CTA(s) | View Header Actions | Create/Edit Save+Cancel | Audit log? | Notes / Exemptions |
|---|---|---|---|---|---|---|---|---|---|---|
| Admin Operations index | `apps/platform/app/Filament/Pages/Monitoring/Operations.php` | Existing filter/context actions only | Full-row click and helper-generated incoming links to the canonical list | none added by this feature | none added by this feature | Existing clear-filter or empty-state CTA unchanged | n/a | n/a | no new audit behavior | Link contract only; no redundant `View` action added |
| Admin operation run detail | `apps/platform/app/Filament/Pages/Operations/TenantlessOperationRunViewer.php` | Existing back/refresh/context actions only | Direct route-resolved detail plus helper-generated incoming links | none added by this feature | none | n/a | Existing header navigation and related links only | n/a | no new audit behavior | Incoming links and related links must use the canonical admin helper |
| System operations runs | `apps/platform/app/Filament/System/Pages/Ops/Runs.php` | Existing filters or context actions only | Full-row click and helper-generated incoming links | none added by this feature | none added by this feature | Existing clear-filter CTA unchanged | n/a | n/a | no new audit behavior | Collection destination must stay in system plane |
| System operation run detail | `apps/platform/app/Filament/System/Pages/Ops/ViewRun.php` | Existing refresh/contextual actions only | Direct route-resolved detail plus helper-generated incoming links | none added by this feature | none | n/a | Existing detail actions unchanged | n/a | no new audit behavior | Incoming links must use the canonical system helper |
### Key Entities *(include if feature involves data)*
- **Admin Operation Link Contract**: The canonical helper-generated admin monitoring destination, including collection/detail route choice and any allowed tenant- or navigation-context query semantics.
- **System Operation Link Contract**: The canonical helper-generated system monitoring destination for platform-plane run history and run detail.
- **Allowlisted Link Producer Exception**: A narrowly justified infrastructure-only producer that is explicitly permitted to bypass the helper family without becoming a general precedent for platform-owned UI code.
## Success Criteria *(mandatory)*
### Measurable Outcomes
- **SC-232-001**: In automated regression coverage, 100% of covered admin-plane operation links open the canonical admin collection or detail destination with the expected tenant/workspace continuity.
- **SC-232-002**: In automated regression coverage, 100% of covered system-plane operation links open `/system/ops/runs` or `/system/ops/runs/{run}`, and 0 covered system links fall back to admin-plane monitoring.
- **SC-232-003**: The guardrail fails on a representative raw-route bypass and passes only for explicitly allowlisted exceptions in automated review coverage.
- **SC-232-004**: Negative authorization coverage confirms that covered links do not convert foreign-tenant or wrong-plane destinations into successful opens for unauthorized actors.
## Assumptions
- `OperationRunLinks` and `SystemOperationRunLinks` remain the chosen canonical helper families for this release rather than being replaced by a broader navigation framework.
- Existing admin-plane and system-plane operations destination pages already enforce the correct authorization and scope semantics; this feature fixes link production, not destination legitimacy.
- The known drift is concentrated in platform-owned UI and shared navigation layers rather than external integrations or public API contracts.
## Non-Goals
- Redesigning operations list/detail layout, action hierarchy, or lifecycle semantics
- Reworking failures or stuck route families into a broader system monitoring link framework beyond the canonical runs collection/detail seam
- Cleaning every historical test literal or non-operator infrastructure route string unrelated to platform-owned UI or shared navigation layers

View File

@ -0,0 +1,228 @@
# Tasks: Operation Run Link Contract Enforcement
**Input**: Design documents from `/specs/232-operation-run-link-contract/`
**Prerequisites**: `plan.md` (required), `spec.md` (required for user stories), `research.md`, `data-model.md`, `contracts/operation-run-link-contract.logical.openapi.yaml`, `quickstart.md`
**Tests**: Required. This feature changes runtime behavior in operator-facing monitoring drill-through and shared link-contract enforcement, so Pest coverage must be added or updated in `apps/platform/tests/Feature/OpsUx/CanonicalViewRunLinksTest.php`, `apps/platform/tests/Feature/Monitoring/OperationsDashboardDrillthroughTest.php`, `apps/platform/tests/Feature/Filament/RecentOperationsSummaryWidgetTest.php`, `apps/platform/tests/Feature/Filament/InventoryCoverageRunContinuityTest.php`, `apps/platform/tests/Feature/ReviewPack/ReviewPackResourceTest.php`, `apps/platform/tests/Feature/144/CanonicalOperationViewerDeepLinkTrustTest.php`, `apps/platform/tests/Feature/078/RelatedLinksOnDetailTest.php`, `apps/platform/tests/Feature/RunAuthorizationTenantIsolationTest.php`, `apps/platform/tests/Feature/System/Spec195/SystemDirectoryResidualSurfaceTest.php`, `apps/platform/tests/Feature/System/Spec113/AuthorizationSemanticsTest.php`, and `apps/platform/tests/Feature/Guards/OperationRunLinkContractGuardTest.php`.
**Operations**: No new `OperationRun` is introduced. Existing admin and system monitoring pages remain the canonical destination surfaces, and this feature must not change run lifecycle, notification, or audit semantics.
**RBAC**: The feature spans the admin `/admin` plane and the platform `/system` plane. It must preserve non-member or wrong-plane `404`, in-scope missing-capability `403`, tenant-safe canonical admin continuity, and current platform-only system access semantics.
**UI / Surface Guardrails**: The changed surfaces are native Filament widgets, pages, resources, and shared navigation builders. The admin monitoring entry points keep the `monitoring-state-page` profile, the remaining surfaces take `standard-native-filament` relief, and the repository signal is `review-mandatory` because the feature adds a bounded guard plus explicit exceptions.
**Filament UI Action Surfaces**: `RecentOperationsSummary`, `InventoryCoverage`, `InventoryItemResource`, `ReviewPackResource`, `TenantlessOperationRunViewer`, and the residual system directory pages keep their existing inspect/open model. No new header, row, bulk, or destructive actions are introduced.
**Badges**: Existing status and outcome badge semantics remain authoritative. This feature must not add ad-hoc badge mappings or a new status taxonomy.
**Organization**: Tasks are grouped by user story so each slice remains independently testable after the shared helper and guard boundary are stabilized. Recommended delivery order is `US1 -> US2 -> US3` because the admin-plane cleanup is the primary migration slice and the guard should close only after the final migrated surface set and exceptions are settled.
## Test Governance Checklist
- [X] Lane assignment is named and is the narrowest sufficient proof for the changed behavior.
- [X] New or changed tests stay in the smallest honest family, and any heavy-governance or browser addition is explicit.
- [X] Shared helpers, factories, seeds, fixtures, and context defaults stay cheap by default; any widening is isolated or documented.
- [X] Planned validation commands cover the change without pulling in unrelated lane cost.
- [X] The declared surface test profile or `standard-native-filament` relief is explicit.
- [X] Any material budget, baseline, trend, or escalation note is recorded in the active spec or PR.
## Phase 1: Setup (Shared Link Contract Scaffolding)
**Purpose**: Prepare the focused regression surfaces that will prove canonical admin and system link behavior before runtime files are edited.
- [X] T001 [P] Extend baseline helper contract coverage for canonical admin and system collection/detail URLs in `apps/platform/tests/Feature/OpsUx/CanonicalViewRunLinksTest.php`
- [X] T002 [P] Extend tenant-summary and dashboard drill-through coverage for canonical admin collection links in `apps/platform/tests/Feature/Filament/RecentOperationsSummaryWidgetTest.php` and `apps/platform/tests/Feature/Monitoring/OperationsDashboardDrillthroughTest.php`
- [X] T003 [P] Extend resource-level admin detail continuity coverage in `apps/platform/tests/Feature/Filament/InventoryCoverageRunContinuityTest.php` and `apps/platform/tests/Feature/ReviewPack/ReviewPackResourceTest.php`
- [X] T004 [P] Extend shared resolver, canonical viewer, and system-plane continuity scaffolding in `apps/platform/tests/Feature/144/CanonicalOperationViewerDeepLinkTrustTest.php`, `apps/platform/tests/Feature/078/RelatedLinksOnDetailTest.php`, `apps/platform/tests/Feature/System/Spec195/SystemDirectoryResidualSurfaceTest.php`, and `apps/platform/tests/Feature/System/Spec113/AuthorizationSemanticsTest.php`
**Checkpoint**: The focused test surfaces are ready to prove canonical helper adoption, system-plane continuity, and bounded guard behavior.
---
## Phase 2: Foundational (Blocking Helper And Guard Boundary)
**Purpose**: Stabilize the canonical helper contract and the route-bounded guard before any user story migration begins.
**Critical**: No user story work should begin until this phase is complete.
- [X] T005 Freeze canonical helper inputs, labels, and accepted delegation boundaries in `apps/platform/app/Support/OperationRunLinks.php`, `apps/platform/app/Support/System/SystemOperationRunLinks.php`, and `apps/platform/app/Support/OpsUx/OperationRunUrl.php`
- [X] T006 [P] Create the bounded raw-bypass guard with scoped include paths, explicit exception candidates, forbidden patterns, and actionable file-plus-snippet output in `apps/platform/tests/Feature/Guards/OperationRunLinkContractGuardTest.php`
**Checkpoint**: Canonical helper semantics and the initial guard boundary are stable enough for surface-by-surface migration work.
---
## Phase 3: User Story 1 - Follow Admin Operations Links Consistently (Priority: P1)
**Goal**: Platform-owned admin surfaces open canonical operations collection and detail URLs through `OperationRunLinks` with the correct tenant and navigation continuity.
**Independent Test**: Open operations links from the tenant summary widget, dashboard drill-throughs, inventory coverage, review packs, and related-link surfaces, then confirm they land on `/admin/operations` or `/admin/operations/{run}` with only helper-supported continuity semantics.
### Tests for User Story 1
- [X] T007 [P] [US1] Add tenant-aware admin collection link assertions for summary and dashboard sources in `apps/platform/tests/Feature/Filament/RecentOperationsSummaryWidgetTest.php` and `apps/platform/tests/Feature/Monitoring/OperationsDashboardDrillthroughTest.php`
- [X] T008 [P] [US1] Add canonical admin detail link assertions for coverage and review-pack sources in `apps/platform/tests/Feature/Filament/InventoryCoverageRunContinuityTest.php` and `apps/platform/tests/Feature/ReviewPack/ReviewPackResourceTest.php`
- [X] T009 [P] [US1] Add canonical related-link, viewer fallback, and explicit admin `404`/`403` authorization assertions, including the in-scope capability-denial proof, in `apps/platform/tests/Feature/144/CanonicalOperationViewerDeepLinkTrustTest.php`, `apps/platform/tests/Feature/078/RelatedLinksOnDetailTest.php`, and `apps/platform/tests/Feature/RunAuthorizationTenantIsolationTest.php`
### Implementation for User Story 1
- [X] T010 [P] [US1] Migrate admin collection links in `apps/platform/app/Filament/Widgets/Tenant/RecentOperationsSummary.php` and the collection/detail continuity paths in `apps/platform/app/Filament/Pages/InventoryCoverage.php` to `OperationRunLinks`
- [X] T011 [P] [US1] Migrate admin detail links in `apps/platform/app/Filament/Resources/InventoryItemResource.php` and `apps/platform/app/Filament/Resources/ReviewPackResource.php` to `OperationRunLinks::view(...)`
- [X] T012 [US1] Migrate shared related-navigation and canonical viewer fallback paths in `apps/platform/app/Support/Navigation/RelatedNavigationResolver.php` and `apps/platform/app/Filament/Pages/Operations/TenantlessOperationRunViewer.php` to helper-owned admin links
- [X] T013 [US1] Run the US1 admin continuity verification flow documented in `specs/232-operation-run-link-contract/quickstart.md`
**Checkpoint**: User Story 1 is independently functional and platform-owned admin drill-throughs consistently use the canonical admin helper family.
---
## Phase 4: User Story 2 - Keep System-Plane Run Links In The System Plane (Priority: P1)
**Goal**: Platform operators keep landing on canonical `/system/ops/runs` surfaces, and system follow-up links do not regress to admin-plane monitoring.
**Independent Test**: Open system directory or operations follow-up links as a platform user and confirm collection/detail URLs remain helper-owned system-plane destinations while wrong-plane access still resolves as deny-as-not-found.
### Tests for User Story 2
- [X] T014 [P] [US2] Add system-plane continuity and platform-authorization assertions in `apps/platform/tests/Feature/System/Spec195/SystemDirectoryResidualSurfaceTest.php` and `apps/platform/tests/Feature/System/Spec113/AuthorizationSemanticsTest.php`
### Implementation for User Story 2
- [X] T015 [P] [US2] Audit system directory follow-up links in `apps/platform/app/Filament/System/Pages/Directory/ViewTenant.php` and `apps/platform/app/Filament/System/Pages/Directory/ViewWorkspace.php` and keep collection/detail navigation on `SystemOperationRunLinks` without admin fallbacks
- [X] T016 [US2] Audit canonical system entry points in `apps/platform/app/Filament/System/Pages/Ops/ViewRun.php`, `apps/platform/app/Filament/System/Pages/Ops/Runs.php`, and `apps/platform/app/Support/System/SystemOperationRunLinks.php` and apply only minimal cleanup needed to keep verified helper-backed system navigation admin-plane free
- [X] T017 [US2] Run the US2 system-plane verification flow documented in `specs/232-operation-run-link-contract/quickstart.md`
**Checkpoint**: User Story 2 is independently functional and system-plane run navigation remains helper-owned and plane-correct.
---
## Phase 5: User Story 3 - Prevent New Raw Operation-Link Bypasses (Priority: P2)
**Goal**: A bounded repository guard blocks new raw operation-route assembly in platform-owned UI and shared navigation code while preserving explicitly justified infrastructure exceptions.
**Independent Test**: Introduce a representative raw `route('admin.operations.view', ...)` or direct system operations URL inside the declared app-side boundary, confirm the guard fails with actionable output, then replace it with the canonical helper or an explicitly justified exception and confirm the guard passes.
### Tests for User Story 3
- [X] T018 [P] [US3] Add bounded app-side scan coverage, exception handling, and actionable failure assertions in `apps/platform/tests/Feature/Guards/OperationRunLinkContractGuardTest.php`
- [X] T019 [P] [US3] Add guard-adjacent regression coverage for accepted delegates and canonical helper outputs in `apps/platform/tests/Feature/OpsUx/CanonicalViewRunLinksTest.php` and `apps/platform/tests/Feature/Monitoring/OperationsDashboardDrillthroughTest.php`
### Implementation for User Story 3
- [X] T020 [US3] Finalize the guard include paths, forbidden patterns, and allowlisted exception entries for `apps/platform/app/Providers/Filament/AdminPanelProvider.php`, `apps/platform/app/Providers/Filament/TenantPanelProvider.php`, `apps/platform/app/Support/Middleware/EnsureFilamentTenantSelected.php`, and `apps/platform/app/Http/Controllers/ClearTenantContextController.php` in `apps/platform/tests/Feature/Guards/OperationRunLinkContractGuardTest.php`
- [X] T021 [US3] Retain the finalized allowlisted exceptions in `apps/platform/app/Providers/Filament/AdminPanelProvider.php`, `apps/platform/app/Providers/Filament/TenantPanelProvider.php`, `apps/platform/app/Support/Middleware/EnsureFilamentTenantSelected.php`, and `apps/platform/app/Http/Controllers/ClearTenantContextController.php`
- [X] T022 [US3] Run the US3 guardrail verification flow documented in `specs/232-operation-run-link-contract/quickstart.md`
**Checkpoint**: User Story 3 is independently functional and future platform-owned raw bypasses are blocked by a bounded, reviewable guard.
---
## Phase 6: Polish & Cross-Cutting Concerns
**Purpose**: Finalize the contract artifacts, formatting, and focused validation workflow for the full feature.
- [X] T023 [P] Refresh `specs/232-operation-run-link-contract/contracts/operation-run-link-contract.logical.openapi.yaml` and `specs/232-operation-run-link-contract/quickstart.md` with the final guard boundary, exception inventory, and focused validation steps
- [X] T024 Run formatting on touched app and test files with `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent`
- [X] T025 Run the focused Pest suite from `specs/232-operation-run-link-contract/quickstart.md` against `apps/platform/tests/Feature/OpsUx/CanonicalViewRunLinksTest.php`, `apps/platform/tests/Feature/Monitoring/OperationsDashboardDrillthroughTest.php`, `apps/platform/tests/Feature/Filament/RecentOperationsSummaryWidgetTest.php`, `apps/platform/tests/Feature/Filament/InventoryCoverageRunContinuityTest.php`, `apps/platform/tests/Feature/ReviewPack/ReviewPackResourceTest.php`, `apps/platform/tests/Feature/144/CanonicalOperationViewerDeepLinkTrustTest.php`, `apps/platform/tests/Feature/078/RelatedLinksOnDetailTest.php`, `apps/platform/tests/Feature/RunAuthorizationTenantIsolationTest.php`, `apps/platform/tests/Feature/System/Spec195/SystemDirectoryResidualSurfaceTest.php`, `apps/platform/tests/Feature/System/Spec113/AuthorizationSemanticsTest.php`, and `apps/platform/tests/Feature/Guards/OperationRunLinkContractGuardTest.php`
- [X] T026 Record the finalized producer inventory, allowlisted exception set, guard boundary, and `document-in-feature` test-governance disposition in `specs/232-operation-run-link-contract/plan.md`
---
## Dependencies & Execution Order
### Phase Dependencies
- **Setup (Phase 1)**: No dependencies; can start immediately.
- **Foundational (Phase 2)**: Depends on Setup completion and blocks all user story work.
- **User Story 1 (Phase 3)**: Depends on Foundational completion and is the recommended first implementation increment.
- **User Story 2 (Phase 4)**: Depends on Foundational completion and can proceed once helper semantics are stable.
- **User Story 3 (Phase 5)**: Depends on User Stories 1 and 2 because the final guard boundary must reflect the settled migrated surfaces and explicit exception set.
- **Polish (Phase 6)**: Depends on all desired user stories being complete.
### User Story Dependencies
- **US1 (P1)**: Starts immediately after Foundational and delivers the primary admin-plane cleanup.
- **US2 (P1)**: Can begin after Foundational, but is easiest to validate once US1 has settled the shared helper vocabulary.
- **US3 (P2)**: Starts after US1 and US2 stabilize because the allowlist and forbidden-pattern boundary should be closed against the final adopted surface set.
### Within Each User Story
- Story tests should be written and fail before the corresponding implementation tasks are considered complete.
- Helper-semantics work in Phase 2 should land before any surface migration adopts the final contract.
- Shared files such as `apps/platform/app/Support/OperationRunLinks.php`, `apps/platform/app/Support/System/SystemOperationRunLinks.php`, and `apps/platform/tests/Feature/Guards/OperationRunLinkContractGuardTest.php` should be edited sequentially even when surrounding tasks are otherwise parallelizable.
- Each storys verification task should complete before moving to the next priority slice when working sequentially.
### Parallel Opportunities
- **Setup**: `T001`, `T002`, `T003`, and `T004` can run in parallel.
- **Foundational**: `T006` can run in parallel with the tail end of `T005` once helper inputs and delegate boundaries are settled.
- **US1 tests**: `T007`, `T008`, and `T009` can run in parallel.
- **US1 implementation**: `T010` and `T011` can run in parallel; `T012` should follow once the surrounding helper semantics are stable.
- **US2**: `T014` can run in parallel with early system normalization in `T015`; `T016` should follow once any needed directory-page normalization is clear.
- **US3 tests**: `T018` and `T019` can run in parallel.
- **Polish**: `T023` can run in parallel with `T024` once implementation is stable.
---
## Parallel Example: User Story 1
```bash
# Run US1 coverage in parallel:
T007 apps/platform/tests/Feature/Filament/RecentOperationsSummaryWidgetTest.php and apps/platform/tests/Feature/Monitoring/OperationsDashboardDrillthroughTest.php
T008 apps/platform/tests/Feature/Filament/InventoryCoverageRunContinuityTest.php and apps/platform/tests/Feature/ReviewPack/ReviewPackResourceTest.php
T009 apps/platform/tests/Feature/144/CanonicalOperationViewerDeepLinkTrustTest.php and apps/platform/tests/Feature/078/RelatedLinksOnDetailTest.php
# Then split the non-overlapping admin migrations:
T010 apps/platform/app/Filament/Widgets/Tenant/RecentOperationsSummary.php and apps/platform/app/Filament/Pages/InventoryCoverage.php
T011 apps/platform/app/Filament/Resources/InventoryItemResource.php and apps/platform/app/Filament/Resources/ReviewPackResource.php
```
---
## Parallel Example: User Story 2
```bash
# Run US2 system assertions while normalizing residual system directory links:
T014 apps/platform/tests/Feature/System/Spec195/SystemDirectoryResidualSurfaceTest.php and apps/platform/tests/Feature/System/Spec113/AuthorizationSemanticsTest.php
T015 apps/platform/app/Filament/System/Pages/Directory/ViewTenant.php and apps/platform/app/Filament/System/Pages/Directory/ViewWorkspace.php
```
---
## Parallel Example: User Story 3
```bash
# Run guard coverage in parallel with adjacent helper-output regressions:
T018 apps/platform/tests/Feature/Guards/OperationRunLinkContractGuardTest.php
T019 apps/platform/tests/Feature/OpsUx/CanonicalViewRunLinksTest.php and apps/platform/tests/Feature/Monitoring/OperationsDashboardDrillthroughTest.php
```
---
## Implementation Strategy
### First Implementation Increment (User Story 1 Only)
1. Complete Phase 1: Setup.
2. Complete Phase 2: Foundational.
3. Complete Phase 3: User Story 1.
4. Validate the feature with `T013` before widening the slice.
### Incremental Delivery
1. Stabilize the helper contract and guard boundary in Setup and Foundational work.
2. Ship US1 to migrate the actual admin-plane drift surface set.
3. Add US2 to lock the system plane and preserve platform-only destination truth.
4. Add US3 to prevent future raw bypasses and make exceptions explicit.
5. Finish with contract refresh, formatting, focused tests, and close-out notes.
### Parallel Team Strategy
With multiple developers:
1. One contributor can extend helper and guard tests while another prepares the admin widget/resource drill-through assertions.
2. After Phase 2, one contributor can migrate admin collection sources, another can migrate admin detail sources, and a third can normalize shared resolver or viewer fallbacks.
3. Keep `OperationRunLinks.php`, `SystemOperationRunLinks.php`, and `OperationRunLinkContractGuardTest.php` serialized because they define the shared contract boundary.
---
## Notes
- `[P]` marks tasks that can run in parallel once their prerequisites are satisfied and the touched files do not overlap.
- `[US1]`, `[US2]`, and `[US3]` map directly to the feature specification user stories.
- The first working increment is Phase 1 through Phase 3, but the approved feature-complete minimum remains Phase 1 through Phase 5 because system-plane continuity and the guardrail are part of the accepted scope.
- All tasks above follow the required checklist format with task ID, optional parallel marker, story label where applicable, and exact file paths.

View File

@ -0,0 +1,36 @@
# Specification Quality Checklist: Operation Run Active-State Visibility & Stale Escalation
**Purpose**: Validate specification completeness and quality before proceeding to planning
**Created**: 2026-04-23
**Feature**: [spec.md](../spec.md)
## Content Quality
- [x] No implementation details (languages, frameworks, APIs)
- [x] Focused on user value and business needs
- [x] Written for non-technical stakeholders
- [x] All mandatory sections completed
## Requirement Completeness
- [x] No [NEEDS CLARIFICATION] markers remain
- [x] Requirements are testable and unambiguous
- [x] Success criteria are measurable
- [x] Success criteria are technology-agnostic (no implementation details)
- [x] All acceptance scenarios are defined
- [x] Edge cases are identified
- [x] Scope is clearly bounded
- [x] Dependencies and assumptions identified
## Feature Readiness
- [x] All functional requirements have clear acceptance criteria
- [x] User scenarios cover primary flows
- [x] Feature meets measurable outcomes defined in Success Criteria
- [x] No implementation details leak into specification
## Notes
- Validated after initial draft creation.
- No clarification markers remain.
- Candidate promoted and removed from the open `Qualified` list in `docs/product/spec-candidates.md`.

View File

@ -0,0 +1,317 @@
openapi: 3.1.0
info:
title: Operation Run Active-State Visibility Logical Contract
version: 1.0.0
description: |
Internal logical contract for Spec 233. This does not introduce a new public API.
It documents the operator-visible active-state payload semantics that existing
Filament and Livewire surfaces derive from current OperationRun lifecycle truth.
servers:
- url: https://logical.tenantpilot.internal
description: Logical contract namespace only
tags:
- name: TenantActivity
- name: WorkspaceMonitoring
- name: CanonicalRunDetail
paths:
/admin/t/{tenant}/dashboard:
get:
tags: [TenantActivity]
summary: Logical tenant activity summary contract
description: |
Represents the active-state payload that tenant dashboard activity and attention
surfaces must expose. Actual implementation is HTML/Filament, not JSON.
operationId: getTenantDashboardOperationActivitySummary
x-logical-contract: true
parameters:
- $ref: '#/components/parameters/TenantId'
responses:
'200':
description: Active-state summary for tenant-visible runs
content:
application/json:
schema:
$ref: '#/components/schemas/TenantActivityOperationsSummary'
'404':
description: Tenant not visible to the current operator
/admin/t/{tenant}/operations/progress:
get:
tags: [TenantActivity]
summary: Logical tenant active-progress overlay contract
description: |
Represents the visible-active payload used by the tenant-local progress overlay.
Stale-active runs remain active for visibility and polling purposes, and
their compact elevation is rendered through the shared badge and Ops UX presenter path.
operationId: getTenantOperationProgressOverlay
x-logical-contract: true
parameters:
- $ref: '#/components/parameters/TenantId'
responses:
'200':
description: Active progress payload for tenant-scoped runs
content:
application/json:
schema:
$ref: '#/components/schemas/TenantOperationProgressOverlay'
'403':
description: Operator is a member but lacks capability to view operation runs
'404':
description: Tenant not visible to the current operator
/admin/operations:
get:
tags: [WorkspaceMonitoring]
summary: Logical canonical operations list contract
description: |
Represents the row-level semantics required by the canonical operations list.
Existing tenant prefilter continuity may be preserved, but active-state meaning
must match the unfiltered list.
operationId: listWorkspaceOperationsWithActiveStateMeaning
x-logical-contract: true
parameters:
- $ref: '#/components/parameters/TenantFilter'
- $ref: '#/components/parameters/ActiveTab'
- $ref: '#/components/parameters/ProblemClass'
responses:
'200':
description: Workspace operations list row contract
content:
application/json:
schema:
$ref: '#/components/schemas/OperationRunCollectionContract'
'404':
description: Workspace scope not visible to the current operator
/admin/operations/{run}:
get:
tags: [CanonicalRunDetail]
summary: Logical canonical run-detail contract
description: |
Represents the top-level summary semantics for a canonical operation-run detail page.
The page remains diagnostic-first while preserving the same active-state meaning seen
on compact tenant and workspace surfaces.
operationId: getCanonicalOperationRunDetailActiveStateSummary
x-logical-contract: true
parameters:
- $ref: '#/components/parameters/RunId'
responses:
'200':
description: Canonical operation-run detail summary contract
content:
application/json:
schema:
$ref: '#/components/schemas/OperationRunDetailContract'
'403':
description: Operator is a member of scope but lacks required capability
'404':
description: Run not visible to the current operator or tenant scope
components:
parameters:
TenantId:
name: tenant
in: path
required: true
schema:
type: integer
minimum: 1
RunId:
name: run
in: path
required: true
schema:
type: integer
minimum: 1
TenantFilter:
name: tenant
in: query
required: false
schema:
type: integer
minimum: 1
description: Optional tenant prefilter preserved from tenant-context navigation
ActiveTab:
name: activeTab
in: query
required: false
schema:
type: string
description: Existing canonical monitoring tab selection
ProblemClass:
name: problemClass
in: query
required: false
schema:
type: string
enum:
- none
- active_stale_attention
- terminal_follow_up
schemas:
OperationRunActiveStateProjection:
type: object
additionalProperties: false
required:
- freshness_state
- problem_class
- surface_category
- is_currently_active
- is_reconciled
- show_in_active_progress
- keep_active_polling
properties:
freshness_state:
type: string
enum:
- fresh_active
- likely_stale
- reconciled_failed
- terminal_normal
- unknown
problem_class:
type: string
enum:
- none
- active_stale_attention
- terminal_follow_up
surface_category:
type: string
enum:
- healthy_active
- past_expected_lifecycle
- likely_stale
- no_longer_active
- unknown
description: |
Derived operator-facing category. Compact surfaces may use
`past_expected_lifecycle` where canonical detail uses `likely_stale`
over the same stale truth.
compact_label:
type: string
nullable: true
diagnostic_label:
type: string
nullable: true
lifecycle_label:
type: string
nullable: true
guidance:
type: string
nullable: true
stale_lineage_note:
type: string
nullable: true
is_currently_active:
type: boolean
is_reconciled:
type: boolean
show_in_active_progress:
type: boolean
keep_active_polling:
type: boolean
OperationRunSurfaceItem:
type: object
additionalProperties: false
required:
- run_id
- operation_label
- active_state
properties:
run_id:
type: integer
minimum: 1
tenant_id:
type: integer
minimum: 1
nullable: true
tenant_label:
type: string
nullable: true
operation_label:
type: string
status_label:
type: string
nullable: true
outcome_label:
type: string
nullable: true
active_state:
$ref: '#/components/schemas/OperationRunActiveStateProjection'
detail_url:
type: string
nullable: true
TenantActivityOperationsSummary:
type: object
additionalProperties: false
required:
- tenant_id
- items
properties:
tenant_id:
type: integer
minimum: 1
items:
type: array
items:
$ref: '#/components/schemas/OperationRunSurfaceItem'
TenantOperationProgressOverlay:
type: object
additionalProperties: false
required:
- tenant_id
- has_visible_active_runs
- poll_interval
- items
properties:
tenant_id:
type: integer
minimum: 1
has_visible_active_runs:
type: boolean
poll_interval:
type: string
nullable: true
enum:
- 10s
items:
type: array
items:
$ref: '#/components/schemas/OperationRunSurfaceItem'
OperationRunCollectionContract:
type: object
additionalProperties: false
required:
- items
properties:
tenant_filter:
type: integer
minimum: 1
nullable: true
active_tab:
type: string
nullable: true
problem_class:
type: string
nullable: true
items:
type: array
items:
$ref: '#/components/schemas/OperationRunSurfaceItem'
OperationRunDetailContract:
type: object
additionalProperties: false
required:
- run_id
- operation_label
- active_state
properties:
run_id:
type: integer
minimum: 1
operation_label:
type: string
active_state:
$ref: '#/components/schemas/OperationRunActiveStateProjection'
top_summary:
type: string
nullable: true
diagnostics_visible_after_summary:
type: boolean
const: true

View File

@ -0,0 +1,147 @@
# Data Model: Operation Run Active-State Visibility & Stale Escalation
## Overview
This feature introduces no new persisted entity, table, or stored projection. It formalizes one derived active-state presentation contract over existing `OperationRun` lifecycle truth so tenant and workspace monitoring surfaces present the same meaning.
## Source Entity: OperationRun
- **Purpose**: Canonical lifecycle and outcome record for long-running admin-plane work.
- **Existing fields used by this feature**:
- `id`
- `workspace_id`
- `tenant_id`
- `type`
- `status`
- `outcome`
- `created_at`
- `started_at`
- `completed_at`
- `context`
- `failure_summary`
- **Existing relationships used by this feature**:
- `tenant`
- `user` where available for initiator context
- **Existing invariants**:
- Lifecycle status and outcome remain service-owned.
- Reconciliation metadata stays inside `context.reconciliation`.
- No new persisted status or outcome values are introduced for visibility purposes.
## Derived Truth: OperationRunFreshnessState
- **Type**: Existing enum `App\Support\Operations\OperationRunFreshnessState`
- **Values**:
- `fresh_active`
- `likely_stale`
- `reconciled_failed`
- `terminal_normal`
- `unknown`
- **Inputs**:
- `status`
- `created_at`
- `started_at`
- `context.reconciliation`
- existing `OperationLifecyclePolicy`
- **Behavioral rule**:
- This remains the only stale/late truth input for surface rendering.
- No widget, page, or Livewire component may introduce its own threshold logic.
## Derived Truth: OperationRun Problem Class
- **Type**: Existing derived string on `OperationRun`
- **Values**:
- `none`
- `active_stale_attention`
- `terminal_follow_up`
- **Purpose**:
- Separates active stale attention from terminal follow-up while keeping both distinct from calm/no-action runs.
- **Relationship to freshness**:
- `likely_stale` freshness yields `active_stale_attention`.
- `reconciled_failed` freshness yields `terminal_follow_up`.
- Completed blocked/partial/failed runs may also yield `terminal_follow_up` without stale lineage.
## Derived View Model: Active-State Presentation Contract
- **Type**: Derived, request-scoped presentation payload. Prefer reuse of `OperationUxPresenter::decisionZoneTruth()` and existing badge/presenter outputs before adding any new helper.
- **Required fields across covered surfaces**:
- `freshness_state`
- `problem_class`
- `is_currently_active`
- `is_reconciled`
- `compact_label`
- `diagnostic_label`
- `guidance`
- `stale_lineage_note`
- `show_in_active_progress`
- `keep_active_polling`
- **Presentation categories**:
- `healthy_active`
- `past_expected_lifecycle`
- `likely_stale`
- `no_longer_active`
- `unknown` fallback
- **Category mapping rules**:
- `fresh_active` + active run -> `healthy_active`
- `likely_stale` on compact summary surfaces -> `past_expected_lifecycle`
- `likely_stale` on canonical or stronger diagnostic surfaces -> `likely_stale`
- `terminal_normal` or `reconciled_failed` -> `no_longer_active`
- `unknown` -> fallback copy without false stale escalation
- **Important constraint**:
- `past_expected_lifecycle` and `likely_stale` are density variants over the same stale truth, not separate persisted states.
## Derived Surface Policy: Tenant Active Progress Visibility
- **Current consumers**:
- `App\Livewire\BulkOperationProgress`
- `App\Support\OpsUx\ActiveRuns`
- **Former issue**:
- Both used `healthyActive()` and therefore suppressed stale-active runs from the tenant progress overlay and polling decision.
- **Implemented rule**:
- Fresh and stale active runs remain visible as active work.
- Terminal runs disappear on the next refresh cycle.
- Polling continues while any visible active work remains, including stale-active runs.
- Overlay rendering uses the existing status badge and `OperationUxPresenter` guidance path so stale-active elevation stays derived from shared freshness truth.
## Covered Surface Consumers
| Consumer | Current Truth Inputs | Required Change |
|---|---|---|
| `BulkOperationProgress` | Active run query, `healthyActive()`, `ActiveRuns` | Include stale-active work in visibility and polling semantics while keeping terminal runs excluded |
| `RecentOperationsSummary` | Raw recent runs for tenant | Ensure active-state emphasis and copy stay aligned with canonical freshness meaning |
| `Dashboard\RecentOperations` | Badge rendering + `OperationUxPresenter` | Preserve and tighten existing freshness-aware row semantics |
| `Dashboard\NeedsAttention` / `DashboardKpis` | Problem-class counts + links | Keep stale-active counts and linked monitoring semantics aligned |
| `WorkspaceOverviewBuilder` / `WorkspaceRecentOperations` | Badge rendering + `OperationUxPresenter` | Preserve workspace summary consistency and diagnostic separation |
| `OperationRunResource` | Status/outcome badges + lifecycle summaries | Keep canonical list/detail authoritative and consistent with compact surfaces |
| `TenantlessOperationRunViewer` | Canonical detail page around resource truth | Preserve diagnostic-first explanation of stale versus terminal meaning |
## State Transitions Relevant To This Feature
1. `queued` or `running` within lifecycle threshold
- Freshness: `fresh_active`
- Presentation: `healthy_active`
- Visible on active-only compact surfaces: yes
2. `queued` or `running` beyond lifecycle threshold
- Freshness: `likely_stale`
- Presentation: `past_expected_lifecycle` on compact surfaces, `likely_stale` on diagnostic surfaces
- Visible on active-only compact surfaces: yes
3. `completed` without reconciliation
- Freshness: `terminal_normal`
- Presentation: `no_longer_active`
- Visible on active-only compact surfaces: no
4. `completed` with reconciliation metadata
- Freshness: `reconciled_failed`
- Presentation: `no_longer_active` with stale-lineage diagnostics
- Visible on active-only compact surfaces: no
## Validation Rules And Invariants
- No new `OperationRun.status` or `OperationRun.outcome` values may be added.
- No new persisted `operation_runs` summary or mirror table may be added.
- All stale/late meaning must derive from existing freshness truth.
- Tenant-scoped surfaces must only reflect runs already visible to the current tenant-entitled operator.
- Workspace summaries must stay limited to entitled tenant slices.
- Healthy queued/running work must not inherit stale emphasis.
- Terminal runs must stop appearing in active-only surfaces on the next refresh cycle.

View File

@ -0,0 +1,237 @@
# Implementation Plan: Operation Run Active-State Visibility & Stale Escalation
**Branch**: `233-stale-run-visibility` | **Date**: 2026-04-23 | **Spec**: [spec.md](./spec.md)
**Input**: Feature specification from `/specs/233-stale-run-visibility/spec.md`
## Summary
Complete one shared active-state presentation contract on top of the already-existing `OperationRun` lifecycle freshness truth by converging tenant dashboard activity signals, tenant-local active-run progress cards, workspace recent-operations summaries, the canonical operations list, and canonical run detail on the same fresh-versus-past-expected-versus-likely-stale semantics without introducing new persisted run state, new status values, or page-local heuristics.
## Technical Context
**Language/Version**: PHP 8.4.15, Laravel 12, Filament v5, Livewire v4
**Primary Dependencies**: Filament widgets/resources/pages, Pest v4, `App\Models\OperationRun`, `App\Support\Operations\OperationRunFreshnessState`, `App\Services\Operations\OperationLifecycleReconciler`, `App\Support\OpsUx\OperationUxPresenter`, `App\Support\OpsUx\ActiveRuns`, `App\Support\Badges\BadgeCatalog` / `BadgeRenderer`, `App\Support\Workspaces\WorkspaceOverviewBuilder`, `App\Support\OperationRunLinks`
**Storage**: Existing PostgreSQL `operation_runs` records and current session/query-backed monitoring navigation state; no new persistence
**Testing**: Focused Pest feature tests over tenant dashboard widgets, tenant active-run progress surfaces, workspace overview operations summaries, canonical operations list/detail pages, and stale-reconciliation semantics
**Validation Lanes**: `fast-feedback`, `confidence`
**Target Platform**: Laravel admin web application in Sail containers with workspace routes under `/admin` and tenant routes under `/admin/t/{tenant}`
**Project Type**: Monorepo with one Laravel runtime in `apps/platform` and spec artifacts at repository root
**Performance Goals**: Preserve existing query shape and request-local presenter work; no additional remote calls, no background-process changes, and no new persisted summary projections
**Constraints**: No new `OperationRun.status` or `OperationRun.outcome` values, no retry/cancel/reconcile-now UX, no new notification channel, no page-local stale heuristics, no cross-tenant leakage, and no second presentation framework beyond the existing badge/presenter path
**Scale/Scope**: Existing tenant dashboard and tenant resource widgets, one Livewire active-progress slice, workspace overview builders/widgets, canonical operations list/detail pages, and their focused feature-test families
## Filament v5 Implementation Contract
- **Livewire v4.0+ compliance**: Preserved. The feature extends existing Filament v5 pages/widgets/resources and Livewire components without introducing legacy Livewire v3 patterns.
- **Provider registration location**: Unchanged. Panel providers remain registered in `bootstrap/providers.php`, not `bootstrap/app.php`.
- **Global search coverage**: `OperationRunResource` already keeps global search disabled via `$isGloballySearchable = false`, so this feature adds no new global-search exposure and does not depend on Edit/View global-search rules.
- **Destructive actions**: No destructive actions are introduced. Existing monitoring/detail actions remain read-only, and this feature must not add retry, cancel, or force-fail controls.
- **Asset strategy**: No new Filament assets are planned. Deployment expectations remain unchanged, including `cd apps/platform && php artisan filament:assets` only if a future implementation adds registered assets.
- **Testing plan**: Prove semantics with focused feature tests for tenant dashboard activity, tenant progress summaries, workspace recent operations, canonical operations list/detail consistency, and stale-versus-fresh boundary cases. No browser or heavy-governance lane is required for this slice.
## UI / Surface Guardrail Plan
- **Guardrail scope**: Changed surfaces across tenant dashboard activity, tenant-local active-run progress, workspace recent operations summaries, canonical monitoring list rows, and canonical run detail summary
- **Native vs custom classification summary**: Mixed shared-family change using native Filament widgets/resources/pages plus one existing Livewire progress component
- **Shared-family relevance**: Status messaging, dashboard signals/cards, monitoring list presentation, canonical drill-through, and run-detail summary semantics
- **State layers in scope**: `page`, `detail`, and one request-scoped/Livewire compact-progress slice; no new URL-state layer beyond existing monitoring continuity
- **Handling modes by drift class or surface**: Review-mandatory because meaning must stay aligned across multiple existing surfaces and one existing hidden gap (`healthyActive()`-only progress) must be closed without widening scope
- **Repository-signal treatment**: Review-mandatory; the feature changes operator-visible semantics but does not need a hard-stop repo guard
- **Special surface test profiles**: `standard-native-filament`, `monitoring-state-page`, `shared-detail-family`
- **Required tests or manual smoke**: `functional-core`, `state-contract`
- **Exception path and spread control**: None planned. All covered surfaces should consume existing freshness truth and shared presenter/badge paths rather than diverging locally.
- **Active feature PR close-out entry**: `Guardrail`
## Shared Pattern & System Fit
- **Cross-cutting feature marker**: yes
- **Systems touched**: `OperationRunFreshnessState`, `OperationLifecycleReconciler`, `OperationRun` problem-class helpers, `OperationUxPresenter`, centralized badge rendering, `BulkOperationProgress`, `RecentOperationsSummary`, `RecentOperations`, `DashboardKpis`, `NeedsAttention`, `WorkspaceOverviewBuilder`, `WorkspaceRecentOperations`, `OperationRunResource`, and `TenantlessOperationRunViewer`
- **Shared abstractions reused**: `OperationRun::freshnessState()`, `OperationRun::problemClass()`, `OperationRunFreshnessState`, `OperationUxPresenter::decisionZoneTruth()`, `OperationUxPresenter::lifecycleAttentionSummary()`, `OperationUxPresenter::surfaceGuidance()`, `ActiveRuns`, `BadgeCatalog` / `BadgeRenderer`, `OperationRunLinks`, and existing workspace/tenant authorization helpers
- **New abstraction introduced? why?**: One bounded derived active-state presentation contract is intentionally made explicit through the existing presenter and active-run helpers so compact and canonical surfaces can stay aligned without introducing a standalone semantic framework.
- **Why the existing abstraction was sufficient or insufficient**: Existing lifecycle truth is sufficient and authoritative. Existing compact-surface adoption is insufficient because some slices already honor freshness (`OperationRunResource`, `RecentOperations`, `WorkspaceOverviewBuilder`) while others still filter to `healthyActive()` or under-communicate stale active work.
- **Bounded deviation / spread control**: Same meaning, different density only. Surface-specific copy may vary by density, but all covered surfaces must consume the same freshness/problem-class truth and must not invent local stale logic.
## Constitution Check
*GATE: Passed before Phase 0 research. Re-checked after Phase 1 design: still passed with one bounded derived presentation contract and no new persisted truth.*
| Gate | Status | Plan Notes |
|------|--------|------------|
| Inventory-first / read-write separation | PASS | The feature is read-only presentation hardening over existing `operation_runs`; no restore, preview, or write-path change is introduced. |
| RBAC, workspace isolation, tenant isolation | PASS | Existing tenant-scoped widgets and canonical workspace monitoring routes remain on current entitlement checks; no new visibility surface is added. |
| Run observability / Ops-UX lifecycle | PASS | `OperationRunService`, lifecycle reconciliation, queued/running/terminal notifications, and run ownership remain unchanged; the plan only changes interpretation and visibility of existing run truth. |
| Shared pattern first | PASS | The plan explicitly reuses existing freshness, problem-class, presenter, and badge paths rather than adding a second semantic layer or local mapping family. |
| Proportionality / no premature abstraction | PASS | The narrowest credible change is one derived presentation contract over current truth plus convergence of existing surfaces. No new persistence, registry, or workflow framework is planned. |
| Badge semantics / Filament-native discipline | PASS | Status-like emphasis stays on centralized badge rendering and existing Filament widgets/resources/pages; no ad-hoc surface-local color system is introduced. |
| Decision-first / operator surfaces | PASS | The operations list remains the primary triage surface, tenant widgets stay secondary context, and canonical run detail stays diagnostic-first. |
| Test governance | PASS | Proof stays in focused feature lanes and existing surface families, with no browser-lane promotion and no heavy shared test infrastructure growth. |
## Test Governance Check
- **Test purpose / classification by changed surface**: `Feature` for tenant dashboard activity, tenant progress surfaces, workspace recent operations summaries, canonical operations list/detail consistency, and stale boundary semantics
- **Affected validation lanes**: `fast-feedback`, `confidence`
- **Why this lane mix is the narrowest sufficient proof**: The business truth is cross-surface semantic consistency over existing `OperationRun` freshness state. That is fully provable with focused feature tests against the touched widgets/pages and existing reconciliation truth; browser coverage would add cost without validating additional domain behavior.
- **Narrowest proving command(s)**:
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent`
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/OpsUx/BulkOperationProgressDbOnlyTest.php tests/Feature/OpsUx/ProgressWidgetFiltersTest.php tests/Feature/OpsUx/ProgressWidgetOverflowTest.php`
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Filament/RecentOperationsSummaryWidgetTest.php tests/Feature/Filament/DashboardKpisWidgetTest.php tests/Feature/Filament/NeedsAttentionWidgetTest.php tests/Feature/Filament/WorkspaceOverviewOperationsTest.php tests/Feature/Monitoring/OperationLifecycleFreshnessPresentationTest.php tests/Feature/Monitoring/MonitoringOperationsTest.php tests/Feature/Monitoring/OperationsDashboardDrillthroughTest.php tests/Feature/Filament/OperationRunEnterpriseDetailPageTest.php tests/Feature/Operations/TenantlessOperationRunViewerTest.php`
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/RunAuthorizationTenantIsolationTest.php tests/Feature/OpsUx/NonLeakageWorkspaceOperationsTest.php`
- **Fixture / helper / factory / seed / context cost risks**: Moderate. Tests need representative fresh queued/running runs, likely stale runs, reconciled terminal runs, tenant membership context, workspace overview payloads, and hidden-tenant/non-member isolation boundaries, but existing factories and operation-run helpers already cover most of that setup.
- **Expensive defaults or shared helper growth introduced?**: No. Existing `OperationRun` factories and workspace/tenant test helpers should stay opt-in and sufficient.
- **Heavy-family additions, promotions, or visibility changes**: none
- **Surface-class relief / special coverage rule**: Standard native-Filament relief plus the existing `monitoring-state-page` proving profile for canonical monitoring pages and summaries
- **Closing validation and reviewer handoff**: Re-run `pint`, then the focused feature command above. Reviewers should verify that stale active work is visible on every covered compact surface, healthy active work is not falsely escalated, and drill-through into canonical detail preserves the same active-state meaning.
- **Budget / baseline / trend follow-up**: none
- **Review-stop questions**: Did any surface invent its own stale threshold? Did `healthyActive()` filtering remain in a surface that should show stale-active attention? Did any test rely on status strings alone instead of freshness truth? Did any change accidentally widen visibility beyond entitled tenant/workspace scope?
- **Escalation path**: `document-in-feature`
- **Active feature PR close-out entry**: `Guardrail`
- **Why no dedicated follow-up spec is needed**: This is bounded current-release convergence of an existing truth family. A separate follow-up spec is only needed if later work tries to add intervention actions or a broader operations workbench.
## Project Structure
### Documentation (this feature)
```text
specs/233-stale-run-visibility/
├── plan.md
├── research.md
├── data-model.md
├── quickstart.md
├── contracts/
│ └── operation-run-active-state-visibility.logical.openapi.yaml
└── tasks.md
```
### Source Code (repository root)
```text
apps/platform/
├── app/
│ ├── Filament/
│ │ ├── Pages/
│ │ │ └── Operations/TenantlessOperationRunViewer.php
│ │ ├── Resources/
│ │ │ └── OperationRunResource.php
│ │ └── Widgets/
│ │ ├── Dashboard/
│ │ │ ├── DashboardKpis.php
│ │ │ ├── NeedsAttention.php
│ │ │ └── RecentOperations.php
│ │ ├── Tenant/
│ │ │ └── RecentOperationsSummary.php
│ │ └── Workspace/
│ │ └── WorkspaceRecentOperations.php
│ ├── Livewire/
│ │ └── BulkOperationProgress.php
│ ├── Models/
│ │ └── OperationRun.php
│ ├── Services/Operations/
│ │ └── OperationLifecycleReconciler.php
│ └── Support/
│ ├── Badges/Domains/OperationRunStatusBadge.php
│ ├── OperationRunLinks.php
│ ├── OpsUx/
│ │ ├── ActiveRuns.php
│ │ └── OperationUxPresenter.php
│ ├── Operations/OperationRunFreshnessState.php
│ └── Workspaces/WorkspaceOverviewBuilder.php
├── resources/views/
│ ├── filament/widgets/
│ │ ├── tenant/recent-operations-summary.blade.php
│ │ └── workspace/workspace-recent-operations.blade.php
│ └── livewire/
│ ├── bulk-operation-progress.blade.php
│ └── bulk-operation-progress-wrapper.blade.php
└── tests/
└── Feature/
├── Filament/
│ ├── DashboardKpisWidgetTest.php
│ ├── NeedsAttentionWidgetTest.php
│ ├── OperationRunEnterpriseDetailPageTest.php
│ ├── RecentOperationsSummaryWidgetTest.php
│ └── WorkspaceOverviewOperationsTest.php
├── Monitoring/
│ ├── MonitoringOperationsTest.php
│ ├── OperationLifecycleFreshnessPresentationTest.php
│ └── OperationsDashboardDrillthroughTest.php
└── Operations/
└── TenantlessOperationRunViewerTest.php
├── OpsUx/
│ ├── BulkOperationProgressDbOnlyTest.php
│ ├── NonLeakageWorkspaceOperationsTest.php
│ ├── ProgressWidgetFiltersTest.php
│ └── ProgressWidgetOverflowTest.php
└── RunAuthorizationTenantIsolationTest.php
```
**Structure Decision**: Single Laravel application inside `apps/platform`. Runtime work stays in existing monitoring widgets/resources/pages and one existing Livewire progress slice; planning artifacts stay under `specs/233-stale-run-visibility`.
## Complexity Tracking
No constitutional violation is planned. One bounded derived presentation contract is intentionally tracked because the spec introduces a small new semantic family over existing truth.
| Violation | Why Needed | Simpler Alternative Rejected Because |
|-----------|------------|-------------------------------------|
| BLOAT-001 derived category family | Compact surfaces currently disagree in practice about whether stale active work is still ordinary progress. One small derived contract keeps surface meaning aligned without changing persisted run state. | Leaving each widget to infer meaning from raw `status` or local heuristics would preserve drift and make future regressions likely. |
## Proportionality Review
- **Current operator problem**: Compact operator surfaces can still hide or understate that active work is already past its expected lifecycle, so operators get false reassurance until they drill into monitoring detail.
- **Existing structure is insufficient because**: Existing freshness truth and presenter helpers already exist, but they are not applied consistently across tenant progress and summary surfaces, and one slice (`BulkOperationProgress`) still intentionally filters stale active work out.
- **Narrowest correct implementation**: Reuse current freshness/problem-class truth, introduce at most one small derived active-state presentation contract, and retrofit only the existing tenant/workspace/canonical monitoring surfaces that already summarize active work.
- **Ownership cost created**: Small ongoing maintenance of one derived category family, shared copy alignment, and focused regression tests across covered surfaces.
- **Alternative intentionally rejected**: Adding new persisted `OperationRun` statuses or separate page-local stale heuristics. Both would widen lifecycle scope or create contradictory truth.
- **Release truth**: Current-release truth and operator-trust hardening.
## Phase 0 Research Summary
- Existing lifecycle and freshness truth already live in `OperationRunFreshnessState`, `OperationRun::problemClass()`, and `OperationLifecycleReconciler`; the feature should consume them rather than create new thresholds.
- Canonical monitoring surfaces already partially honor stale-active semantics: `OperationRunResource`, `Dashboard\RecentOperations`, and `WorkspaceOverviewBuilder` all feed badge/presenter state with `freshness_state` or lifecycle summaries.
- The clearest gap was tenant-local active-progress visibility: `BulkOperationProgress` scoped to `healthyActive()`, which hid stale active work from a high-frequency tenant surface and created exactly the cross-surface contradiction the spec describes.
- `OperationUxPresenter::surfaceGuidance()` already differentiates likely stale, reconciled failed, and ordinary queued/running work, so Phase 1 should extend adoption before inventing new presentation machinery.
- Existing focused tests already cover parts of the semantics (`OperationLifecycleFreshnessPresentationTest`, `MonitoringOperationsTest`, `RecentOperationsSummaryWidgetTest`, `WorkspaceOverviewOperationsTest`, `OperationRunEnterpriseDetailPageTest`, `TenantlessOperationRunViewerTest`), so implementation should prefer extending those families over introducing new broad suites.
## Phase 1 Design Summary
- `data-model.md` defines the derived active-state presentation model over existing `OperationRun`, freshness state, problem class, and covered surface consumers.
- `contracts/operation-run-active-state-visibility.logical.openapi.yaml` documents the internal logical contract for how covered surfaces derive and display active-state meaning from existing run truth.
- `quickstart.md` gives the narrow validation path for fresh-versus-stale fixtures, compact-surface rendering, canonical drill-through, and regression checks.
## Implementation Strategy
1. **Converge on one freshness-to-surface contract**
- Reuse `OperationUxPresenter::decisionZoneTruth()`, `lifecycleAttentionSummary()`, current badge helpers, and `ActiveRuns` as the default convergence path.
- Keep all thresholds and lifecycle windows owned by existing freshness truth.
2. **Fix the tenant-local active-progress blind spot**
- Update `BulkOperationProgress` so stale active runs are not silently excluded from tenant-local progress visibility.
- Preserve calm presentation for healthy active work while allowing stale/late work to escalate visibly.
3. **Align tenant dashboard and tenant-summary surfaces**
- Reconcile `DashboardKpis`, `NeedsAttention`, `RecentOperationsSummary`, and any shared tenant activity slices so they expose the same active-state meaning and drill-through expectations.
- Ensure mixed tenant activity does not over-generalize one stale run into “all activity is stale.”
4. **Keep workspace and canonical monitoring surfaces authoritative**
- Reuse existing freshness-aware row/detail rendering in `OperationRunResource`, `TenantlessOperationRunViewer`, and `WorkspaceOverviewBuilder`, tightening copy and top-level summary semantics only where necessary.
- Preserve canonical list/detail roles and existing filter continuity from tenant context.
5. **Regression-protect fresh versus stale boundaries**
- Extend the existing monitoring and Filament feature tests to prove fresh active, likely stale, reconciled terminal, and terminal-transition cases across covered surfaces.
- Explicitly assert that healthy queued/running runs do not inherit stale emphasis and that terminal runs disappear from active-only compact surfaces after refresh.
## Risks and Mitigations
- **Surface drift survives in one slice**: A compact surface may continue to rely on `status` only. Mitigation: inventory and update every covered surface in this plan, with tests tied to each family.
- **Over-escalation of healthy active work**: Copy or badge reuse could make all queued/running work feel unhealthy. Mitigation: keep the proving fixtures split between fresh and stale runs and assert negative cases explicitly.
- **Tenant progress regression**: Broadening `BulkOperationProgress` could accidentally turn a calm progress bar into a noisy problem board. Mitigation: keep one bounded active-state distinction and preserve existing density expectations.
- **New semantic layer grows too far**: It would be easy to invent a broader taxonomy. Mitigation: constrain the plan to one derived presentation contract backed entirely by existing freshness/problem-class truth.
## Implementation Close-Out
- **Finalized affected surfaces**: Tenant active progress overlay and polling now include all active `OperationRun` records, including stale-active runs. Tenant summary, dashboard KPI/attention, workspace overview, canonical operations list, and canonical detail surfaces already consume shared freshness, badge, and presenter paths and were validated without widening the runtime change.
- **Density-specific copy retained**: Compact surfaces use shared badge copy such as `Likely stale` plus `OperationUxPresenter::surfaceGuidance()` text about being past the lifecycle window. Canonical detail keeps the stronger `Likely stale operation` diagnostic banner.
- **Test-governance disposition**: `document-in-feature`. Coverage stayed inside existing feature-test families and the focused `fast-feedback` / `confidence` lanes; no browser lane, heavy-governance family, shared fixture widening, or new test infrastructure was introduced.
## Post-Design Re-check
Phase 0 and Phase 1 outputs keep the feature within existing `OperationRun` lifecycle truth, existing Filament/Livewire surfaces, and focused feature-test families. The plan remains constitution-compliant, Livewire v4 / Filament v5 compliant, and ready for `/speckit.tasks`.

View File

@ -0,0 +1,79 @@
# Quickstart: Operation Run Active-State Visibility & Stale Escalation
## Preconditions
1. Start the application stack:
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail up -d`
2. Work from branch `233-stale-run-visibility`.
3. Keep the scope bounded to existing admin-plane monitoring and progress surfaces.
## Primary Files To Review First
- `apps/platform/app/Models/OperationRun.php`
- `apps/platform/app/Support/Operations/OperationRunFreshnessState.php`
- `apps/platform/app/Support/OpsUx/OperationUxPresenter.php`
- `apps/platform/app/Support/OpsUx/ActiveRuns.php`
- `apps/platform/app/Livewire/BulkOperationProgress.php`
- `apps/platform/app/Filament/Widgets/Tenant/RecentOperationsSummary.php`
- `apps/platform/app/Filament/Widgets/Dashboard/RecentOperations.php`
- `apps/platform/app/Filament/Resources/OperationRunResource.php`
- `apps/platform/app/Support/Workspaces/WorkspaceOverviewBuilder.php`
- `apps/platform/app/Filament/Pages/Operations/TenantlessOperationRunViewer.php`
## Recommended Implementation Order
1. **Lock the truth source**
- Confirm no surface-local stale thresholds exist outside `OperationRunFreshnessState` and `OperationRun::problemClass()`.
- Reuse `OperationUxPresenter::decisionZoneTruth()`, `lifecycleAttentionSummary()`, and `surfaceGuidance()` wherever possible.
2. **Fix tenant progress visibility first**
- Update `ActiveRuns` and `BulkOperationProgress` so stale-active runs still count as active work for visibility and polling.
- Keep terminal runs disappearing on the next refresh cycle.
- Render stale-active elevation through the shared operation status badge and `OperationUxPresenter` guidance path.
3. **Converge tenant and workspace summary surfaces**
- Align `RecentOperationsSummary`, `Dashboard\RecentOperations`, `Dashboard\NeedsAttention`, `DashboardKpis`, and `WorkspaceOverviewBuilder` on the same compact/detailed stale-active semantics.
- Do not create a new dashboard surface family.
4. **Tighten canonical monitoring consistency last**
- Preserve `OperationRunResource` and `TenantlessOperationRunViewer` as the authoritative diagnostic surfaces.
- Adjust top-level explanation or row emphasis only where it improves consistency with the compact surfaces.
5. **Update focused tests in the same slice**
- Flip stale-hidden assertions in the tenant progress tests.
- Extend monitoring, widget, and visibility-safety tests to prove fresh versus stale boundaries, terminal transitions, and hidden-tenant/non-member isolation.
## Focused Test Matrix
| Scenario | Expected Result | Likely Test Family |
|---|---|---|
| Fresh queued/running run on tenant surface | Visible as healthy active work, no stale escalation | `tests/Feature/OpsUx/BulkOperationProgressDbOnlyTest.php`, `tests/Feature/Filament/RecentOperationsSummaryWidgetTest.php` |
| Stale queued/running run on tenant surface | Still visible as active work, but clearly elevated | `tests/Feature/OpsUx/BulkOperationProgressDbOnlyTest.php`, `tests/Feature/OpsUx/ProgressWidgetFiltersTest.php` |
| Stale run on workspace summaries | Scanable as attention-worthy, not collapsed into calm recency | `tests/Feature/Filament/WorkspaceOverviewOperationsTest.php`, `tests/Feature/Monitoring/MonitoringOperationsTest.php` |
| Stale run on canonical list/detail | Same active-state meaning preserved after drill-through | `tests/Feature/Monitoring/OperationLifecycleFreshnessPresentationTest.php`, `tests/Feature/Operations/TenantlessOperationRunViewerTest.php`, `tests/Feature/Filament/OperationRunEnterpriseDetailPageTest.php` |
| Terminal transition after refresh | Removed from active-only overlays and no longer presented as active | `tests/Feature/OpsUx/BulkOperationProgressDbOnlyTest.php` |
| Hidden or out-of-scope runs during summary rendering | Remain invisible and do not alter visible active-state summaries | `tests/Feature/OpsUx/NonLeakageWorkspaceOperationsTest.php`, `tests/Feature/RunAuthorizationTenantIsolationTest.php` |
## Minimum Validation Commands
```bash
export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/OpsUx/BulkOperationProgressDbOnlyTest.php tests/Feature/OpsUx/ProgressWidgetFiltersTest.php tests/Feature/OpsUx/ProgressWidgetOverflowTest.php
export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Filament/RecentOperationsSummaryWidgetTest.php tests/Feature/Filament/DashboardKpisWidgetTest.php tests/Feature/Filament/NeedsAttentionWidgetTest.php tests/Feature/Filament/WorkspaceOverviewOperationsTest.php tests/Feature/Monitoring/OperationLifecycleFreshnessPresentationTest.php tests/Feature/Monitoring/MonitoringOperationsTest.php tests/Feature/Monitoring/OperationsDashboardDrillthroughTest.php tests/Feature/Filament/OperationRunEnterpriseDetailPageTest.php tests/Feature/Operations/TenantlessOperationRunViewerTest.php
export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/RunAuthorizationTenantIsolationTest.php tests/Feature/OpsUx/NonLeakageWorkspaceOperationsTest.php
export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent
```
## Manual Smoke Checklist
1. Seed one fresh running run and one stale queued/running run for the same tenant.
2. Open the tenant dashboard and confirm the stale run is visible and clearly elevated without making the fresh run look unhealthy.
3. Confirm the tenant progress surface keeps polling while only stale-active work remains.
4. Open `/admin/operations` and verify the same run is distinguishable at row level.
5. Drill into `/admin/operations/{run}` and confirm the top summary preserves the same active-state meaning before raw diagnostics.
## Out Of Scope Guardrails
- Do not add retry, cancel, reconcile-now, or worker-control actions.
- Do not add new notifications or queued/running DB notifications.
- Do not add new persisted run summary data or new `OperationRun` status values.
- Do not widen the work into a new operations workbench or cross-workspace fleet view.

View File

@ -0,0 +1,49 @@
# Research: Operation Run Active-State Visibility & Stale Escalation
## Decision 1: Keep lifecycle freshness truth in the existing run model and reconciler
- **Decision**: Use `OperationRunFreshnessState`, `OperationRun::freshnessState()`, `OperationRun::problemClass()`, and `OperationLifecycleReconciler` as the only lifecycle-truth inputs for this feature.
- **Rationale**: The application already computes `fresh_active`, `likely_stale`, `reconciled_failed`, `terminal_normal`, and `unknown` from the run record plus `OperationLifecyclePolicy`. Canonical monitoring surfaces already rely on that truth, so adding a second stale heuristic would immediately recreate the drift this spec is trying to remove.
- **Alternatives considered**:
- Add new `OperationRun.status` values such as `stale` or `late`: rejected because the distinction is presentation and triage-oriented, not a new persisted lifecycle state.
- Add page-local thresholds per widget: rejected because it would create conflicting meaning across tenant, workspace, and canonical monitoring surfaces.
## Decision 2: Reuse the existing Ops UX presenter path before introducing a new helper
- **Decision**: Prefer `OperationUxPresenter::decisionZoneTruth()`, `lifecycleAttentionSummary()`, `surfaceGuidance()`, and centralized badge rendering as the presentation backbone.
- **Rationale**: The code already exposes a derived decision-zone payload and shared stale/reconciled copy. `OperationRunStatusBadge` already renders `Likely stale` when queued/running work carries `freshness_state=likely_stale`, and `OperationUxPresenter` already provides compact and diagnostic explanations off the same truth.
- **Alternatives considered**:
- New dedicated presenter family for active-state visibility: rejected unless the existing presenter path proves insufficient during implementation.
- Widget-local copy branches: rejected because they would increase semantic spread and regression risk.
## Decision 3: Treat stale-active runs as still active for tenant progress visibility
- **Decision**: Change tenant-local active-progress visibility to include freshness-elevated active runs rather than suppressing them via `healthyActive()`.
- **Rationale**: `BulkOperationProgress` and `ActiveRuns::existForTenantId()` previously used `healthyActive()`, which caused stale queued/running work to disappear from the tenant progress overlay and stopped polling when only stale runs remained. That was the clearest concrete contradiction with the canonical monitoring surfaces.
- **Alternatives considered**:
- Keep stale runs hidden in the progress overlay and rely on dashboard/list only: rejected because the spec explicitly covers tenant-local active-run cards and progress summaries.
- Add a separate stale-only overlay: rejected because it would create a second active-work surface family instead of fixing the existing one.
## Decision 4: Preserve current surface roles and drill-through flow
- **Decision**: Keep the current route and surface model: tenant dashboard and tenant progress remain secondary context, `/admin/operations` remains the primary triage list, and `/admin/operations/{run}` remains diagnostic-first.
- **Rationale**: Existing links already converge through `OperationRunLinks`, and current pages/widgets match the constitution's decision-first model. The gap is the honesty of compact active-state messaging, not missing routes.
- **Alternatives considered**:
- New operations hub or new tenant-local detail page: rejected as unnecessary workflow expansion.
- New notification channel for stale active work: rejected because the spec explicitly excludes new notification behavior.
## Decision 5: Extend existing focused tests and invert stale-hidden assumptions where necessary
- **Decision**: Update existing monitoring, Filament, and Ops UX tests rather than creating a new broad suite.
- **Rationale**: The repository already has focused coverage for lifecycle presentation and tenant progress behavior. In particular, `BulkOperationProgressDbOnlyTest` and `ProgressWidgetFiltersTest` currently codify the stale-hidden behavior that this feature must deliberately replace.
- **Alternatives considered**:
- Add a brand-new browser suite: rejected because feature tests already cover the underlying business truth and UI copy.
- Leave old progress-widget tests untouched and add parallel tests: rejected because the old assertions would preserve the wrong contract.
## Decision 6: Keep “past expected lifecycle” and “likely stale” as density-specific labels over the same stale truth
- **Decision**: Model compact “past expected lifecycle” phrasing and stronger “likely stale” diagnostic phrasing as different density outputs over the same `likely_stale` freshness truth rather than as separate persisted states.
- **Rationale**: The spec allows same meaning, different density. The current code already points in that direction: `OperationUxPresenter::surfaceGuidance()` says the run is “past its lifecycle window,” while `OperationRunStatusBadge` can label the same run `Likely stale`.
- **Alternatives considered**:
- Create two separate freshness states for “late” and “likely stale”: rejected because existing lifecycle truth has only one stale boundary and no additional behavioral consequence.
- Collapse all stale-active copy to a single label everywhere: rejected because compact surfaces and canonical detail need different density without changing meaning.

View File

@ -0,0 +1,252 @@
# Feature Specification: Operation Run Active-State Visibility & Stale Escalation
**Feature Branch**: `233-stale-run-visibility`
**Created**: 2026-04-23
**Status**: Draft
**Input**: User description: "Operation Run Active-State Visibility & Stale Escalation"
## Spec Candidate Check *(mandatory — SPEC-GATE-001)*
- **Problem**: `OperationRun` lifecycle truth already exists, but compact operator surfaces still under-communicate when active work is past expectation or likely stuck.
- **Today's failure**: A run can be visibly stale on the canonical monitoring detail yet still read like ordinary `Queued` or `Running` work on tenant cards, dashboard attention surfaces, or list rows. Operators then receive false reassurance until they drill into monitoring.
- **User-visible improvement**: Active runs that are healthy, late, or likely stuck become visibly distinct across tenant and workspace surfaces, so operators can see unhealthy work in one scan without losing the canonical run detail as the diagnostic source of truth.
- **Smallest enterprise-capable version**: Add one bounded active-state presentation contract derived from existing lifecycle, freshness, and reconciliation truth, then apply it consistently to tenant dashboard activity surfaces, tenant-local active-run cards, the workspace operations list, and the canonical run detail summary.
- **Explicit non-goals**: No new `OperationRun` status values, no retry or cancel actions, no queue or worker redesign, no new notification channel, no full operations hub redesign, no cross-workspace fleet monitoring, and no parallel UI-only stale heuristic that bypasses existing lifecycle truth.
- **Permanent complexity imported**: One bounded derived active-state category family over existing run truth, one shared cross-surface presentation contract, focused operator copy updates on existing monitoring surfaces, and regression coverage for fresh versus stale semantics across tenant and workspace entry points.
- **Why now**: This is an active near-term operator-trust hardening item in the roadmap, and it becomes more urgent as more governance, evidence, and review workflows depend on `OperationRun`. Spec 232 now hardens link continuity into canonical monitoring, so the next high-leverage gap is what those linked surfaces actually communicate about unhealthy active work.
- **Why not local**: Fixing one widget or one row badge would still leave contradictory lifecycle meaning across cards, list rows, dashboard attention, and run detail. The problem is cross-surface truth drift, not one local rendering bug.
- **Approval class**: Core Enterprise
- **Red flags triggered**: Cross-cutting interaction-class scope plus one new derived presentation category family. Defense: the feature derives entirely from existing lifecycle and freshness truth, avoids persistence or backend-state expansion, and stays bounded to existing admin-plane surfaces.
- **Score**: Nutzen: 2 | Dringlichkeit: 2 | Scope: 2 | Komplexität: 1 | Produktnähe: 2 | Wiederverwendung: 2 | **Gesamt: 11/12**
- **Decision**: approve
## Spec Scope Fields *(mandatory)*
- **Scope**: workspace, tenant, canonical-view
- **Primary Routes**:
- `/admin/t/{tenant}` for tenant dashboard activity and attention surfaces
- Existing tenant-local active-run cards linked from tenant-scoped admin surfaces that already summarize active work
- `/admin/operations` as the canonical workspace monitoring list
- `/admin/operations/{run}` as the canonical run detail surface
- **Data Ownership**: `operation_runs` remain the only source of lifecycle and freshness truth. Tenant-local cards, dashboard signals, and workspace monitoring rows remain derived read models over existing run records, lifecycle policy, and stale-detection truth. No new persisted visibility flag, summary mirror, or auxiliary active-run table is introduced.
- **RBAC**: Admin-plane workspace membership remains required for `/admin/operations` and run detail visibility. Tenant-scoped surfaces remain constrained by current tenant entitlement. Non-members and out-of-scope tenant requests remain deny-as-not-found. This feature does not introduce new capability strings, new roles, or new mutation permissions.
For canonical-view specs, the spec MUST define:
- **Default filter behavior when tenant-context is active**: When operators enter `/admin/operations` from a tenant-scoped surface, the canonical monitoring list may preserve the active tenant as a default prefilter while retaining the same workspace-level route and clearing behavior used by existing canonical monitoring links.
- **Explicit entitlement checks preventing cross-tenant leakage**: Tenant-local cards and dashboard signals may summarize only runs already visible to the current operator within that tenant. The canonical operations list and run detail continue to re-check workspace membership and tenant entitlement before rendering. No stale or past-lifecycle signal may reveal the existence of hidden runs or tenants.
## Cross-Cutting / Shared Pattern Reuse *(mandatory when the feature touches notifications, status messaging, action links, header actions, dashboard signals/cards, alerts, navigation entry points, 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)**: status messaging, dashboard signals/cards, monitoring list presentation, canonical run detail summary
- **Systems touched**: tenant dashboard activity surfaces, tenant-local active-run cards, workspace monitoring list rows, canonical monitoring detail, and existing canonical drill-through links into `/admin/operations`
- **Existing pattern(s) to extend**: current `OperationRun` lifecycle policies, freshness/stale detection, canonical monitoring pages, shared badge semantics, and existing tenant-to-monitoring drill-through patterns
- **Shared contract / presenter / builder / renderer to reuse**: `OperationRunService` remains the sole owner of lifecycle transitions; `App\Support\OperationCatalog` remains the canonical operation label source; existing central badge rendering remains the status-like rendering path; existing canonical operations links remain the navigation path into `/admin/operations`
- **Why the existing shared path is sufficient or insufficient**: Existing lifecycle, freshness, and reconciliation truth are sufficient and must remain authoritative. Existing compact-surface presentation is insufficient because it compresses unhealthy active work too aggressively and can contradict the canonical run detail.
- **Allowed deviation and why**: Same meaning, different density is allowed. Tenant cards, dashboard signals, list rows, and run detail may vary in information density, but they may not disagree about whether active work is healthy, late, or likely stuck.
- **Consistency impact**: Active-state language, status emphasis, badge meaning, next-step cues, and drill-through expectations must stay aligned across tenant dashboards, tenant cards, monitoring rows, and canonical detail.
- **Review focus**: Reviewers must verify that no compact surface presents a run as ordinary active work when the canonical monitoring detail presents it as past expectation or likely stuck, and that no new page-local stale heuristic bypasses the shared lifecycle truth.
## 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 |
|---|---|---|---|---|---|---|
| Tenant dashboard activity and attention surfaces | yes | Native Filament dashboard widgets and shared summary primitives | Same tenant-home signal family as existing attention and activity summaries | summary, attention cues, drill-through links | no | Existing surfaces only; no new dashboard page |
| Tenant-local active-run cards and progress summaries | yes | Native Filament widgets/cards and shared run primitives | Same tenant-scoped activity family as existing recent-run and progress hints | compact active-state presentation, drill-through links | no | Existing cards only; no new card family |
| Workspace operations list / monitoring rows | yes | Native Filament table and shared run presentation primitives | Same canonical monitoring family as existing `/admin/operations` list | row-level active-state emphasis, filter continuity, list scanability | no | Existing registry surface only |
| Canonical operation run detail summary | yes | Native Filament detail surface and shared run summary primitives | Same canonical monitoring detail family | top-level active-state explanation, diagnostics separation | no | Detail remains diagnostic-first, not a new page |
## 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 |
|---|---|---|---|---|---|---|---|
| Tenant dashboard activity and attention surfaces | Secondary Context Surface | An operator lands on the tenant dashboard and decides whether current tenant activity needs immediate follow-up | Whether active work is healthy, needs attention, or likely stuck, plus one drill-through into monitoring | Full run history, raw run context, and extended diagnostics on the canonical monitoring surfaces | Secondary because the dashboard signals attention but should not replace the monitoring register | Follows tenant-home to monitoring workflow instead of creating a second workbench | Removes the need to open monitoring just to learn that active work is already stale or late |
| Tenant-local active-run cards and progress summaries | Secondary Context Surface | An operator inspects one tenant-scoped active-work summary and decides whether to open monitoring now | Current run identity, active-state category, and whether the work is merely active or needs attention | Full lifecycle history, stale-cause detail, and related diagnostics on run detail | Secondary because the card summarizes active work inside a broader tenant workflow | Follows tenant-scoped operational follow-up without duplicating monitoring detail | Prevents misleading neutral `Queued` or `Running` summaries from hiding unhealthy work |
| Workspace operations list / monitoring rows | Primary Decision Surface | A workspace operator scans the active operations register and decides which run needs inspection first | Whether active runs are normal, past expectation, or likely stuck, together with run identity and scope context | Full run diagnostics, raw context, and related artifacts on run detail | Primary because this is the canonical list where operators prioritize run follow-up | Follows monitoring and triage workflow instead of forcing row-by-row drill-in | Makes unhealthy active runs scanable without opening every row |
| Canonical operation run detail summary | Tertiary Evidence / Diagnostics Surface | After choosing one run, the operator confirms what kind of active-state problem exists and what it means | Clear top-level explanation of active-state category, lifecycle expectation status, and why diagnostics matter | Raw payloads, stack traces, reconciliation context, and deep technical detail | Tertiary because the operator first decides to inspect the run elsewhere; this page then provides the authoritative diagnosis | Preserves the existing monitoring-detail workflow | Reduces back-and-forth between list rows and diagnostics to understand whether the run is merely active or likely stuck |
## 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 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Tenant dashboard activity and attention surfaces | Monitoring / Queue / Workbench | Read-only Registry / Report Surface | Open the tenant-scoped monitoring slice or the highlighted run | Explicit drill-through CTA or linked run summary | forbidden | Dashboard CTAs remain limited to monitoring follow-up | none | `/admin/t/{tenant}` | `/admin/operations/{run}` | Tenant context, activity state, attention weighting | Operations / Operation | Whether tenant-visible active work is healthy, late, or likely stuck | Embedded summary drill-in only |
| Tenant-local active-run cards and progress summaries | Monitoring / Queue / Workbench | Read-only Registry / Report Surface | Open the run detail or canonical monitoring list | Explicit linked run summary | forbidden | Card CTA stays secondary to the active-state summary | none | `/admin/t/{tenant}` | `/admin/operations/{run}` | Tenant context, active work scope, active-state emphasis | Active operations / Operation | Whether the highlighted active work is ordinary progress or already needs attention | Embedded compact summary; no new surface family |
| Workspace operations list / monitoring rows | List / Table / Bulk | Read-only Registry / Report Surface | Open the run most likely to need follow-up | Full-row open to canonical run detail | required | Existing filters and list controls stay in table chrome, not row noise | none | `/admin/operations` | `/admin/operations/{run}` | Workspace scope, optional tenant prefilter, active-state emphasis | Operations / Operation | Which active runs are healthy versus problematic without opening every row | none |
| Canonical operation run detail summary | Record / Detail / Edit | Detail-first Operational Surface | Inspect one run's active-state explanation and diagnostics | Canonical run detail page | n/a | Related links and secondary diagnostics stay below the top-level summary | Existing dangerous follow-up actions remain wherever already governed; none are added here | `/admin/operations` | `/admin/operations/{run}` | Workspace scope, tenant context when applicable, active-state explanation | Operation run | Why the run is healthy, late, or likely stuck before raw diagnostics appear | canonical evidence detail |
## 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 |
|---|---|---|---|---|---|---|---|---|---|---|
| Tenant dashboard activity and attention surfaces | Tenant operator | Decide whether tenant-visible activity already needs monitoring follow-up | Summary drill-in | Is anything actively happening on this tenant that is already late or likely stuck? | Active-state category, tenant-visible run identity, and one drill-through path | Full run diagnostics and raw lifecycle context remain in monitoring | execution lifecycle, freshness, attention state | none | Open monitoring or run detail | none |
| Tenant-local active-run cards and progress summaries | Tenant operator | Decide whether the highlighted active run is still ordinary progress | Compact run summary | Is this active work still healthy, or does it already need attention? | Run identity, active-state category, elapsed-state emphasis | Deep diagnostics, raw context, and reconciliation detail remain on run detail | execution lifecycle, freshness, active-state interpretation | none | Open run detail | none |
| Workspace operations list / monitoring rows | Workspace operator | Prioritize which active run deserves inspection first | Read-only monitoring registry | Which active runs are normal, and which are already late or likely stuck? | Run identity, scope, high-level lifecycle, and active-state emphasis | Raw payloads and detailed run history remain on run detail | lifecycle, freshness, problem emphasis | none | Open run detail, apply filters | none |
| Canonical operation run detail summary | Workspace operator | Confirm the meaning of the active-state issue before deeper diagnosis | Diagnostic detail surface | Why does this active run read as late or likely stuck, and what should I inspect next? | Top-level active-state explanation, scope, and summary guidance | Raw payloads, stack traces, and extended technical details | lifecycle, freshness, diagnostic readiness | none | Open related context or diagnostics | none |
## Proportionality Review *(mandatory when structural complexity is introduced)*
- **New source of truth?**: no
- **New persisted entity/table/artifact?**: no
- **New abstraction?**: yes — one bounded derived active-state presentation contract expressed through existing presenter and active-run helpers rather than a standalone framework
- **New enum/state/reason family?**: yes — one derived operator-facing category family for `active / normal`, `active / past expectation`, `stale / likely stuck`, and `terminal / no longer active`, composed from existing freshness and problem-class truth
- **New cross-domain UI framework/taxonomy?**: no
- **Current operator problem**: Operators cannot trust compact active-work surfaces because they can understate or hide the difference between healthy progress and likely stuck work.
- **Existing structure is insufficient because**: Existing lifecycle truth lives in monitoring detail and backend services, but compact surfaces can still render active work as neutral `Queued` or `Running` states without exposing that the lifecycle expectation has already been exceeded.
- **Narrowest correct implementation**: Keep all backend lifecycle truth as-is, add one derived presentation contract, and retrofit only the existing tenant/dashboard/monitoring surfaces that already summarize active work.
- **Ownership cost**: Ongoing maintenance for one small presentation category family, cross-surface wording alignment, and regression coverage for fresh versus stale semantics.
- **Alternative intentionally rejected**: Introducing new `OperationRun` statuses or page-local stale heuristics was rejected because both would either widen persistence and lifecycle scope or create contradictory truth outside the existing lifecycle policy.
- **Release truth**: Current-release truth. This feature makes existing active-run observability honest now rather than preparing a future intervention framework.
### Compatibility posture
This feature assumes a pre-production environment.
Backward compatibility, legacy aliases, migration shims, historical fixtures, and compatibility-specific tests are out of scope unless explicitly required by this spec.
Canonical replacement is preferred over preservation.
## Testing / Lane / Runtime Impact *(mandatory for runtime behavior changes)*
- **Test purpose / classification**: Feature
- **Validation lane(s)**: fast-feedback, confidence
- **Why this classification and these lanes are sufficient**: The proof burden is operator-visible active-state semantics across existing admin surfaces. Focused feature coverage is sufficient to prove fresh versus stale differentiation, tenant/workspace visibility boundaries, cross-surface consistency, and no false escalation without needing browser or heavy-governance lanes.
- **New or expanded test families**: Extend `tests/Feature/OpsUx/BulkOperationProgressDbOnlyTest.php`, `tests/Feature/OpsUx/ProgressWidgetFiltersTest.php`, `tests/Feature/OpsUx/ProgressWidgetOverflowTest.php`, `tests/Feature/Filament/RecentOperationsSummaryWidgetTest.php`, `tests/Feature/Filament/DashboardKpisWidgetTest.php`, `tests/Feature/Filament/NeedsAttentionWidgetTest.php`, `tests/Feature/Filament/WorkspaceOverviewOperationsTest.php`, `tests/Feature/Monitoring/OperationLifecycleFreshnessPresentationTest.php`, `tests/Feature/Monitoring/MonitoringOperationsTest.php`, `tests/Feature/Monitoring/OperationsDashboardDrillthroughTest.php`, `tests/Feature/Filament/OperationRunEnterpriseDetailPageTest.php`, `tests/Feature/Operations/TenantlessOperationRunViewerTest.php`, `tests/Feature/RunAuthorizationTenantIsolationTest.php`, and `tests/Feature/OpsUx/NonLeakageWorkspaceOperationsTest.php`.
- **Fixture / helper cost impact**: Moderate. Tests need representative `OperationRun` fixtures for healthy queued/running work, past-expectation work, likely stuck work, terminal transitions during navigation, mixed tenant visibility, and hidden-tenant/non-member isolation boundaries.
- **Heavy-family visibility / justification**: none
- **Special surface test profile**: monitoring-state-page
- **Standard-native relief or required special coverage**: Ordinary feature coverage is sufficient, plus explicit proof that healthy active runs do not escalate falsely and that stale semantics remain consistent after drill-through from tenant surfaces into canonical monitoring.
- **Reviewer handoff**: Reviewers must confirm that the same active-state meaning appears on tenant cards, dashboard attention, operations rows, and canonical run detail; that no new backend status values or UI-only stale heuristics are introduced; and that hidden or out-of-scope runs do not influence visible summaries.
- **Budget / baseline / trend impact**: none
- **Escalation needed**: none
- **Active feature PR close-out entry**: Guardrail
- **Planned validation commands**:
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/OpsUx/BulkOperationProgressDbOnlyTest.php tests/Feature/OpsUx/ProgressWidgetFiltersTest.php tests/Feature/OpsUx/ProgressWidgetOverflowTest.php`
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Filament/RecentOperationsSummaryWidgetTest.php tests/Feature/Filament/DashboardKpisWidgetTest.php tests/Feature/Filament/NeedsAttentionWidgetTest.php tests/Feature/Filament/WorkspaceOverviewOperationsTest.php tests/Feature/Monitoring/OperationLifecycleFreshnessPresentationTest.php tests/Feature/Monitoring/MonitoringOperationsTest.php tests/Feature/Monitoring/OperationsDashboardDrillthroughTest.php tests/Feature/Filament/OperationRunEnterpriseDetailPageTest.php tests/Feature/Operations/TenantlessOperationRunViewerTest.php`
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/RunAuthorizationTenantIsolationTest.php tests/Feature/OpsUx/NonLeakageWorkspaceOperationsTest.php`
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent`
## User Scenarios & Testing *(mandatory)*
### User Story 1 - See unhealthy tenant activity without opening monitoring first (Priority: P1)
As a tenant operator, I want tenant-scoped activity surfaces to distinguish healthy active work from late or likely stuck work, so that I can notice unhealthy runs before manually opening the monitoring register.
**Why this priority**: Tenant cards and dashboard attention surfaces are the highest-frequency compact summaries for active work. If they remain misleading, the rest of the monitoring model is harder to trust.
**Independent Test**: Can be fully tested by seeding healthy and stale tenant-visible active runs, rendering the tenant dashboard and active-run cards, and verifying that only late or likely stuck work escalates while healthy work stays calm.
**Acceptance Scenarios**:
1. **Given** a tenant-visible run is queued or running within its expected lifecycle, **When** the tenant dashboard or active-run card renders, **Then** the run appears as healthy active work and does not escalate as stale or likely stuck.
2. **Given** a tenant-visible run is queued or running well past its expected lifecycle, **When** the same compact surfaces render, **Then** the run is visibly and linguistically distinct from healthy active work.
3. **Given** a run becomes terminal, **When** the tenant-scoped compact surfaces refresh, **Then** the run no longer appears as active work.
---
### User Story 2 - Scan problematic active runs in the canonical operations list (Priority: P1)
As a workspace operator, I want the canonical operations list to make problematic active runs obvious at row level, so that I can prioritize follow-up without opening each run.
**Why this priority**: The canonical operations list is the primary monitoring surface for run triage. If unhealthy active runs are not scanable there, operators lose the central prioritization surface.
**Independent Test**: Can be fully tested by seeding a mix of fresh and stale active runs across visible tenants and verifying that the operations list highlights problematic rows without falsely escalating healthy rows.
**Acceptance Scenarios**:
1. **Given** the operations list contains a mix of healthy active runs and likely stuck runs, **When** the workspace operator opens `/admin/operations`, **Then** the problematic active runs are immediately distinguishable at row level.
2. **Given** the operator enters `/admin/operations` from a tenant-scoped surface, **When** the canonical list opens with tenant context preserved, **Then** the same active-state semantics remain visible within that filtered monitoring slice.
3. **Given** an active run is fresh, **When** the operations list renders, **Then** it does not inherit the same escalation treatment as a late or likely stuck run.
---
### User Story 3 - Keep compact surfaces aligned with canonical run detail (Priority: P2)
As a workspace operator, I want canonical run detail to confirm the same active-state meaning that I saw on tenant and list surfaces, so that I can trust compact summaries without losing diagnostic depth.
**Why this priority**: The canonical detail page is the authoritative diagnostic surface. It must confirm, not contradict, the meaning shown elsewhere.
**Independent Test**: Can be fully tested by navigating from tenant-scoped or list surfaces into run detail for both healthy and stale runs and verifying that the same active-state meaning holds after drill-through.
**Acceptance Scenarios**:
1. **Given** a run reads as likely stuck on a tenant surface or list row, **When** the operator opens canonical run detail, **Then** the detail summary confirms that the run is past expectation or likely stuck before exposing raw diagnostics.
2. **Given** a run changes from active to terminal while the operator navigates between surfaces, **When** the compact surface and run detail refresh, **Then** neither surface continues to present the run as active.
3. **Given** a scheduled or initiator-null run becomes late, **When** the operator inspects it through monitoring, **Then** the active-state semantics remain truthful without implying a new notification channel or mutation behavior.
### Edge Cases
- A run may move from healthy to late or likely stuck between two page loads; the presentation contract must tolerate state changes without requiring a manual semantic reset.
- An operator may open the canonical run detail from a tenant-filtered monitoring slice; the run detail must remain authoritative while preserving scope context where already allowed.
- Multiple active runs may exist for one tenant; compact surfaces must not imply that all tenant activity is stale because one run is problematic.
- A run may become terminal while the operator is on a tenant dashboard or monitoring list; compact surfaces and run detail must converge on non-active presentation after refresh.
- Initiator-null scheduled work may become stale without producing a terminal DB notification; monitoring semantics must remain truthful without inventing a new scheduled-run notification contract.
## Requirements *(mandatory)*
**Constitution alignment (required):** This feature adds no Microsoft Graph calls, no new write flow, and no new `OperationRun`. It changes only how existing run lifecycle and freshness truth are interpreted on admin-plane operator surfaces.
**Constitution alignment (PROP-001 / ABSTR-001 / PERSIST-001 / STATE-001 / BLOAT-001):** The feature introduces one derived active-state presentation contract because current-release operator trust requires it now. A narrower local patch is insufficient because the same truth must remain aligned across tenant cards, dashboard signals, list rows, and canonical detail. The addition stays derived, not persisted.
**Constitution alignment (XCUT-001):** This feature is cross-cutting across status messaging and dashboard/monitoring surfaces. It reuses the existing lifecycle, freshness, badge, and canonical monitoring paths, and it allows only one bounded deviation: same meaning, different density.
**Constitution alignment (TEST-GOV-001):** Focused feature tests on tenant dashboard surfaces, tenant active-run cards, monitoring rows, and run detail are the narrowest sufficient proof. No browser or heavy-governance lane is required.
**Constitution alignment (OPS-UX):** Existing Ops-UX 3-surface feedback remains unchanged. Toasts stay intent-only, progress surfaces remain the existing active-work surfaces, and terminal DB notifications stay unchanged. `OperationRun.status` and `OperationRun.outcome` transitions remain service-owned exclusively through `OperationRunService`. This feature must not introduce new `summary_counts` keys or any new scheduled-run notification semantics.
**Constitution alignment (RBAC-UX):** The feature operates only in the admin plane (`/admin` and `/admin/t/{tenant}/...`). Tenant-scoped compact surfaces remain tenant-entitlement safe, while canonical monitoring routes continue to enforce workspace membership and tenant entitlement on tenant-owned runs. No cross-plane visibility or raw capability checks may be added.
**Constitution alignment (BADGE-001):** Any changed status-like emphasis must continue to use centralized badge or status rendering. No page-local mapping from stale-detection inputs to color or label semantics is allowed.
**Constitution alignment (UI-FIL-001):** Touched surfaces must use native Filament widgets, tables, summaries, and existing shared status primitives. No custom local status markup or page-local color systems may replace shared run presentation.
**Constitution alignment (UI-NAMING-001):** Operator-facing language must use one canonical vocabulary for active-state meaning: healthy active work, past expected lifecycle, likely stuck, and no longer active. Implementation-first phrases or raw stale heuristics must not become primary labels.
**Constitution alignment (DECIDE-001):** The operations list remains the primary decision surface for run triage. Tenant dashboard surfaces remain secondary context surfaces, and canonical run detail remains the diagnostic surface. This feature must make those roles calmer and clearer, not create a new workbench.
**Constitution alignment (UI-CONST-001 / UI-SURF-001 / ACTSURF-001 / UI-HARD-001 / UI-EX-001 / UI-REVIEW-001 / HDR-001):** No new route, no new inspect model, and no new destructive action family are introduced. The canonical inspect model remains the run detail page. Compact surfaces may add emphasis and drill-through clarity only.
**Constitution alignment (UI-SEM-001 / LAYER-001 / TEST-TRUTH-001):** Direct mapping from `queued` and `running` alone is insufficient because lifecycle expectation and freshness change the operator meaning of active work. The feature adds one bounded derived interpretation layer and must prove business consequences across surfaces rather than only unit-testing one presenter.
### Functional Requirements
- **FR-001**: The system MUST derive one active-state presentation category for visible runs using existing lifecycle, freshness, and reconciliation truth.
- **FR-002**: The derived presentation contract MUST distinguish at least `active / normal`, `active / past expected lifecycle`, `stale / likely stuck`, and `terminal / no longer active`.
- **FR-003**: The feature MUST NOT introduce new `OperationRun.status` or `OperationRun.outcome` values to express those categories.
- **FR-004**: Tenant dashboard activity and attention surfaces MUST visibly and linguistically distinguish healthy active work from active work that is late or likely stuck.
- **FR-005**: Tenant-local active-run cards and progress summaries MUST surface the same active-state meaning as the tenant dashboard attention layer for the same run.
- **FR-006**: The workspace operations list MUST make late or likely stuck active runs distinguishable at row level without requiring drill-in.
- **FR-007**: Canonical run detail MUST explain the active-state category before exposing raw diagnostics, and that explanation MUST remain consistent with the compact surfaces that linked into it.
- **FR-008**: A run that is presented as late or likely stuck on canonical run detail MUST NOT appear as ordinary healthy active work on any covered tenant or monitoring surface.
- **FR-009**: Healthy active runs MUST NOT inherit stale or likely-stuck emphasis solely because they are `queued` or `running`.
- **FR-010**: When a run becomes terminal, covered compact surfaces MUST stop presenting it as active work on the next refresh cycle.
- **FR-011**: Existing lifecycle, freshness, and reconciliation logic MUST remain the source of truth; covered surfaces MUST NOT implement separate page-local stale heuristics.
- **FR-012**: When operators enter canonical monitoring from tenant context, existing tenant-prefilter continuity MAY be preserved, but the active-state semantics MUST remain identical to the unfiltered canonical list.
- **FR-013**: Covered surfaces MUST remain tenant-safe and workspace-safe; hidden runs or hidden tenants MUST NOT influence visible active-state summaries.
- **FR-014**: Scheduled or initiator-null runs MUST use the same active-state presentation rules as user-initiated runs where lifecycle and freshness truth are comparable.
- **FR-015**: This feature MUST NOT add retry, cancel, force-fail, reconcile-now, or other intervention actions to any covered surface.
- **FR-016**: Existing Ops-UX toast, progress-surface, and terminal-notification behavior MUST remain unchanged.
## UI Action Matrix *(mandatory when Filament is changed)*
| Surface | Location | Header Actions | Inspect Affordance (List/Table) | Row Actions (max 2 visible) | Bulk Actions (grouped) | Empty-State CTA(s) | View Header Actions | Create/Edit Save+Cancel | Audit log? | Notes / Exemptions |
|---|---|---|---|---|---|---|---|---|---|---|
| Tenant dashboard activity and attention surfaces | `/admin/t/{tenant}` | none added by this feature | Explicit dashboard CTA or linked run summary | none | none | none | n/a | n/a | No new mutation audit because the surface remains read-only | Action Surface Contract satisfied. Dashboard remains a signal surface, not a second workbench. UI-FIL-001 satisfied through native widgets and shared status primitives. |
| Tenant-local active-run cards and progress summaries | Tenant-scoped admin surfaces that already summarize active work | none added by this feature | Explicit linked run summary | none | none | none | n/a | n/a | No new mutation audit because the surface remains read-only | One primary inspect model remains the run detail. No redundant View action is added. |
| Workspace operations list | `/admin/operations` | Existing filters only; none added by this feature | Full-row open to run detail | none added by this feature | none | Existing list empty state unchanged | n/a | n/a | No new mutation audit because the surface remains read-only | Action Surface Contract satisfied. Row click stays the only primary inspect model. |
| Canonical operation run detail summary | `/admin/operations/{run}` | Existing safe/context actions unchanged | n/a | n/a | n/a | n/a | Existing detail/header actions unchanged | n/a | No new mutation audit because the feature changes summary semantics only | No exemption needed. This feature changes top-level explanation, not the action layout. |
### Key Entities *(include if feature involves data)*
- **Active-state presentation category**: A derived operator-facing category that explains whether visible active work is healthy, late, likely stuck, or no longer active.
- **Lifecycle expectation window**: The existing timing and policy truth that determines when active work has exceeded its normal expected lifecycle.
- **Stale active run**: An existing `OperationRun` whose lifecycle and freshness truth indicate that active work is likely stuck rather than merely still in progress.
## Success Criteria *(mandatory)*
### Measurable Outcomes
- **SC-001**: In acceptance review, an operator can distinguish healthy tenant-visible active work from likely stuck work in one scan of the covered tenant surfaces.
- **SC-002**: 100% of covered fresh-versus-stale automated scenarios show the correct active-state category on the tenant dashboard, tenant active-run cards, and workspace operations list.
- **SC-003**: 100% of covered drill-through scenarios preserve the same active-state meaning between compact surfaces and canonical run detail.
- **SC-004**: 100% of covered healthy-active scenarios avoid false escalation when lifecycle expectation has not yet been exceeded.

View File

@ -0,0 +1,228 @@
# Tasks: Operation Run Active-State Visibility & Stale Escalation
**Input**: Design documents from `/specs/233-stale-run-visibility/`
**Prerequisites**: `plan.md` (required), `spec.md` (required for user stories), `research.md`, `data-model.md`, `contracts/operation-run-active-state-visibility.logical.openapi.yaml`, `quickstart.md`
**Tests**: Required. This feature changes runtime behavior across tenant progress surfaces, tenant dashboard summaries, workspace summaries, and canonical monitoring detail, so Pest coverage must be added or updated in `apps/platform/tests/Feature/OpsUx/BulkOperationProgressDbOnlyTest.php`, `apps/platform/tests/Feature/OpsUx/ProgressWidgetFiltersTest.php`, `apps/platform/tests/Feature/OpsUx/ProgressWidgetOverflowTest.php`, `apps/platform/tests/Feature/Filament/RecentOperationsSummaryWidgetTest.php`, `apps/platform/tests/Feature/Filament/DashboardKpisWidgetTest.php`, `apps/platform/tests/Feature/Filament/NeedsAttentionWidgetTest.php`, `apps/platform/tests/Feature/Filament/WorkspaceOverviewOperationsTest.php`, `apps/platform/tests/Feature/Monitoring/OperationLifecycleFreshnessPresentationTest.php`, `apps/platform/tests/Feature/Monitoring/MonitoringOperationsTest.php`, `apps/platform/tests/Feature/Monitoring/OperationsDashboardDrillthroughTest.php`, `apps/platform/tests/Feature/Filament/OperationRunEnterpriseDetailPageTest.php`, `apps/platform/tests/Feature/Operations/TenantlessOperationRunViewerTest.php`, `apps/platform/tests/Feature/RunAuthorizationTenantIsolationTest.php`, and `apps/platform/tests/Feature/OpsUx/NonLeakageWorkspaceOperationsTest.php`.
**Operations**: No new `OperationRun` is introduced. Existing lifecycle truth, reconciliation, toast/progress/terminal notification behavior, and service-owned status/outcome transitions must remain unchanged.
**RBAC**: The feature stays in the admin plane (`/admin` and `/admin/t/{tenant}/...`). It must preserve current tenant-entitlement and workspace-entitlement behavior, including non-member `404`, in-scope capability denial semantics, and tenant-safe summaries with no cross-tenant leakage.
**UI / Surface Guardrails**: The changed surfaces are native Filament widgets/resources/pages plus one existing Livewire progress component. The feature keeps `monitoring-state-page` coverage for canonical monitoring surfaces, uses `standard-native-filament` relief elsewhere, and remains `review-mandatory` because multiple existing operator surfaces must converge on the same truth.
**Filament UI Action Surfaces**: `RecentOperationsSummary`, tenant dashboard widgets, `OperationRunResource`, and `TenantlessOperationRunViewer` keep their existing inspect/open model. No new header, row, bulk, retry, cancel, or destructive actions are introduced.
**Badges**: Status-like semantics must stay on `BadgeCatalog` / `BadgeRenderer` and existing shared `OperationRun` presenter paths. No page-local stale badge mapping is allowed.
**Organization**: Tasks are grouped by user story so each slice is independently implementable and testable. Recommended delivery order is `US1 -> US2 -> US3` because tenant-surface honesty is the most urgent gap, canonical list scanability builds on the same truth, and detail-surface confirmation should close last against the final compact-surface semantics.
## Test Governance Checklist
- [X] Lane assignment is named and is the narrowest sufficient proof for the changed behavior.
- [X] New or changed tests stay in the smallest honest family, and any heavy-governance or browser addition is explicit.
- [X] Shared helpers, factories, seeds, fixtures, and context defaults stay cheap by default; any widening is isolated or documented.
- [X] Planned validation commands cover the change without pulling in unrelated lane cost.
- [X] The declared surface test profile or `standard-native-filament` relief is explicit.
- [X] Any material budget, baseline, trend, or escalation note is recorded in the active spec or PR.
## Phase 1: Setup (Shared Surface Scaffolding)
**Purpose**: Prepare the focused regression surfaces that will prove fresh-versus-stale semantics before runtime files are edited.
- [X] T001 [P] Extend stale-versus-fresh progress-overlay scaffolding in `apps/platform/tests/Feature/OpsUx/BulkOperationProgressDbOnlyTest.php`, `apps/platform/tests/Feature/OpsUx/ProgressWidgetFiltersTest.php`, and `apps/platform/tests/Feature/OpsUx/ProgressWidgetOverflowTest.php`
- [X] T002 [P] Extend tenant dashboard and tenant-summary semantics scaffolding in `apps/platform/tests/Feature/Filament/RecentOperationsSummaryWidgetTest.php`, `apps/platform/tests/Feature/Filament/DashboardKpisWidgetTest.php`, `apps/platform/tests/Feature/Filament/NeedsAttentionWidgetTest.php`, and `apps/platform/tests/Feature/Monitoring/OperationsDashboardDrillthroughTest.php`
- [X] T003 [P] Extend workspace monitoring and visibility-safety scaffolding in `apps/platform/tests/Feature/Filament/WorkspaceOverviewOperationsTest.php`, `apps/platform/tests/Feature/Monitoring/MonitoringOperationsTest.php`, `apps/platform/tests/Feature/Monitoring/OperationLifecycleFreshnessPresentationTest.php`, `apps/platform/tests/Feature/RunAuthorizationTenantIsolationTest.php`, and `apps/platform/tests/Feature/OpsUx/NonLeakageWorkspaceOperationsTest.php`
- [X] T004 [P] Extend canonical detail and drill-through continuity scaffolding in `apps/platform/tests/Feature/Filament/OperationRunEnterpriseDetailPageTest.php` and `apps/platform/tests/Feature/Operations/TenantlessOperationRunViewerTest.php`
**Checkpoint**: Focused tenant, workspace, and canonical test surfaces are ready to fail on stale-hidden regressions before implementation begins.
---
## Phase 2: Foundational (Blocking Truth And Shared Contract)
**Purpose**: Stabilize the one freshness-to-surface contract before any individual surface is changed.
**Critical**: No user story work should begin until this phase is complete.
- [X] T005 Freeze the canonical stale/fresh truth inputs and any needed thin derived adapter boundaries in `apps/platform/app/Models/OperationRun.php`, `apps/platform/app/Support/Operations/OperationRunFreshnessState.php`, `apps/platform/app/Support/OpsUx/OperationUxPresenter.php`, and `apps/platform/app/Support/OpsUx/ActiveRuns.php`
- [X] T006 [P] Refresh the feature contract artifacts in `specs/233-stale-run-visibility/contracts/operation-run-active-state-visibility.logical.openapi.yaml` and `specs/233-stale-run-visibility/quickstart.md` so implementation and review language stay aligned with the finalized shared truth path
**Checkpoint**: The feature has one agreed freshness/problem-class/presenter contract and the docs match that contract before surface-by-surface retrofits begin.
---
## Phase 3: User Story 1 - See unhealthy tenant activity without opening monitoring first (Priority: P1) 🎯 MVP
**Goal**: Tenant-scoped dashboard and progress surfaces distinguish healthy active work from late or likely stuck work without inventing a second stale heuristic.
**Independent Test**: Seed fresh and stale tenant-visible queued/running runs, render tenant dashboard and progress surfaces, and verify that healthy work stays calm while stale-active work remains visible and elevated until the run becomes terminal.
### Tests for User Story 1
- [X] T007 [P] [US1] Add fresh-versus-stale tenant progress assertions, including polling continuity while only stale-active runs remain, in `apps/platform/tests/Feature/OpsUx/BulkOperationProgressDbOnlyTest.php`, `apps/platform/tests/Feature/OpsUx/ProgressWidgetFiltersTest.php`, and `apps/platform/tests/Feature/OpsUx/ProgressWidgetOverflowTest.php`
- [X] T008 [P] [US1] Add tenant summary and tenant dashboard copy/assertion coverage for calm-versus-elevated active work in `apps/platform/tests/Feature/Filament/RecentOperationsSummaryWidgetTest.php`, `apps/platform/tests/Feature/Filament/DashboardKpisWidgetTest.php`, `apps/platform/tests/Feature/Filament/NeedsAttentionWidgetTest.php`, and `apps/platform/tests/Feature/Monitoring/OperationsDashboardDrillthroughTest.php`
### Implementation for User Story 1
- [X] T009 [P] [US1] Update stale-active visibility and polling semantics in `apps/platform/app/Support/OpsUx/ActiveRuns.php`, `apps/platform/app/Livewire/BulkOperationProgress.php`, `apps/platform/resources/views/livewire/bulk-operation-progress.blade.php`, and `apps/platform/resources/views/livewire/bulk-operation-progress-wrapper.blade.php`
- [X] T010 [P] [US1] Align tenant activity summaries with the shared presenter/badge truth in `apps/platform/app/Filament/Widgets/Tenant/RecentOperationsSummary.php`, `apps/platform/resources/views/filament/widgets/tenant/recent-operations-summary.blade.php`, `apps/platform/app/Filament/Widgets/Dashboard/DashboardKpis.php`, and `apps/platform/app/Filament/Widgets/Dashboard/NeedsAttention.php`
- [X] T011 [US1] Tighten tenant-surface copy and density handling in `apps/platform/app/Support/OpsUx/OperationUxPresenter.php` and any touched shared badge mappings in `apps/platform/app/Support/Badges/Domains/OperationRunStatusBadge.php` without adding a second semantic framework
- [X] T012 [US1] Run the US1 tenant-surface verification flow from `specs/233-stale-run-visibility/quickstart.md`
**Checkpoint**: User Story 1 is independently functional and tenant operators can see unhealthy active work before opening canonical monitoring.
---
## Phase 4: User Story 2 - Scan problematic active runs in the canonical operations list (Priority: P1)
**Goal**: Workspace monitoring rows and workspace recent-operation summaries make problematic active runs obvious at scan time without falsely escalating fresh work.
**Independent Test**: Seed a mixed slice of fresh and stale active runs across visible tenants, open workspace summaries and `/admin/operations`, and verify that stale-active rows are immediately distinguishable while fresh active rows remain calm.
### Tests for User Story 2
- [X] T013 [P] [US2] Add workspace summary, recency, and visibility-safety assertions for fresh-versus-stale active work in `apps/platform/tests/Feature/Filament/WorkspaceOverviewOperationsTest.php`, `apps/platform/tests/Feature/Monitoring/MonitoringOperationsTest.php`, `apps/platform/tests/Feature/RunAuthorizationTenantIsolationTest.php`, and `apps/platform/tests/Feature/OpsUx/NonLeakageWorkspaceOperationsTest.php`
- [X] T014 [P] [US2] Add canonical operations-list assertions for row-level stale-active scanability and tenant-prefilter continuity in `apps/platform/tests/Feature/Monitoring/OperationLifecycleFreshnessPresentationTest.php` and `apps/platform/tests/Feature/Monitoring/OperationsDashboardDrillthroughTest.php`
### Implementation for User Story 2
- [X] T015 [P] [US2] Align workspace summary payloads and view rendering in `apps/platform/app/Support/Workspaces/WorkspaceOverviewBuilder.php`, `apps/platform/app/Filament/Widgets/Workspace/WorkspaceRecentOperations.php`, and `apps/platform/resources/views/filament/widgets/workspace/workspace-recent-operations.blade.php`
- [X] T016 [P] [US2] Tighten canonical list scanability and stale-active row semantics in `apps/platform/app/Filament/Widgets/Dashboard/RecentOperations.php` and `apps/platform/app/Filament/Resources/OperationRunResource.php`
- [X] T017 [US2] Reconcile any remaining stale-active badge/copy differences across workspace and canonical list surfaces in `apps/platform/app/Support/OpsUx/OperationUxPresenter.php` and `apps/platform/app/Support/Badges/Domains/OperationRunStatusBadge.php`
- [X] T018 [US2] Run the US2 workspace-list verification flow from `specs/233-stale-run-visibility/quickstart.md`
**Checkpoint**: User Story 2 is independently functional and workspace operators can scan the canonical monitoring list for unhealthy active work without opening every row.
---
## Phase 5: User Story 3 - Keep compact surfaces aligned with canonical run detail (Priority: P2)
**Goal**: Canonical run detail confirms the same active-state meaning that compact tenant and workspace surfaces already communicate, including terminal transitions and stale lineage.
**Independent Test**: Navigate from compact tenant/workspace surfaces into canonical run detail for fresh, stale, and reconciled-terminal runs, then verify that the top summary preserves the same meaning before deeper diagnostics render.
### Tests for User Story 3
- [X] T019 [P] [US3] Add canonical detail summary and stale-lineage assertions in `apps/platform/tests/Feature/Filament/OperationRunEnterpriseDetailPageTest.php` and `apps/platform/tests/Feature/Operations/TenantlessOperationRunViewerTest.php`
- [X] T020 [P] [US3] Add refresh-boundary and terminal-transition consistency assertions spanning compact-to-detail flows in `apps/platform/tests/Feature/Monitoring/OperationLifecycleFreshnessPresentationTest.php` and `apps/platform/tests/Feature/Monitoring/MonitoringOperationsTest.php`
### Implementation for User Story 3
- [X] T021 [P] [US3] Align canonical detail summary copy and decision-zone truth in `apps/platform/app/Filament/Resources/OperationRunResource.php` and `apps/platform/app/Support/OpsUx/OperationUxPresenter.php`
- [X] T022 [US3] Align tenantless canonical viewer summary behavior and drill-through continuity in `apps/platform/app/Filament/Pages/Operations/TenantlessOperationRunViewer.php` and any touched related monitoring helpers under `apps/platform/app/Support/OperationRunLinks.php`
- [X] T023 [US3] Run the US3 compact-to-detail verification flow from `specs/233-stale-run-visibility/quickstart.md`
**Checkpoint**: User Story 3 is independently functional and canonical run detail confirms, rather than contradicts, the compact active-state meaning.
---
## Phase 6: Polish & Cross-Cutting Concerns
**Purpose**: Finalize documentation, formatting, and focused validation for the whole feature without widening scope.
- [X] T024 [P] Refresh `specs/233-stale-run-visibility/plan.md`, `specs/233-stale-run-visibility/research.md`, and `specs/233-stale-run-visibility/data-model.md` if implementation proves a thinner shared contract or adjusts touched file scope
- [X] T025 Run formatting on touched application and test files with `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent`
- [X] T026 Run the focused Pest suite from `specs/233-stale-run-visibility/quickstart.md` against `apps/platform/tests/Feature/OpsUx/BulkOperationProgressDbOnlyTest.php`, `apps/platform/tests/Feature/OpsUx/ProgressWidgetFiltersTest.php`, `apps/platform/tests/Feature/OpsUx/ProgressWidgetOverflowTest.php`, `apps/platform/tests/Feature/Filament/RecentOperationsSummaryWidgetTest.php`, `apps/platform/tests/Feature/Filament/DashboardKpisWidgetTest.php`, `apps/platform/tests/Feature/Filament/NeedsAttentionWidgetTest.php`, `apps/platform/tests/Feature/Filament/WorkspaceOverviewOperationsTest.php`, `apps/platform/tests/Feature/Monitoring/OperationLifecycleFreshnessPresentationTest.php`, `apps/platform/tests/Feature/Monitoring/MonitoringOperationsTest.php`, `apps/platform/tests/Feature/Monitoring/OperationsDashboardDrillthroughTest.php`, `apps/platform/tests/Feature/Filament/OperationRunEnterpriseDetailPageTest.php`, `apps/platform/tests/Feature/Operations/TenantlessOperationRunViewerTest.php`, `apps/platform/tests/Feature/RunAuthorizationTenantIsolationTest.php`, and `apps/platform/tests/Feature/OpsUx/NonLeakageWorkspaceOperationsTest.php`
- [X] T027 Record the finalized affected surfaces, any retained density-specific copy decisions, and the `document-in-feature` test-governance disposition in `specs/233-stale-run-visibility/plan.md`
---
## Dependencies & Execution Order
### Phase Dependencies
- **Setup (Phase 1)**: No dependencies; can start immediately.
- **Foundational (Phase 2)**: Depends on Setup completion and blocks all user story work.
- **User Story 1 (Phase 3)**: Depends on Foundational completion and is the recommended first implementation increment.
- **User Story 2 (Phase 4)**: Depends on Foundational completion and can begin after the shared truth contract is stable.
- **User Story 3 (Phase 5)**: Depends on User Stories 1 and 2 because canonical detail should be aligned against the final compact-surface semantics.
- **Polish (Phase 6)**: Depends on all desired user stories being complete.
### User Story Dependencies
- **US1 (P1)**: Starts immediately after Foundational and delivers the highest-value tenant-surface honesty fix.
- **US2 (P1)**: Can begin after Foundational, but is easiest to complete after US1 settles the compact stale-active vocabulary.
- **US3 (P2)**: Starts after US1 and US2 stabilize because canonical detail should confirm the final compact-surface contract, not compete with an in-progress one.
### Within Each User Story
- Story tests should be written and fail before the corresponding implementation tasks are considered complete.
- Shared files such as `apps/platform/app/Support/OpsUx/OperationUxPresenter.php`, `apps/platform/app/Support/OpsUx/ActiveRuns.php`, and `apps/platform/app/Filament/Resources/OperationRunResource.php` should be edited sequentially even when surrounding tasks are otherwise parallelizable.
- Each story's verification task should complete before moving to the next priority slice when working sequentially.
### Parallel Opportunities
- **Setup**: `T001`, `T002`, `T003`, and `T004` can run in parallel.
- **Foundational**: `T006` can run in parallel with the tail end of `T005` once the shared contract is clear.
- **US1 tests**: `T007` and `T008` can run in parallel.
- **US1 implementation**: `T009` and `T010` can run in parallel; `T011` should follow once the touched surface outputs are visible.
- **US2 tests**: `T013` and `T014` can run in parallel.
- **US2 implementation**: `T015` and `T016` can run in parallel; `T017` should follow once both summary and canonical list semantics are visible.
- **US3 tests**: `T019` and `T020` can run in parallel.
- **Polish**: `T024` can run in parallel with `T025` after runtime implementation is stable.
---
## Parallel Example: User Story 1
```bash
# Run tenant-surface test work in parallel:
T007 apps/platform/tests/Feature/OpsUx/BulkOperationProgressDbOnlyTest.php, apps/platform/tests/Feature/OpsUx/ProgressWidgetFiltersTest.php, apps/platform/tests/Feature/OpsUx/ProgressWidgetOverflowTest.php
T008 apps/platform/tests/Feature/Filament/RecentOperationsSummaryWidgetTest.php, apps/platform/tests/Feature/Filament/DashboardKpisWidgetTest.php, apps/platform/tests/Feature/Filament/NeedsAttentionWidgetTest.php, apps/platform/tests/Feature/Monitoring/OperationsDashboardDrillthroughTest.php
# Then split the non-overlapping implementation work:
T009 apps/platform/app/Support/OpsUx/ActiveRuns.php, apps/platform/app/Livewire/BulkOperationProgress.php, apps/platform/resources/views/livewire/bulk-operation-progress.blade.php, apps/platform/resources/views/livewire/bulk-operation-progress-wrapper.blade.php
T010 apps/platform/app/Filament/Widgets/Tenant/RecentOperationsSummary.php, apps/platform/resources/views/filament/widgets/tenant/recent-operations-summary.blade.php, apps/platform/app/Filament/Widgets/Dashboard/DashboardKpis.php, apps/platform/app/Filament/Widgets/Dashboard/NeedsAttention.php
```
---
## Parallel Example: User Story 2
```bash
# Run workspace-summary, visibility-safety, and canonical-list assertions in parallel:
T013 apps/platform/tests/Feature/Filament/WorkspaceOverviewOperationsTest.php, apps/platform/tests/Feature/Monitoring/MonitoringOperationsTest.php, apps/platform/tests/Feature/RunAuthorizationTenantIsolationTest.php, and apps/platform/tests/Feature/OpsUx/NonLeakageWorkspaceOperationsTest.php
T014 apps/platform/tests/Feature/Monitoring/OperationLifecycleFreshnessPresentationTest.php and apps/platform/tests/Feature/Monitoring/OperationsDashboardDrillthroughTest.php
```
---
## Parallel Example: User Story 3
```bash
# Run canonical-detail and transition-consistency assertions in parallel:
T019 apps/platform/tests/Feature/Filament/OperationRunEnterpriseDetailPageTest.php and apps/platform/tests/Feature/Operations/TenantlessOperationRunViewerTest.php
T020 apps/platform/tests/Feature/Monitoring/OperationLifecycleFreshnessPresentationTest.php and apps/platform/tests/Feature/Monitoring/MonitoringOperationsTest.php
```
---
## Implementation Strategy
### First Implementation Increment (User Story 1 Only)
1. Complete Phase 1: Setup.
2. Complete Phase 2: Foundational.
3. Complete Phase 3: User Story 1.
4. Validate the feature with `T012` before widening the slice.
### Incremental Delivery
1. Stabilize the shared freshness/problem-class contract and docs.
2. Ship US1 to fix the tenant-surface blind spot and stale-hidden progress behavior.
3. Ship US2 to make workspace summaries and the canonical list scanable.
4. Ship US3 to ensure canonical detail confirms the same meaning after drill-through.
5. Finish with formatting, focused tests, and close-out notes.
### Parallel Team Strategy
With multiple developers:
1. One contributor can own Ops UX progress visibility while another extends tenant dashboard/widget assertions.
2. After Phase 2, one contributor can update workspace summary builders while another adjusts canonical list/detail semantics.
3. Keep `OperationUxPresenter.php`, `ActiveRuns.php`, and `OperationRunResource.php` serialized because they anchor the shared truth and surface contract.
---
## Notes
- `[P]` marks tasks that can run in parallel once prerequisites are satisfied and touched files do not overlap.
- `[US1]`, `[US2]`, and `[US3]` map directly to the feature specification user stories.
- The first working increment is Phase 1 through Phase 3, but the feature-complete approved scope remains Phase 1 through Phase 5 because canonical list and detail alignment are part of the accepted problem statement.
- All tasks above use exact repository paths and keep the work bounded to the existing admin-plane monitoring and progress surfaces.

View File

@ -0,0 +1,35 @@
# Specification Quality Checklist: Dead Transitional Residue Cleanup
**Purpose**: Validate specification completeness and quality before proceeding to planning
**Created**: 2026-04-23
**Feature**: [spec.md](../spec.md)
## Content Quality
- [x] No implementation details (languages, frameworks, APIs)
- [x] Focused on user value and business needs
- [x] Written for non-technical stakeholders
- [x] All mandatory sections completed
## Requirement Completeness
- [x] No [NEEDS CLARIFICATION] markers remain
- [x] Requirements are testable and unambiguous
- [x] Success criteria are measurable
- [x] Success criteria are technology-agnostic (no implementation details)
- [x] All acceptance scenarios are defined
- [x] Edge cases are identified
- [x] Scope is clearly bounded
- [x] Dependencies and assumptions identified
## Feature Readiness
- [x] All functional requirements have clear acceptance criteria
- [x] User scenarios cover primary flows
- [x] Feature meets measurable outcomes defined in Success Criteria
- [x] No implementation details leak into specification
## Notes
- Validated against the current cleanup sequencing in `docs/product/spec-candidates.md` and the active near-term roadmap focus in `docs/product/roadmap.md` on 2026-04-23.
- Scope stays intentionally narrow: remove dead residue first, defer schema removal and adjacent cleanup strands to follow-up specs.

View File

@ -0,0 +1,86 @@
# Phase 1 Data Model: Dead Transitional Residue Cleanup
## Overview
This feature introduces no new table, enum, or persisted artifact. It narrows the active runtime language around two already-existing truth domains:
1. Baseline profile lifecycle truth should flow only through `BaselineProfileStatus`.
2. Tenant app-status should remain, at most, historical stored data, not active runtime/support truth.
## Persistent Source Truths
### BaselineProfile
**Purpose**: Workspace-owned baseline profile record.
**Key fields**:
- `id`
- `workspace_id`
- `name`
- `status`
- `capture_mode`
- `active_snapshot_id`
**Validation rules**:
- `status` is cast to `BaselineProfileStatus` and is the only active lifecycle contract for draft, active, and archived behavior.
- Deprecated alias constants on the model are not part of persistent truth and can be removed once no runtime caller depends on them.
### Tenant
**Purpose**: Tenant-owned lifecycle and management record.
**Key fields**:
- `id`
- `workspace_id`
- `name`
- `status`
- `rbac_status`
- `app_status` (historical legacy field)
**Validation rules**:
- `status` remains the active tenant lifecycle truth.
- `rbac_status` remains a separate active management truth.
- `app_status` may remain stored historically, but current runtime/support paths must not treat it as active default truth.
## Support Artifacts In Scope
### Deprecated alias layer
**Artifact**: `BaselineProfile::STATUS_DRAFT`, `STATUS_ACTIVE`, `STATUS_ARCHIVED`
**Role after cleanup**:
- removed from active runtime language
### Legacy badge layer
**Artifacts**:
- `BadgeDomain::TenantAppStatus`
- `BadgeCatalog` mapper entry for tenant app status
- `TenantAppStatusBadge`
**Role after cleanup**:
- removed if no runtime consumer remains
### Legacy default setup
**Artifacts**:
- `TenantFactory` default `app_status => 'ok'`
- `SeedBackupHealthBrowserFixture` default `app_status => 'ok'`
**Role after cleanup**:
- removed as ambient defaults
- legacy `app_status` becomes explicit per-test or per-scenario setup only
## Behavioral Rules
1. Removing dead residue must not change baseline profile archive/list/workspace behavior.
2. Removing dead residue must not change tenant lifecycle or RBAC truth behavior.
3. Tests that still need a legacy `app_status` value must set it explicitly in the scenario.
4. Historical schema and migrations remain historical artifacts, not cleanup targets in this slice.
## No New Persistence
- No new table is introduced.
- No new enum or reason family is introduced.
- No new derived readiness or cleanup artifact is introduced.
- No stored field is repurposed into a new active truth contract.

View File

@ -0,0 +1,245 @@
# Implementation Plan: Dead Transitional Residue Cleanup
**Branch**: `234-dead-transitional-residue` | **Date**: 2026-04-23 | **Spec**: [spec.md](./spec.md)
**Input**: Feature specification from `/specs/234-dead-transitional-residue/spec.md`
**Note**: This plan keeps historical tenant `app_status` storage and historical migrations intact. It removes only dead runtime alias/support paths and tightens fixtures/tests so legacy values become explicit opt-in setup rather than ambient repo truth.
## Summary
Remove the dead `BaselineProfile::STATUS_*` alias layer and retire tenant app-status residue from the centralized badge catalog, default test fixtures, browser smoke seed data, and legacy-facing tests. The implementation stays intentionally small: no schema change, no new status family, no operator-surface redesign, and no compatibility shim. The proof burden is that current tenant truth and baseline profile behavior remain unchanged while the dead semantics disappear from active runtime language.
## Technical Context
**Language/Version**: PHP 8.4.15, Laravel 12, Filament v5, Livewire v4
**Primary Dependencies**: `App\Models\BaselineProfile`, `App\Support\Baselines\BaselineProfileStatus`, `App\Support\Badges\BadgeCatalog`, `App\Support\Badges\BadgeDomain`, `Database\Factories\TenantFactory`, `App\Console\Commands\SeedBackupHealthBrowserFixture`, existing tenant-truth and baseline-profile Pest tests
**Storage**: Existing PostgreSQL `baseline_profiles` and `tenants` tables; no new persistence and no schema migration in this slice
**Testing**: Pest v4 feature and unit tests through Laravel Sail
**Validation Lanes**: `fast-feedback`, `confidence`
**Target Platform**: Laravel admin web application in Sail containers with admin routes under `/admin`
**Project Type**: Monorepo with one Laravel runtime in `apps/platform` and spec artifacts at repository root
**Performance Goals**: Preserve current request/query behavior; cleanup must not add runtime branching, new queries, or new UI layers
**Constraints**: No schema change, no compatibility aliases, no new badge/readiness domain, no authorization changes, no global-search broadening, and no new operator-facing surface
**Scale/Scope**: One Eloquent model, one central badge registry path, one legacy badge mapper, one tenant factory, one browser fixture command, and focused tenant/baseline regression families
## Filament v5 Implementation Contract
- **Livewire v4.0+ compliance**: Preserved. The cleanup does not introduce any legacy Livewire patterns and does not add new Filament component types.
- **Provider registration location**: Unchanged. Panel providers remain registered in `bootstrap/providers.php`.
- **Global search coverage**: `TenantResource` remains globally searchable and already has View/Edit pages. `BaselineProfileResource` keeps global search disabled via `$isGloballySearchable = false` and already has View/Edit pages. This cleanup adds no new global-search exposure.
- **Destructive actions**: No destructive action is introduced or changed. Existing tenant and baseline profile destructive flows remain on their current confirmation and authorization paths.
- **Asset strategy**: No new assets are planned. Deployment expectations remain unchanged, including `cd apps/platform && php artisan filament:assets` only when future work adds registered assets.
- **Testing plan**: Prove the cleanup with focused feature tests for tenant-truth continuity and baseline-profile list/view/edit/archive continuity, plus unit coverage for central badge-catalog cleanup.
## UI / Surface Guardrail Plan
- **Guardrail scope**: no operator-facing surface change
- **Native vs custom classification summary**: `N/A`
- **Shared-family relevance**: none
- **State layers in scope**: none
- **Handling modes by drift class or surface**: `report-only`
- **Repository-signal treatment**: `review-mandatory` because this is a repo-hygiene cleanup that removes active residue rather than hiding it
- **Special surface test profiles**: `N/A`
- **Required tests or manual smoke**: `functional-core`
- **Exception path and spread control**: none
- **Active feature PR close-out entry**: `Guardrail`
## Shared Pattern & System Fit
- **Cross-cutting feature marker**: no
- **Systems touched**: centralized badge registry, baseline profile status model language, tenant default fixtures, browser smoke fixture command, and focused tenant/baseline regression files
- **Shared abstractions reused**: `BaselineProfileStatus`, `BadgeCatalog`, `BadgeDomain`, and the existing tenant/baseline regression families
- **New abstraction introduced? why?**: none
- **Why the existing abstraction was sufficient or insufficient**: Existing abstractions are sufficient because this work removes dead support paths instead of creating a new semantic or presentation layer.
- **Bounded deviation / spread control**: none
## Constitution Check
*GATE: Passed before Phase 0 research. Re-check after Phase 1 design: still passed with no new persistence, no new semantic family, and no operator-surface expansion.*
| Gate | Status | Plan Notes |
|------|--------|------------|
| Inventory-first / read-write separation | PASS | No Graph path, no new mutation flow, and no snapshot or restore behavior change. |
| RBAC, workspace isolation, tenant isolation | PASS | No route, policy, capability, or resource visibility behavior changes are planned. |
| Run observability / Ops-UX lifecycle | PASS | No `OperationRun` is created or modified; this cleanup is outside queued or remote execution semantics. |
| Shared pattern first | PASS | The plan simplifies the central badge path by removing an unused legacy domain rather than bypassing it with a local mapping. |
| Proportionality / no premature abstraction | PASS | The plan removes existing residue and introduces no new abstraction, enum, registry, or persistence. |
| Persisted truth / behavioral state | PASS | Historical columns and migrations remain untouched; no new state or artifact is introduced. |
| Badge semantics / Filament-native discipline | PASS | Central badge semantics remain authoritative, and no page-local or view-local replacement badge language is added. |
| Filament v5 / Livewire v4 contract | PASS | Existing resources remain unchanged in behavior; provider registration and global-search posture stay compliant. |
| Test governance | PASS | Proof stays in focused feature/unit lanes with no heavy-family promotion and a net reduction in fixture-default ambiguity. |
## Test Governance Check
- **Test purpose / classification by changed surface**: `Feature` for tenant-truth and baseline-profile continuity; `Unit` for central badge-catalog cleanup
- **Affected validation lanes**: `fast-feedback`, `confidence`
- **Why this lane mix is the narrowest sufficient proof**: The business truth is continuity after residue removal. Feature tests prove runtime behavior on current tenant and baseline flows, while one small unit slice proves the central badge cleanup without widening into browser or heavy-governance coverage.
- **Narrowest proving command(s)**:
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent`
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Filament/TenantTruthCleanupSpec179Test.php tests/Feature/Filament/TenantLifecycleStatusDomainSeparationTest.php`
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Baselines/BaselineProfileArchiveActionTest.php tests/Feature/Filament/BaselineProfileListFiltersTest.php tests/Feature/Filament/BaselineProfileScopeV2PersistenceTest.php tests/Feature/Baselines/BaselineProfileWorkspaceOwnershipTest.php tests/Feature/Baselines/BaselineProfileAuthorizationTest.php`
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Unit/Badges/TenantBadgesTest.php tests/Unit/Badges/BadgeCatalogTest.php`
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd /Users/ahmeddarrazi/Documents/projects/wt-plattform && rg -n "BaselineProfile::STATUS_|TenantAppStatus|tenant_app_status" apps/platform/app apps/platform/tests apps/platform/database/factories && rg -n -- "app_status" apps/platform/app apps/platform/tests apps/platform/database/factories`
- **Fixture / helper / factory / seed / context cost risks**: Low to moderate. Removing the default `app_status` from `TenantFactory` and browser fixture setup can expose hidden reliance on ambient legacy values in tests or smoke commands.
- **Expensive defaults or shared helper growth introduced?**: No. The change reduces a misleading default rather than adding a new helper burden.
- **Heavy-family additions, promotions, or visibility changes**: none
- **Surface-class relief / special coverage rule**: `standard-native relief`
- **Closing validation and reviewer handoff**: Reviewers should verify that removed residue had no active runtime dependency, that tenant/baseline tests still pass without ambient legacy defaults, and that no migration or new compatibility path slipped in.
- **Budget / baseline / trend follow-up**: none
- **Review-stop questions**: Did any test or fixture still need `app_status` but fail to set it explicitly? Did badge cleanup remove a still-used domain? Did alias removal trigger any runtime or static reference outside the planned scope? Did the cleanup expand into schema or route changes?
- **Escalation path**: `document-in-feature`
- **Active feature PR close-out entry**: `Guardrail`
- **Why no dedicated follow-up spec is needed**: This is routine current-release cleanup. A follow-up spec is only needed if a hidden runtime dependency forces a broader domain decision rather than simple residue removal.
## Project Structure
### Documentation (this feature)
```text
specs/234-dead-transitional-residue/
├── spec.md
├── plan.md
├── research.md
├── data-model.md
├── quickstart.md
├── checklists/
│ └── requirements.md
└── tasks.md
```
No contracts artifact is planned because this cleanup changes no route, API, or standalone logical interaction contract.
### Source Code (repository root)
```text
apps/platform/
├── app/
│ ├── Console/Commands/
│ │ └── SeedBackupHealthBrowserFixture.php
│ ├── Models/
│ │ └── BaselineProfile.php
│ └── Support/Badges/
│ ├── BadgeCatalog.php
│ ├── BadgeDomain.php
│ └── Domains/
│ └── TenantAppStatusBadge.php
├── database/
│ └── factories/
│ └── TenantFactory.php
└── tests/
├── Feature/
│ ├── Baselines/
│ │ ├── BaselineProfileArchiveActionTest.php
│ │ ├── BaselineProfileAuthorizationTest.php
│ │ └── BaselineProfileWorkspaceOwnershipTest.php
│ └── Filament/
│ ├── BaselineProfileListFiltersTest.php
│ ├── BaselineProfileScopeV2PersistenceTest.php
│ ├── TenantLifecycleStatusDomainSeparationTest.php
│ └── TenantTruthCleanupSpec179Test.php
└── Unit/
└── Badges/
├── BadgeCatalogTest.php
└── TenantBadgesTest.php
```
**Structure Decision**: Keep the work entirely inside the existing Laravel application in `apps/platform`. The plan updates one model, one central badge path, one default tenant factory, one browser fixture command, and focused regression files rather than touching resource layout or introducing a cleanup subsystem.
## Complexity Tracking
No constitutional violation is planned. No new structure is introduced, so no complexity exception is required.
| Violation | Why Needed | Simpler Alternative Rejected Because |
|-----------|------------|-------------------------------------|
| — | — | — |
## Proportionality Review
> No new enum, persisted entity, abstraction layer, taxonomy, or cross-domain UI framework is planned in this slice.
- **Current operator problem**: Dead alias and legacy-support residue still make it easy for contributors and tests to treat retired semantics as current repo truth.
- **Existing structure is insufficient because**: The current code already has the canonical `BaselineProfileStatus` path and current tenant-truth behavior, but dead support artifacts continue to conserve the older semantics around them.
- **Narrowest correct implementation**: Remove only the dead alias/support paths and make any still-needed legacy values explicit in the few tests or fixtures that truly need them.
- **Ownership cost created**: Small one-time cleanup across a handful of files, followed by lower ongoing cognitive and maintenance cost.
- **Alternative intentionally rejected**: Keeping deprecated aliases, badge domains, or factory defaults for convenience was rejected because this is a pre-production repo and the residue already undermines cleanup discipline.
- **Release truth**: Current-release truth cleanup.
## Phase 0 Research Summary
- `BaselineProfile::STATUS_DRAFT`, `STATUS_ACTIVE`, and `STATUS_ARCHIVED` exist only in `apps/platform/app/Models/BaselineProfile.php`; no current `apps/platform` runtime or test reference still needs them.
- The tenant app-status badge path is now pure residue: `BadgeDomain::TenantAppStatus`, the `BadgeCatalog` mapper entry, and `TenantAppStatusBadge` remain, but the only confirmed consumers are badge tests, not current runtime surfaces.
- `TenantFactory` still defaults `app_status` to `ok`, and `SeedBackupHealthBrowserFixture` still writes `app_status => 'ok'`, which keeps a retired value ambient in test and smoke data even though current tenant surfaces no longer depend on it.
- Existing tenant-truth regressions intentionally set `app_status` in a few scenarios to prove suppression. Those explicit setups should remain where meaningful; only ambient defaults should go away.
- Historical migrations and historical stored columns are not part of this cleanup. The correct scope is runtime/support residue removal first, not schema deletion.
## Phase 1 Design Summary
- `research.md` records the cleanup decisions and rejected alternatives.
- `data-model.md` documents the still-active persistent truths and the support artifacts that should stop acting as active repo truth.
- `quickstart.md` gives the narrow validation order for alias removal, badge cleanup, fixture cleanup, and regression verification.
- No contracts artifact is created because the feature changes no route, API, or new user interaction contract.
## Phase 1 — Agent Context Update
Run after artifact generation:
- `.specify/scripts/bash/update-agent-context.sh copilot`
## Implementation Strategy
### Phase A — Remove dead baseline profile alias language
**Goal**: Make `BaselineProfileStatus` the only active baseline profile status contract.
| Step | File | Change |
|------|------|--------|
| A.1 | `apps/platform/app/Models/BaselineProfile.php` | Remove deprecated `STATUS_DRAFT`, `STATUS_ACTIVE`, and `STATUS_ARCHIVED` constants. |
| A.2 | Targeted grep + baseline regression files | Confirm no runtime or test path in `apps/platform` still references the removed aliases; keep baseline profile behavior proved through existing feature tests rather than adding a new alias-specific shim. |
### Phase B — Retire the dead tenant app-status badge path centrally
**Goal**: Remove the last active runtime support entry point for tenant app-status semantics.
| Step | File | Change |
|------|------|--------|
| B.1 | `apps/platform/app/Support/Badges/BadgeDomain.php` | Remove the `TenantAppStatus` enum case if no active runtime consumer remains. |
| B.2 | `apps/platform/app/Support/Badges/BadgeCatalog.php` | Remove the `TenantAppStatus` mapper registration. |
| B.3 | `apps/platform/app/Support/Badges/Domains/TenantAppStatusBadge.php` | Remove the mapper class once the registry path is gone. |
| B.4 | `apps/platform/tests/Unit/Badges/TenantBadgesTest.php` and `apps/platform/tests/Unit/Badges/BadgeCatalogTest.php` | Update badge coverage to prove the canonical tenant lifecycle/RBAC/permission domains still work and that the removed legacy domain no longer acts as active repo truth. |
### Phase C — Make legacy app-status explicit instead of ambient in defaults and smoke data
**Goal**: Stop silently injecting retired tenant app-status semantics into factories and browser fixture setup.
| Step | File | Change |
|------|------|--------|
| C.1 | `apps/platform/database/factories/TenantFactory.php` | Remove the default `app_status => 'ok'` assignment so tests must opt in explicitly when they need a historical value. |
| C.2 | `apps/platform/app/Console/Commands/SeedBackupHealthBrowserFixture.php` | Remove or conditionalize the forced `app_status => 'ok'` write unless the scenario explicitly requires it for a still-active smoke purpose. |
| C.3 | Targeted tenant-truth tests | Keep explicit `app_status` setup only in cases that intentionally prove the legacy field no longer surfaces as truth. |
### Phase D — Rebalance regression coverage around explicit legacy setup
**Goal**: Preserve current behavior while making the cleanup durable.
| Step | File | Change |
|------|------|--------|
| D.1 | `apps/platform/tests/Feature/Filament/TenantTruthCleanupSpec179Test.php` | Keep explicit legacy `app_status` values where they prove suppression; stop depending on factory defaults. |
| D.2 | `apps/platform/tests/Feature/Filament/TenantLifecycleStatusDomainSeparationTest.php` | Keep lifecycle/RBAC separation assertions intact with explicit historical setup where needed. |
| D.3 | `apps/platform/tests/Feature/Baselines/BaselineProfileArchiveActionTest.php`, `apps/platform/tests/Feature/Filament/BaselineProfileListFiltersTest.php`, `apps/platform/tests/Feature/Filament/BaselineProfileScopeV2PersistenceTest.php`, `apps/platform/tests/Feature/Baselines/BaselineProfileWorkspaceOwnershipTest.php`, and `apps/platform/tests/Feature/Baselines/BaselineProfileAuthorizationTest.php` | Use these existing baseline profile tests as continuity proof after alias removal across archive, list/filter, view/edit, and workspace-owned behavior. |
## Risks and Mitigations
- **Hidden dependency on removed badge domain**: A helper or test outside the initial grep scope may still call `BadgeDomain::TenantAppStatus`. Mitigation: targeted grep before merge plus running `TenantBadgesTest` and `BadgeCatalogTest` after central removal.
- **Ambient fixture reliance on `app_status`**: Removing the factory default can reveal tests or smoke commands that only passed because `app_status` was silently set to `ok`. Mitigation: convert those cases to explicit setup rather than restoring the default.
- **Baseline alias removal reaches farther than expected**: A non-obvious reference could still exist outside the model. Mitigation: grep for `BaselineProfile::STATUS_` before merge and rely on existing baseline feature tests for continuity.
- **Cleanup scope drifts into schema deletion**: The presence of migrations and stored columns can tempt a larger cut. Mitigation: keep historical schema/migrations explicitly out of scope in both plan and tasks.
## Post-Design Re-check
The plan remains constitution-compliant, Livewire v4 / Filament v5 compliant, and appropriately narrow. It removes dead runtime/support residue, preserves existing tenant and baseline behavior, introduces no new persistence or abstraction, and is ready for `/speckit.tasks`.
## Implementation Close-Out
- **Residue search result**: Clean for active runtime/support paths. Remaining `tenant_app_status` and `BaselineProfile::class.'::STATUS_*'` matches are negative regression assertions only.
- **Remaining `app_status` usage**: Explicit historical setup and suppression assertions in tenant-truth tests only. `TenantFactory` and the backup-health browser fixture no longer write ambient `app_status` defaults.
- **Follow-up decision**: No hidden runtime dependency was found, so no follow-up cleanup spec is needed for this slice.

View File

@ -0,0 +1,80 @@
# Quickstart: Dead Transitional Residue Cleanup
## Goal
Validate that dead baseline profile alias language and dead tenant app-status support residue are removed without changing current tenant-truth or baseline-profile behavior.
## Prerequisites
1. Start Sail for `apps/platform`.
2. Ensure the current branch is `234-dead-transitional-residue`.
3. Be ready to update tests that intentionally use historical `app_status` values so they set those values explicitly.
## Implementation Validation Order
### 1. Format touched files
```bash
export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent
```
Expected outcome:
- Touched PHP and test files follow project formatting rules.
### 2. Run tenant-truth continuity coverage
```bash
export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Filament/TenantTruthCleanupSpec179Test.php tests/Feature/Filament/TenantLifecycleStatusDomainSeparationTest.php
```
Expected outcome:
- Tenant lifecycle and RBAC truth remain unchanged.
- Any legacy `app_status` used in those tests is explicit scenario setup, not a hidden factory default.
### 3. Run baseline-profile continuity coverage
```bash
export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Baselines/BaselineProfileArchiveActionTest.php tests/Feature/Filament/BaselineProfileListFiltersTest.php tests/Feature/Filament/BaselineProfileScopeV2PersistenceTest.php tests/Feature/Baselines/BaselineProfileWorkspaceOwnershipTest.php tests/Feature/Baselines/BaselineProfileAuthorizationTest.php
```
Expected outcome:
- Baseline profile archive, list, view, and edit behavior still work after removing deprecated status aliases.
### 4. Run central badge cleanup coverage
```bash
export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Unit/Badges/TenantBadgesTest.php tests/Unit/Badges/BadgeCatalogTest.php
```
Expected outcome:
- The central badge catalog still resolves active tenant badge domains correctly.
- The removed tenant app-status badge path no longer acts as active runtime truth.
### 5. Run a focused residue grep before merge
```bash
export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd /Users/ahmeddarrazi/Documents/projects/wt-plattform && rg -n "BaselineProfile::STATUS_|TenantAppStatus|tenant_app_status" apps/platform/app apps/platform/tests apps/platform/database/factories
export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd /Users/ahmeddarrazi/Documents/projects/wt-plattform && rg -n -- "app_status" apps/platform/app apps/platform/tests apps/platform/database/factories
```
Expected outcome:
- No unexpected alias or badge-domain references remain.
- Any remaining `app_status` matches are deliberate explicit historical setup or reviewed historical artifacts, not ambient defaults or active truth.
Implementation close-out:
- Active runtime/support paths are clean.
- Remaining `tenant_app_status` and `BaselineProfile::class.'::STATUS_*'` matches are negative regression assertions.
- Remaining `app_status` matches are explicit tenant-truth setup or suppression assertions; no follow-up spec is needed.
## Optional Manual Smoke
1. Open `/admin/tenants` and verify current tenant truth still behaves as before.
2. Open `/admin/baseline-profiles`, then a baseline profile view page and edit page, and verify list, view, edit, and archive behavior still read normally.
3. If the backup-health browser fixture command is still used locally, run it once and confirm it no longer depends on ambient `app_status` defaults.
## Non-Goals For This Slice
- No database migration.
- No route or global-search change.
- No new readiness or badge framework.
- No onboarding or provider-connection cleanup outside the approved dead-residue scope.

View File

@ -0,0 +1,41 @@
# Phase 0 Research: Dead Transitional Residue Cleanup
## Decision: Remove the deprecated `BaselineProfile::STATUS_*` aliases entirely
**Rationale**: The only confirmed definitions of `BaselineProfile::STATUS_DRAFT`, `STATUS_ACTIVE`, and `STATUS_ARCHIVED` are in `apps/platform/app/Models/BaselineProfile.php`. No current `apps/platform` runtime or test reference still depends on those aliases. The canonical contract is already the `BaselineProfileStatus` enum cast, so keeping the constants adds dead language without serving current behavior.
**Alternatives considered**:
- Keep the aliases but leave them deprecated: rejected because they no longer protect any active caller and continue to advertise parallel truth.
- Replace them with forwarding helpers: rejected because that would add new residue to preserve dead semantics.
## Decision: Remove the tenant app-status badge domain from the central badge path
**Rationale**: The remaining runtime path for tenant app-status semantics is the central badge registry: `BadgeDomain::TenantAppStatus`, the `BadgeCatalog` mapper entry, and `TenantAppStatusBadge`. Current confirmed consumers are badge tests, not active tenant surfaces. Once the legacy badge domain has no runtime consumer, removing it centrally is cleaner than keeping it as diagnostic folklore.
**Alternatives considered**:
- Keep the badge domain as a dormant diagnostic mapping: rejected because no current runtime surface needs it and dormant central mappings make it easier to reintroduce dead semantics accidentally.
- Move the mapping into a test helper: rejected because test-only preservation would still keep the dead semantics alive as sanctioned language.
## Decision: Remove ambient `app_status` defaults from test and smoke setup
**Rationale**: `TenantFactory` still defaults `app_status` to `ok`, and `SeedBackupHealthBrowserFixture` still writes `app_status => 'ok'`. That keeps a retired value ambient in new tenant records and smoke data even though current tenant surfaces no longer depend on it. The safer contract is explicit legacy setup only where a test or fixture intentionally proves suppression.
**Alternatives considered**:
- Keep the default for convenience: rejected because convenience is exactly how dead semantics keep surviving.
- Remove `app_status` from every explicit test and fixture immediately: rejected because a few tests intentionally set historical values to prove they no longer surface as truth.
## Decision: Keep historical schema and stored fields out of scope
**Rationale**: The repo still contains historical migrations and the stored `tenants.app_status` column. This cleanup is about active runtime/support residue, not schema deletion. Removing columns or historical migrations would widen the slice beyond the approved cleanup boundary.
**Alternatives considered**:
- Drop the column now: rejected because the spec explicitly forbids schema work in this slice.
- Add a migration shim or deprecation wrapper: rejected because this is pre-production cleanup, not a compatibility exercise.
## Decision: Reuse existing tenant-truth and baseline-profile regressions instead of creating a new cleanup harness
**Rationale**: The current proof burden is continuity after residue removal. Existing tenant-truth feature tests and baseline-profile feature tests already exercise the active behavior we need to protect. A small badge-catalog unit slice is enough for the central registry cleanup. A new meta guard framework would add more long-term burden than value.
**Alternatives considered**:
- Add grep-driven guard tests for every removed symbol: rejected because behavior-facing tests are the primary proof and repo grep is sufficient as a review aid.
- Rely on manual inspection only: rejected because cleanup regressions are easy to reintroduce silently.

View File

@ -0,0 +1,223 @@
# Feature Specification: Dead Transitional Residue Cleanup
**Feature Branch**: `234-dead-transitional-residue`
**Created**: 2026-04-23
**Status**: Draft
**Input**: User description: "Dead Transitional Residue Cleanup"
## Spec Candidate Check *(mandatory — SPEC-GATE-001)*
- **Problem**: The repo still carries dead transitional residue around tenant truth and baseline profile status language. Deprecated baseline profile status aliases and retired tenant app-status support artifacts still survive in support code, fixtures, and tests even though the current product no longer treats them as active truth.
- **Today's failure**: Contributors and regressions can still conserve or reintroduce dead semantics because the repo keeps them available as if they were valid current-release language. That weakens earlier tenant-truth cleanup and makes follow-up cleanup strands harder to land cleanly.
- **User-visible improvement**: Existing tenant and baseline profile surfaces keep the same current truth, but retired app-status and deprecated status-alias semantics stop leaking back through defaults, badges, fixtures, and tests.
- **Smallest enterprise-capable version**: Remove dead baseline profile status aliases and tenant app-status residue from active runtime support code, factories, seeds, fixtures, and tests after verifying no productive dependency still exists. Do not redesign tenant readiness, baseline semantics, or storage.
- **Explicit non-goals**: No new readiness model, no new status family, no schema redesign, no provider-connection cleanup beyond the dead tenant app-status residue, no onboarding fallback cleanup, and no canonical operation-type convergence work.
- **Permanent complexity imported**: None. The feature reduces permanent complexity by removing dead symbols, dead badge semantics, and fixture conservatism while keeping focused regression coverage.
- **Why now**: This is the first step in the active repository cleanup strand. Leaving dead residue in place makes the next cleanup slices and source-of-truth work riskier because they must keep fighting old semantics that should already be gone.
- **Why not local**: Deleting only one constant or one test would leave the same dead semantics alive in other seams such as badge registration, factories, browser fixtures, or seed data. The problem is distributed residue, not one stray reference.
- **Approval class**: Cleanup
- **Red flags triggered**: One mild red flag: the cleanup spans model, badge, fixture, seed, and test seams. Defense: those seams all conserve the same retired semantics, so one bounded cleanup spec is smaller and safer than several micro-specs.
- **Score**: Nutzen: 2 | Dringlichkeit: 2 | Scope: 2 | Komplexität: 2 | Produktnähe: 1 | Wiederverwendung: 2 | **Gesamt: 11/12**
- **Decision**: approve
## Spec Scope Fields *(mandatory)*
- **Scope**: workspace + tenant
- **Primary Routes**:
- `/admin/tenants`
- `/admin/tenants/{tenant}`
- `/admin/baseline-profiles`
- `/admin/baseline-profiles/{profile}`
- `/admin/baseline-profiles/{profile}/edit`
- **Data Ownership**:
- `tenants` remain the tenant-owned source of lifecycle, provider, and RBAC truth. This feature does not add, remove, or reinterpret tenant lifecycle semantics.
- `baseline_profiles` remain the workspace-owned source of baseline profile truth. This feature does not change profile lifecycle behavior; it removes deprecated alias language around that existing truth.
- No new persisted truth, mirror field, or cleanup ledger is introduced.
- **RBAC**:
- Workspace membership remains required for the affected admin resources.
- Tenant isolation and current capability checks remain unchanged.
- This feature does not broaden visibility, alter 404 versus 403 semantics, or add new authorization paths.
## Cross-Cutting / Shared Pattern Reuse *(mandatory when the feature touches notifications, status messaging, action links, header actions, dashboard signals/cards, alerts, navigation entry points, evidence/report viewers, or any other existing shared operator interaction family; otherwise write `N/A - no shared interaction family touched`)*
N/A - no shared interaction family touched.
## UI / Surface Guardrail Impact *(mandatory when operator-facing surfaces are changed; otherwise write `N/A`)*
N/A - no operator-facing surface change. Existing tenant and baseline profile surfaces must keep their current behavior while dead supporting residue is removed.
## 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**: Dead residue keeps retired semantics available, which makes it easier for tests, fixtures, and future changes to treat them as current truth again.
- **Existing structure is insufficient because**: The existing structure is the problem; it still contains aliases and support artifacts that no longer represent active product language.
- **Narrowest correct implementation**: Remove only the dead residue that has no active runtime contract and update the focused regressions that still conserve it.
- **Ownership cost**: One bounded cleanup pass across affected support code, fixtures, and tests, followed by lower long-term cognitive and maintenance cost.
- **Alternative intentionally rejected**: Leaving deprecated aliases and legacy support artifacts in place "just in case" was rejected because this is a pre-production repo and the residue already causes semantic drift.
- **Release truth**: Current-release truth cleanup.
### Compatibility posture
This feature assumes a pre-production environment.
Backward compatibility, legacy aliases, migration shims, historical fixtures, and compatibility-specific tests are out of scope unless explicitly required by this spec.
Canonical replacement is preferred over preservation.
## Testing / Lane / Runtime Impact *(mandatory for runtime behavior changes)*
- **Test purpose / classification**: Feature
- **Validation lane(s)**: fast-feedback, confidence
- **Why this classification and these lanes are sufficient**: The proof burden is that existing tenant and baseline profile behaviors still work after dead residue is removed, and that dead semantics no longer survive in active support paths. Focused feature coverage with one small badge-regression slice is sufficient.
- **New or expanded test families**: Update the existing tenant-truth cleanup regressions, tenant lifecycle domain-separation regressions, baseline profile behavior regressions, and badge catalog regressions. Add a narrow baseline-status cleanup regression only if an existing file cannot express the assertion cleanly.
- **Fixture / helper cost impact**: Lower overall. Factories, seeds, and browser fixtures should stop carrying dead app-status defaults unless a still-active boundary proves they are needed.
- **Heavy-family visibility / justification**: none
- **Special surface test profile**: standard-native-filament
- **Standard-native relief or required special coverage**: Ordinary feature coverage is sufficient. The cleanup must also prove that central badge registration and default fixtures do not conserve retired semantics.
- **Reviewer handoff**: Reviewers must confirm that no active runtime dependency still needs the removed residue, that tenant and baseline profile behavior remains unchanged for current truth, and that dead semantics are removed rather than rewrapped in a compatibility shim.
- **Budget / baseline / trend impact**: none
- **Escalation needed**: none
- **Active feature PR close-out entry**: Guardrail
- **Planned validation commands**:
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Filament/TenantTruthCleanupSpec179Test.php tests/Feature/Filament/TenantLifecycleStatusDomainSeparationTest.php`
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Baselines/BaselineProfileArchiveActionTest.php tests/Feature/Filament/BaselineProfileListFiltersTest.php`
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Unit/Badges/TenantBadgesTest.php tests/Unit/Badges/BadgeCatalogTest.php`
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent`
## User Scenarios & Testing *(mandatory)*
### User Story 1 - Keep tenant truth free of retired app-status semantics (Priority: P1)
As an operator, I can continue to use tenant surfaces without retired app-status semantics resurfacing through defaults, badges, or seeded examples, so the current lifecycle, provider, and RBAC truth stays trustworthy.
**Why this priority**: Tenant truth cleanup already happened on primary surfaces. The highest-value part of this cleanup is making sure dead supporting residue cannot silently undo that work.
**Independent Test**: Can be fully tested by exercising the existing tenant list and tenant detail regressions with records that still contain legacy app-status values and proving that current tenant truth stays unchanged.
**Acceptance Scenarios**:
1. **Given** a tenant record still stores a legacy app-status value, **When** the operator opens the existing tenant list or tenant detail surface, **Then** that legacy value does not regain current-status meaning.
2. **Given** seeded or factory-created tenant examples are used in current tenant-truth regressions, **When** those regressions run after cleanup, **Then** they no longer depend on app-status defaults to make the surfaces work.
3. **Given** lifecycle, provider, and RBAC truth already coexist on a tenant surface, **When** the cleanup is complete, **Then** those active truths remain separate and unchanged.
---
### User Story 2 - Use one baseline profile status language (Priority: P1)
As a maintainer, I can reason about baseline profile state through one canonical status contract, so draft, active, and archived behavior is not split between live status truth and deprecated aliases.
**Why this priority**: The deprecated baseline profile aliases are explicitly dead residue. Removing them is the cleanest proof that the repo now has one active baseline profile status language.
**Independent Test**: Can be fully tested by running existing baseline profile archive, list/filter, and view/edit continuity regressions after the deprecated alias language is removed and confirming that current baseline profile behavior stays intact.
**Acceptance Scenarios**:
1. **Given** baseline profiles still move through draft, active, and archived behavior today, **When** existing baseline profile regressions run after cleanup, **Then** the behavior still works without deprecated status aliases.
2. **Given** a contributor updates baseline profile logic or tests, **When** they read current profile status semantics, **Then** only the canonical status contract is available as active language.
3. **Given** an operator opens or saves an existing baseline profile through the current view and edit surfaces, **When** the cleanup is complete, **Then** those surfaces continue to render and persist through the canonical status contract without depending on deprecated aliases.
---
### Cross-Cutting Verification - Prove the residue is fully retired (Release Gate)
As a reviewer, I can verify the cleanup in one focused pass, so the repo does not keep half-dead semantics alive in support code, fixtures, or tests.
**Why this priority**: Cleanup value is only real if the dead semantics are actually gone rather than merely hidden in one layer.
**Release Gate**: This verification runs after User Story 1 and User Story 2 are complete and confirms that the touched runtime and test paths no longer expose the retired semantics as active language.
**Note**: This is not an independently shippable MVP slice; it is the feature-level closeout check that proves the cleanup is complete.
**Acceptance Scenarios**:
1. **Given** the cleanup branch, **When** the reviewer runs the focused validation commands, **Then** current tenant and baseline profile behaviors still pass without the retired residue.
2. **Given** a touched support path, fixture, or test previously conserved dead semantics, **When** the cleanup is reviewed, **Then** that path is either removed, rewritten to current truth, or explicitly deferred as a follow-up rather than silently preserved.
### Edge Cases
- A legacy storage field may still exist historically or in migrations even when it no longer has any active runtime meaning.
- A browser fixture or seeder may still populate a retired value for historical realism; this spec must remove mandatory dependency on that value without changing unrelated fixture intent.
- Some tests may use literal status values rather than deprecated aliases; the cleanup must distinguish current canonical value usage from dead alias usage.
- Central badge registration may still contain dormant legacy entries even when no current surface consumes them; dormant entries count as cleanup scope if they no longer support active truth.
- Baseline profile archive, list, view, and edit behavior must continue to work because the active status contract already exists independently of the deprecated aliases.
## Requirements *(mandatory)*
**Constitution alignment (required):** This feature adds no Microsoft Graph calls, no long-running work, and no new write workflow. It is a bounded runtime and test cleanup over existing tenant and baseline profile truth.
**Constitution alignment (PROP-001 / ABSTR-001 / PERSIST-001 / STATE-001 / BLOAT-001):** This feature introduces no new persistence, abstraction, state family, or semantic layer. It removes dead structure that no longer represents current-release truth.
**Constitution alignment (XCUT-001):** Not applicable. The feature does not add or modify a shared operator interaction family.
**Constitution alignment (TEST-GOV-001):** Focused feature and badge-regression coverage is the narrowest sufficient proof because the business risk is dead semantics surviving in active support paths, not a new workflow or surface family.
**Constitution alignment (RBAC-UX):** No authorization behavior changes. Existing workspace membership, tenant isolation, and capability enforcement remain authoritative.
**Constitution alignment (BADGE-001):** Centralized badge semantics remain authoritative. If the retired tenant app-status badge domain has no active consumer, it must be removed centrally rather than replaced with any page-local or test-local mapping.
**Constitution alignment (UI-FIL-001):** No new Filament screen, action surface, or custom markup is introduced. Existing tenant and baseline profile surfaces must continue to rely on their current native presentation.
**Constitution alignment (UI-SEM-001 / LAYER-001 / TEST-TRUTH-001):** The feature removes dead interpretation residue rather than adding another semantic layer. Tests must prove current business truth survives while the residue disappears.
### Functional Requirements
- **FR-234-001**: The system MUST remove deprecated baseline profile status aliases from active runtime language once the cleanup proves no productive dependency remains.
- **FR-234-002**: The system MUST treat the canonical baseline profile status contract as the only active source of draft, active, and archived profile semantics.
- **FR-234-003**: The system MUST remove retired tenant app-status support residue from active badge registration, factory defaults, browser fixtures, seeds, and tests when those paths no longer serve a current runtime contract.
- **FR-234-004**: Existing tenant list and tenant detail behavior MUST remain unchanged for current lifecycle, provider, and RBAC truth after the retired residue is removed.
- **FR-234-005**: Existing baseline profile list, view, edit, and archive behavior MUST remain unchanged for current profile status truth after deprecated aliases are removed.
- **FR-234-006**: Every removed residue item MUST be checked for hidden runtime, UI, filter, cast, policy, or API dependency before deletion.
- **FR-234-007**: If a hidden dependency is found, the dependency MUST be documented and moved to a follow-up cleanup decision rather than preserving the residue as silent compatibility lore.
- **FR-234-008**: The feature MUST NOT introduce compatibility aliases, fallback readers, migration shims, or new legacy fixtures to preserve removed residue.
- **FR-234-009**: The feature MUST NOT introduce a new readiness model, new status family, new cleanup ledger, or any other replacement semantic layer.
- **FR-234-010**: Historical storage or migration remnants MAY remain only as historical artifacts and MUST NOT regain default-visible operator meaning.
- **FR-234-011**: Focused regression coverage MUST prove both tenant-truth continuity and baseline-profile continuity after the cleanup.
- **FR-234-012**: The feature MUST stay bounded to dead transitional residue cleanup and MUST NOT absorb onboarding fallback retirement, provider-connection legacy cleanup, or canonical operation-type convergence.
### Key Entities *(include if feature involves data)*
- **Canonical baseline profile status**: The active status language that already governs baseline profile lifecycle behavior.
- **Deprecated baseline profile status aliases**: Retired alias constants that mirror current profile statuses but no longer represent active repo truth.
- **Tenant app-status residue**: Retired support artifacts around a legacy tenant-level status signal that current tenant surfaces no longer treat as authoritative.
- **Residual support artifacts**: Factories, fixtures, seeds, tests, and badge registrations that can conserve dead semantics even when primary product surfaces no longer use them.
## Success Criteria *(mandatory)*
### Measurable Outcomes
- **SC-234-001**: In acceptance review, no targeted tenant surface or supporting default path reintroduces tenant app-status as current truth.
- **SC-234-002**: 100% of targeted tenant-truth regressions pass after the retired tenant app-status residue is removed.
- **SC-234-003**: 100% of targeted baseline profile regressions pass after the deprecated baseline profile status aliases are removed.
- **SC-234-004**: The cleanup ships without adding any new persistence, status family, compatibility shim, or replacement semantic layer.
## Assumptions
- The current baseline profile status contract is already sufficient for existing profile behavior.
- Existing tenant surfaces no longer require app-status to express current tenant truth.
- Historical migration files may retain old field names or values as history without counting as active runtime truth.
## Non-Goals
- Dropping legacy database columns in this slice
- Redesigning tenant readiness, provider readiness, or baseline readiness semantics
- Performing onboarding fallback retirement
- Performing provider-connection legacy cleanup outside the dead tenant app-status residue
- Resolving the canonical operation-type source-of-truth conflict
## Dependencies
- Existing tenant lifecycle, provider, and RBAC truth separation on current tenant surfaces
- Existing baseline profile behavior and current baseline profile status contract
- Existing focused regressions for tenant truth, baseline profile behavior, and central badge registration
## Definition of Done
- Deprecated baseline profile status aliases are gone from active runtime language.
- Retired tenant app-status residue is gone from active badge registration, default fixtures, seeds, and tests unless an explicit still-active dependency is documented.
- Existing tenant and baseline profile behaviors remain unchanged for current truth.
- No new compatibility path or replacement status layer was introduced.
- Focused regression coverage passes.

View File

@ -0,0 +1,225 @@
# Tasks: Dead Transitional Residue Cleanup
**Input**: Design documents from `/specs/234-dead-transitional-residue/`
**Prerequisites**: `plan.md`, `spec.md`, `research.md`, `data-model.md`, `quickstart.md`
**Tests**: Required. This feature changes runtime behavior by removing active runtime/support residue, so Pest coverage must be added or updated in `apps/platform/tests/Feature/Filament/TenantTruthCleanupSpec179Test.php`, `apps/platform/tests/Feature/Filament/TenantLifecycleStatusDomainSeparationTest.php`, `apps/platform/tests/Feature/Baselines/BaselineProfileArchiveActionTest.php`, `apps/platform/tests/Feature/Filament/BaselineProfileListFiltersTest.php`, `apps/platform/tests/Feature/Filament/BaselineProfileScopeV2PersistenceTest.php`, `apps/platform/tests/Feature/Baselines/BaselineProfileWorkspaceOwnershipTest.php`, `apps/platform/tests/Feature/Baselines/BaselineProfileAuthorizationTest.php`, `apps/platform/tests/Unit/Badges/TenantBadgesTest.php`, and `apps/platform/tests/Unit/Badges/BadgeCatalogTest.php`.
**Operations**: No new `OperationRun`, audit-only DB action, or queued workflow is introduced. This cleanup stays inside existing runtime behavior, fixture defaults, and regression coverage.
**RBAC**: No authorization semantics change. Existing tenant/admin Filament access, tenant isolation, and current `404` versus `403` behavior must remain unchanged in the touched tenant and baseline regression files.
**UI / Surface Guardrails**: No operator-facing surface is added or redesigned. Keep `standard-native-filament` relief and use the existing tenant and baseline pages only as continuity proof.
**Filament UI Action Surfaces**: No new Filament Resource, Page, RelationManager, or action surface is introduced. `TenantResource` and `BaselineProfileResource` keep their current surfaces and global-search posture.
**Badges**: `BadgeCatalog` remains authoritative. The legacy tenant app-status badge domain must be removed centrally from `apps/platform/app/Support/Badges/BadgeCatalog.php` and `apps/platform/app/Support/Badges/BadgeDomain.php`, and active badge domains must remain covered by tests.
**Organization**: Tasks are grouped by user story so each slice stays independently testable after the shared proof surfaces are prepared. Recommended delivery order is `US1` and `US2` in parallel after Foundational, then a final cross-cutting verification phase, because the retirement proof only matters once the tenant and baseline cleanup slices are both in place.
## Test Governance Checklist
- [X] Lane assignment is named and is the narrowest sufficient proof for the changed behavior.
- [X] New or changed tests stay in the smallest honest family, and any heavy-governance or browser addition is explicit.
- [X] Shared helpers, factories, seeds, fixtures, and context defaults stay cheap by default; any widening is isolated or documented.
- [X] Planned validation commands cover the change without pulling in unrelated lane cost.
- [X] The declared surface test profile or `standard-native-filament` relief is explicit.
- [X] Any material budget, baseline, trend, or escalation note is recorded in the active spec or PR.
## Phase 1: Setup (Shared Cleanup Anchors)
**Purpose**: Lock the cleanup inventory and proving commands before editing runtime or test files.
- [X] T001 [P] Verify the cleanup anchor inventory across `apps/platform/app/Models/BaselineProfile.php`, `apps/platform/app/Support/Badges/BadgeCatalog.php`, `apps/platform/app/Support/Badges/BadgeDomain.php`, `apps/platform/app/Support/Badges/Domains/TenantAppStatusBadge.php`, `apps/platform/database/factories/TenantFactory.php`, and `apps/platform/app/Console/Commands/SeedBackupHealthBrowserFixture.php`
- [X] T002 [P] Verify the narrow validation lane and proving commands in `specs/234-dead-transitional-residue/plan.md` and `specs/234-dead-transitional-residue/quickstart.md`
**Checkpoint**: Cleanup scope and proving commands are locked before code changes begin.
---
## Phase 2: Foundational (Blocking Proof Surfaces)
**Purpose**: Audit the real consumer boundaries before removing residue so story work does not rediscover hidden dependencies mid-slice.
**CRITICAL**: No user story work should begin until this phase is complete.
- [X] T003 [P] Audit all `TenantAppStatus` and `TenantAppStatusBadge` consumers across `apps/platform/app/Support/Badges/BadgeCatalog.php`, `apps/platform/app/Support/Badges/BadgeDomain.php`, `apps/platform/app/Support/Badges/Domains/TenantAppStatusBadge.php`, `apps/platform/tests/Unit/Badges/TenantBadgesTest.php`, and `apps/platform/tests/Unit/Badges/BadgeCatalogTest.php`
- [X] T004 [P] Audit every ambient or explicit `app_status` usage boundary across `apps/platform/database/factories/TenantFactory.php`, `apps/platform/app/Console/Commands/SeedBackupHealthBrowserFixture.php`, `apps/platform/tests/Feature/Filament/TenantTruthCleanupSpec179Test.php`, and `apps/platform/tests/Feature/Filament/TenantLifecycleStatusDomainSeparationTest.php`
- [X] T005 [P] Audit every `BaselineProfile::STATUS_` consumer and baseline continuity proof file across `apps/platform/app/Models/BaselineProfile.php`, `apps/platform/tests/Feature/Baselines/BaselineProfileArchiveActionTest.php`, `apps/platform/tests/Feature/Filament/BaselineProfileListFiltersTest.php`, `apps/platform/tests/Feature/Filament/BaselineProfileScopeV2PersistenceTest.php`, `apps/platform/tests/Feature/Baselines/BaselineProfileWorkspaceOwnershipTest.php`, and `apps/platform/tests/Feature/Baselines/BaselineProfileAuthorizationTest.php`
- [X] T006 [P] Lock the cross-cutting retirement proof and follow-up decision sink in `specs/234-dead-transitional-residue/plan.md`, `specs/234-dead-transitional-residue/quickstart.md`, and `specs/234-dead-transitional-residue/tasks.md`
**Checkpoint**: Hidden-dependency boundaries are explicit and the story slices can proceed without overlapping proof setup work.
---
## Phase 3: User Story 1 - Keep Tenant Truth Free Of Retired App-Status Semantics (Priority: P1) 🎯 MVP
**Goal**: Remove tenant app-status residue from active badge/default paths without changing current tenant lifecycle, provider, or RBAC truth.
**Independent Test**: Run the tenant-truth regressions with explicit legacy `app_status` values and verify the tenant list/detail surfaces still suppress them while active tenant truth remains unchanged.
### Tests for User Story 1
- [X] T007 [P] [US1] Add tenant list/detail assertions that explicit historical `app_status` values stay suppressed in `apps/platform/tests/Feature/Filament/TenantTruthCleanupSpec179Test.php`
- [X] T008 [P] [US1] Add lifecycle and RBAC separation assertions that do not rely on `TenantFactory` defaults in `apps/platform/tests/Feature/Filament/TenantLifecycleStatusDomainSeparationTest.php`
- [X] T009 [P] [US1] Add tenant badge assertions that the legacy app-status domain no longer participates in active tenant semantics in `apps/platform/tests/Unit/Badges/TenantBadgesTest.php`
### Implementation for User Story 1
- [X] T010 [P] [US1] Remove the `TenantAppStatus` registration path from `apps/platform/app/Support/Badges/BadgeDomain.php` and `apps/platform/app/Support/Badges/BadgeCatalog.php`
- [X] T011 [US1] Delete the retired mapper in `apps/platform/app/Support/Badges/Domains/TenantAppStatusBadge.php`
- [X] T012 [P] [US1] Remove the ambient `app_status` default from `apps/platform/database/factories/TenantFactory.php`
- [X] T013 [P] [US1] Remove the forced tenant `app_status` fixture value from `apps/platform/app/Console/Commands/SeedBackupHealthBrowserFixture.php`
- [X] T014 [US1] Reconcile explicit legacy setup and active-domain expectations in `apps/platform/tests/Feature/Filament/TenantTruthCleanupSpec179Test.php`, `apps/platform/tests/Feature/Filament/TenantLifecycleStatusDomainSeparationTest.php`, and `apps/platform/tests/Unit/Badges/TenantBadgesTest.php`
**Checkpoint**: User Story 1 is independently functional and tenant truth no longer depends on the retired badge path or ambient `app_status` defaults.
---
## Phase 4: User Story 2 - Use One Baseline Profile Status Language (Priority: P1)
**Goal**: Remove deprecated baseline profile alias language so `BaselineProfileStatus` is the only active lifecycle contract.
**Independent Test**: Run the existing baseline profile archive, list/filter, view/edit continuity, and workspace-ownership regressions after alias removal and verify behavior is unchanged.
### Tests for User Story 2
- [X] T015 [P] [US2] Add archive-flow assertions that only `BaselineProfileStatus` drives lifecycle behavior in `apps/platform/tests/Feature/Baselines/BaselineProfileArchiveActionTest.php`
- [X] T016 [P] [US2] Add list/filter assertions that baseline profile behavior does not require alias constants in `apps/platform/tests/Feature/Filament/BaselineProfileListFiltersTest.php`
- [X] T017 [P] [US2] Add view/edit continuity assertions after alias removal in `apps/platform/tests/Feature/Filament/BaselineProfileScopeV2PersistenceTest.php`
- [X] T018 [P] [US2] Add workspace-ownership continuity assertions after alias removal in `apps/platform/tests/Feature/Baselines/BaselineProfileWorkspaceOwnershipTest.php`
### Implementation for User Story 2
- [X] T019 [US2] Remove deprecated `STATUS_DRAFT`, `STATUS_ACTIVE`, and `STATUS_ARCHIVED` constants from `apps/platform/app/Models/BaselineProfile.php`
- [X] T020 [US2] Update baseline profile regressions to use only `App\Support\Baselines\BaselineProfileStatus` in `apps/platform/tests/Feature/Baselines/BaselineProfileArchiveActionTest.php`, `apps/platform/tests/Feature/Filament/BaselineProfileListFiltersTest.php`, `apps/platform/tests/Feature/Filament/BaselineProfileScopeV2PersistenceTest.php`, and `apps/platform/tests/Feature/Baselines/BaselineProfileWorkspaceOwnershipTest.php`
**Checkpoint**: User Story 2 is independently functional and baseline profile lifecycle behavior now has one canonical status language.
---
## Phase 5: Cross-Cutting Verification - Prove The Residue Is Fully Retired
**Goal**: Lock in regression proof that the retired semantics are gone from active runtime/support paths.
**Release Gate**: Run the focused regression pack plus the residue searches after User Story 1 and User Story 2 are complete, and confirm there are no unexpected matches or hidden-default dependencies left in the touched files.
### Verification for Cross-Cutting Closeout
- [X] T021 [P] Add badge catalog assertions that the retired tenant app-status domain is absent while active domains remain registered in `apps/platform/tests/Unit/Badges/BadgeCatalogTest.php`
- [X] T022 [P] Add regression assertions that legacy `app_status` is always opt-in setup in `apps/platform/tests/Feature/Filament/TenantTruthCleanupSpec179Test.php` and `apps/platform/tests/Feature/Filament/TenantLifecycleStatusDomainSeparationTest.php`
### Implementation for Cross-Cutting Closeout
- [X] T023 Run and review the residue searches for `BaselineProfile::STATUS_|TenantAppStatus|tenant_app_status` and `app_status` across `apps/platform/app/Models/BaselineProfile.php`, `apps/platform/app/Support/Badges/BadgeCatalog.php`, `apps/platform/app/Support/Badges/BadgeDomain.php`, `apps/platform/app/Support/Badges/Domains/TenantAppStatusBadge.php`, `apps/platform/database/factories/TenantFactory.php`, `apps/platform/app/Console/Commands/SeedBackupHealthBrowserFixture.php`, `apps/platform/tests/Feature/Filament/TenantTruthCleanupSpec179Test.php`, `apps/platform/tests/Feature/Filament/TenantLifecycleStatusDomainSeparationTest.php`, `apps/platform/tests/Feature/Baselines/BaselineProfileArchiveActionTest.php`, `apps/platform/tests/Feature/Filament/BaselineProfileListFiltersTest.php`, `apps/platform/tests/Feature/Filament/BaselineProfileScopeV2PersistenceTest.php`, `apps/platform/tests/Feature/Baselines/BaselineProfileWorkspaceOwnershipTest.php`, `apps/platform/tests/Unit/Badges/TenantBadgesTest.php`, and `apps/platform/tests/Unit/Badges/BadgeCatalogTest.php`
- [X] T024 Record any hidden-dependency follow-up or confirm clean retirement in `specs/234-dead-transitional-residue/plan.md` and `specs/234-dead-transitional-residue/quickstart.md`
**Checkpoint**: The feature has explicit proof that the dead residue is no longer part of active truth.
---
## Phase 6: Polish & Cross-Cutting Concerns
**Purpose**: Finish formatting and run the narrow proving workflow for the full cleanup.
- [X] T025 Run formatting for `apps/platform/app/Models/BaselineProfile.php`, `apps/platform/app/Support/Badges/BadgeCatalog.php`, `apps/platform/app/Support/Badges/BadgeDomain.php`, `apps/platform/database/factories/TenantFactory.php`, `apps/platform/app/Console/Commands/SeedBackupHealthBrowserFixture.php`, `apps/platform/tests/Unit/Badges/BadgeCatalogTest.php`, `apps/platform/tests/Unit/Badges/TenantBadgesTest.php`, `apps/platform/tests/Feature/Filament/TenantTruthCleanupSpec179Test.php`, `apps/platform/tests/Feature/Filament/TenantLifecycleStatusDomainSeparationTest.php`, `apps/platform/tests/Feature/Baselines/BaselineProfileArchiveActionTest.php`, `apps/platform/tests/Feature/Filament/BaselineProfileListFiltersTest.php`, `apps/platform/tests/Feature/Filament/BaselineProfileScopeV2PersistenceTest.php`, `apps/platform/tests/Feature/Baselines/BaselineProfileWorkspaceOwnershipTest.php`, and `apps/platform/tests/Feature/Baselines/BaselineProfileAuthorizationTest.php` with `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent`
- [X] T026 [P] Run the tenant-truth validation pack from `specs/234-dead-transitional-residue/quickstart.md` against `apps/platform/tests/Feature/Filament/TenantTruthCleanupSpec179Test.php` and `apps/platform/tests/Feature/Filament/TenantLifecycleStatusDomainSeparationTest.php`
- [X] T027 [P] Run the baseline-profile and badge validation pack from `specs/234-dead-transitional-residue/quickstart.md` against `apps/platform/tests/Feature/Baselines/BaselineProfileArchiveActionTest.php`, `apps/platform/tests/Feature/Filament/BaselineProfileListFiltersTest.php`, `apps/platform/tests/Feature/Filament/BaselineProfileScopeV2PersistenceTest.php`, `apps/platform/tests/Feature/Baselines/BaselineProfileWorkspaceOwnershipTest.php`, `apps/platform/tests/Feature/Baselines/BaselineProfileAuthorizationTest.php`, `apps/platform/tests/Unit/Badges/TenantBadgesTest.php`, and `apps/platform/tests/Unit/Badges/BadgeCatalogTest.php`
---
## Dependencies & Execution Order
### Phase Dependencies
- **Setup (Phase 1)**: Starts immediately and locks the cleanup inventory plus proving commands.
- **Foundational (Phase 2)**: Depends on Setup and blocks all story work until the hidden-dependency boundaries and closeout proof sinks are explicit.
- **User Story 1 (Phase 3)**: Depends on Foundational and is the recommended MVP cut.
- **User Story 2 (Phase 4)**: Depends on Foundational and can proceed in parallel with User Story 1 because it touches a separate runtime truth domain.
- **Cross-Cutting Verification (Phase 5)**: Depends on User Story 1 and User Story 2 because the final retirement proof only makes sense after both cleanup slices land.
- **Polish (Phase 6)**: Depends on all desired user stories and the cross-cutting verification phase being complete.
### User Story Dependencies
- **US1**: No dependencies beyond Foundational.
- **US2**: No dependencies beyond Foundational.
### Within Each User Story
- Write the story tests first and confirm they fail before implementation is considered complete.
- Keep the cleanup canonical: no compatibility aliases, no fallback readers, and no restoration of ambient legacy defaults.
- Keep `BadgeCatalog` authoritative for tenant badge semantics and `BaselineProfileStatus` authoritative for baseline lifecycle semantics.
- Finish story-level verification before moving to the next dependent slice.
### Parallel Opportunities
- `T001` and `T002` can run in parallel during Setup.
- `T003`, `T004`, `T005`, and `T006` can run in parallel during Foundational work.
- `T007`, `T008`, and `T009` can run in parallel for User Story 1, followed by `T010`, `T011`, `T012`, and `T013` in parallel before reconciling tests in `T014`.
- `T015`, `T016`, `T017`, and `T018` can run in parallel for User Story 2.
- User Story 1 and User Story 2 can proceed in parallel after Foundational is complete.
- `T021` and `T022` can run in parallel during cross-cutting verification.
- `T026` and `T027` can run in parallel during final validation.
---
## Parallel Example: User Story 1
```bash
# User Story 1 tests in parallel
T007 apps/platform/tests/Feature/Filament/TenantTruthCleanupSpec179Test.php
T008 apps/platform/tests/Feature/Filament/TenantLifecycleStatusDomainSeparationTest.php
T009 apps/platform/tests/Unit/Badges/TenantBadgesTest.php
# User Story 1 implementation in parallel after the tests are in place
T010 apps/platform/app/Support/Badges/BadgeDomain.php + apps/platform/app/Support/Badges/BadgeCatalog.php
T011 apps/platform/app/Support/Badges/Domains/TenantAppStatusBadge.php
T012 apps/platform/database/factories/TenantFactory.php
T013 apps/platform/app/Console/Commands/SeedBackupHealthBrowserFixture.php
```
## Parallel Example: User Story 2
```bash
# User Story 2 tests in parallel
T015 apps/platform/tests/Feature/Baselines/BaselineProfileArchiveActionTest.php
T016 apps/platform/tests/Feature/Filament/BaselineProfileListFiltersTest.php
T017 apps/platform/tests/Feature/Filament/BaselineProfileScopeV2PersistenceTest.php
T018 apps/platform/tests/Feature/Baselines/BaselineProfileWorkspaceOwnershipTest.php
```
## Parallel Example: Cross-Story Delivery After Foundational
```bash
# Tenant cleanup and baseline cleanup can proceed in parallel after Phase 2
T010-T014 apps/platform/app/Support/Badges/* + apps/platform/database/factories/TenantFactory.php + apps/platform/app/Console/Commands/SeedBackupHealthBrowserFixture.php
T019-T020 apps/platform/app/Models/BaselineProfile.php + apps/platform/tests/Feature/Baselines/* + apps/platform/tests/Feature/Filament/BaselineProfileListFiltersTest.php + apps/platform/tests/Feature/Filament/BaselineProfileScopeV2PersistenceTest.php
```
---
## Implementation Strategy
### MVP First (User Story 1 Only)
1. Complete Phase 1: Setup.
2. Complete Phase 2: Foundational.
3. Complete Phase 3: User Story 1.
4. Run `T025` and `T026` before widening the slice.
### Incremental Delivery
1. Ship User Story 1 to remove tenant app-status residue from active badge/default paths.
2. Ship User Story 2 to collapse baseline lifecycle language onto `BaselineProfileStatus` only.
3. Run the cross-cutting verification phase to lock in proof that the residue is fully retired.
4. Finish with formatting and the focused validation workflow.
### Parallel Team Strategy
1. One contributor can prepare the badge and tenant-truth proof surfaces while another prepares the baseline continuity proof surfaces in Phase 2.
2. After Foundational is complete, one contributor can execute User Story 1 while another executes User Story 2.
3. Once both cleanup slices land, a final pass can focus on cross-cutting retirement proof and the narrow validation commands.
---
## Notes
- `[P]` tasks target different files and can be worked independently once upstream blockers are cleared.
- `[US1]` and `[US2]` map directly to the feature specification user stories.
- The suggested MVP scope is Phase 1 through Phase 3 only.
- All tasks above follow the required checklist format with task ID, optional parallel marker, story label where applicable, and exact file paths.