feat: close spec 195 action surface residuals #230

Merged
ahmido merged 1 commits from 195-action-surface-closure into dev 2026-04-13 07:47:59 +00:00
20 changed files with 2989 additions and 7 deletions

View File

@ -174,6 +174,8 @@ ## Active Technologies
- PHP 8.4.15 + Laravel 12, Filament v5, Livewire v4, Pest v4, Tailwind CSS v4, existing `OperateHubShell`, `CanonicalNavigationContext`, `CanonicalAdminTenantFilterState`, `UiEnforcement`, `ActionSurfaceValidator`, and Filament page or resource action builders (193-monitoring-action-hierarchy)
- PostgreSQL through existing workspace-owned and tenant-owned models; no schema change planned (193-monitoring-action-hierarchy)
- PHP 8.4.15 + Laravel 12, Filament v5, Livewire v4, Pest v4, Tailwind CSS v4, existing `UiEnforcement`, existing audit loggers (`AuditLogger`, `WorkspaceAuditLogger`, `SystemConsoleAuditLogger`), existing mutation services (`FindingExceptionService`, `FindingWorkflowService`, `TenantReviewLifecycleService`, `EvidenceSnapshotService`, `OperationRunTriageService`) (194-governance-friction-hardening)
- PHP 8.4.15 + Laravel 12, Filament v5, Livewire v4, Pest v4, Tailwind CSS v4, existing `ActionSurfaceDiscovery`, `ActionSurfaceValidator`, `ActionSurfaceExemptions`, `GovernanceActionCatalog`, `UiEnforcement`, `WorkspaceContext`, and existing system/onboarding/auth helpers (195-action-surface-closure)
- PostgreSQL through existing workspace-owned, tenant-owned, and system-visible models; no schema change planned (195-action-surface-closure)
- PHP 8.4.15 (feat/005-bulk-operations)
@ -208,8 +210,8 @@ ## Code Style
PHP 8.4.15: Follow standard conventions
## Recent Changes
- 195-action-surface-closure: Added PHP 8.4.15 + Laravel 12, Filament v5, Livewire v4, Pest v4, Tailwind CSS v4, existing `ActionSurfaceDiscovery`, `ActionSurfaceValidator`, `ActionSurfaceExemptions`, `GovernanceActionCatalog`, `UiEnforcement`, `WorkspaceContext`, and existing system/onboarding/auth helpers
- 195-action-surface-closure: Added PostgreSQL through existing workspace-owned, tenant-owned, and system-visible models; no schema change planned
- 194-governance-friction-hardening: Added PHP 8.4.15 + Laravel 12, Filament v5, Livewire v4, Pest v4, Tailwind CSS v4, existing `UiEnforcement`, existing audit loggers (`AuditLogger`, `WorkspaceAuditLogger`, `SystemConsoleAuditLogger`), existing mutation services (`FindingExceptionService`, `FindingWorkflowService`, `TenantReviewLifecycleService`, `EvidenceSnapshotService`, `OperationRunTriageService`)
- 193-monitoring-action-hierarchy: Added PHP 8.4.15 + Laravel 12, Filament v5, Livewire v4, Pest v4, Tailwind CSS v4, existing `OperateHubShell`, `CanonicalNavigationContext`, `CanonicalAdminTenantFilterState`, `UiEnforcement`, `ActionSurfaceValidator`, and Filament page or resource action builders
- 192-record-header-discipline: Added PHP 8.4.15 + Laravel 12, Filament v5, Livewire v4, Pest v4, Tailwind CSS v4, existing `UiEnforcement`, `RelatedNavigationResolver`, `ActionSurfaceValidator`, and page-local Filament action builders
<!-- MANUAL ADDITIONS START -->
<!-- MANUAL ADDITIONS END -->

View File

@ -49,7 +49,7 @@ public function workspaceTenants(): Collection
->where('workspace_id', (int) $this->workspace->getKey())
->orderBy('name')
->limit(10)
->get(['id', 'name', 'status', 'workspace_id']);
->get(['id', 'name', 'status', 'workspace_id', 'external_id']);
}
/**

View File

@ -36,7 +36,7 @@ public function panel(Panel $panel): Panel
->colors([
'primary' => Color::Blue,
])
->databaseNotifications()
->databaseNotifications(isLazy: false)
->databaseNotificationsPolling(null)
->renderHook(
PanelsRenderHook::BODY_START,

View File

@ -4,6 +4,9 @@
namespace App\Support\Ui\ActionSurface;
use App\Filament\Pages\BreakGlassRecovery;
use App\Filament\Pages\ChooseTenant;
use App\Filament\Pages\ChooseWorkspace;
use App\Filament\Pages\BaselineCompareLanding;
use App\Filament\Pages\BaselineCompareMatrix;
use App\Filament\Pages\Monitoring\Alerts;
@ -13,7 +16,11 @@
use App\Filament\Pages\Monitoring\Operations;
use App\Filament\Pages\Operations\TenantlessOperationRunViewer;
use App\Filament\Pages\Reviews\ReviewRegister;
use App\Filament\Pages\TenantDashboard;
use App\Filament\Pages\TenantDiagnostics;
use App\Filament\Pages\Tenancy\RegisterTenant;
use App\Filament\Pages\Workspaces\ManagedTenantOnboardingWizard;
use App\Filament\Pages\Workspaces\ManagedTenantsLanding;
use App\Filament\Resources\AlertDeliveryResource\Pages\ListAlertDeliveries;
use App\Filament\Resources\AlertDestinationResource\Pages\ViewAlertDestination;
use App\Filament\Resources\BackupSetResource\Pages\ViewBackupSet;
@ -29,6 +36,12 @@
use App\Filament\Resources\TenantResource\Pages\ViewTenant;
use App\Filament\Resources\TenantReviewResource\Pages\ViewTenantReview;
use App\Filament\Resources\Workspaces\Pages\ViewWorkspace;
use App\Filament\System\Pages\Dashboard as SystemDashboard;
use App\Filament\System\Pages\Directory\ViewTenant as SystemDirectoryViewTenant;
use App\Filament\System\Pages\Directory\ViewWorkspace as SystemDirectoryViewWorkspace;
use App\Filament\System\Pages\Ops\Runbooks;
use App\Filament\System\Pages\Ops\ViewRun;
use App\Filament\System\Pages\RepairWorkspaceOwners;
use App\Support\WorkspaceIsolation\TenantOwnedModelFamilies;
final class ActionSurfaceExemptions
@ -46,7 +59,6 @@ public static function baseline(): self
// Baseline allowlist for legacy surfaces. Keep shrinking this list.
// Declared system table pages are discovered directly; deferred system tooling stays out of scope by not opting in.
'App\\Filament\\Pages\\Auth\\Login' => 'Auth entry page is out-of-scope for action-surface retrofits in spec 082.',
'App\\Filament\\Pages\\BreakGlassRecovery' => 'Break-glass flow is governed by dedicated security specs and tests.',
'App\\Filament\\Pages\\ChooseTenant' => 'Tenant chooser has no contract-style table action surface.',
'App\\Filament\\Pages\\ChooseWorkspace' => 'Workspace chooser has no contract-style table action surface.',
'App\\Filament\\Pages\\Tenancy\\RegisterTenant' => 'Tenant onboarding route is covered by onboarding/RBAC specs.',
@ -541,4 +553,400 @@ public static function spec193MonitoringSurface(string $className): ?array
{
return self::spec193MonitoringSurfaceInventory()[$className] ?? null;
}
/**
* @return array<string, array{
* surfaceKey: string,
* surfaceName: string,
* pageClass: string,
* panelPlane: string,
* surfaceKind: string,
* discoveryState: string,
* closureDecision: string,
* reasonCategory: ?string,
* explicitReason: string,
* evidence: array<int, array{
* kind: string,
* reference: string,
* proves: string
* }>,
* followUpAction: string,
* mustRemainBaselineExempt: bool,
* mustNotRemainBaselineExempt: bool
* }>
*/
public static function spec195ResidualSurfaceInventory(): array
{
return [
SystemDashboard::class => [
'surfaceKey' => 'system_dashboard',
'surfaceName' => 'System Console Dashboard',
'pageClass' => SystemDashboard::class,
'panelPlane' => 'system',
'surfaceKind' => 'dashboard_shell',
'discoveryState' => 'outside_primary_discovery',
'closureDecision' => 'separately_governed',
'reasonCategory' => 'workflow_specific_governance',
'explicitReason' => 'The system dashboard keeps its console-window and break-glass controls under dedicated system and recovery tests instead of the generic declaration-backed contract.',
'evidence' => [
[
'kind' => 'feature_livewire_test',
'reference' => 'tests/Feature/System/Spec114/ControlTowerDashboardTest.php',
'proves' => 'The control-tower shell keeps its window action and dashboard rendering behavior under focused system coverage.',
],
[
'kind' => 'feature_livewire_test',
'reference' => 'tests/Feature/Auth/BreakGlassModeTest.php',
'proves' => 'Break-glass entry and exit remain confirmed, audited dashboard actions rather than silent utility links.',
],
],
'followUpAction' => 'add_guard_only',
'mustRemainBaselineExempt' => false,
'mustNotRemainBaselineExempt' => true,
],
ViewRun::class => [
'surfaceKey' => 'system_ops_view_run',
'surfaceName' => 'System Ops View Run',
'pageClass' => ViewRun::class,
'panelPlane' => 'system',
'surfaceKind' => 'system_detail',
'discoveryState' => 'outside_primary_discovery',
'closureDecision' => 'separately_governed',
'reasonCategory' => 'system_triage_surface',
'explicitReason' => 'Run triage remains a dedicated decision surface with confirmed retry, cancel, and investigate behavior instead of fitting the generic declaration-backed list/detail shape.',
'evidence' => [
[
'kind' => 'feature_livewire_test',
'reference' => 'tests/Feature/System/Spec114/OpsTriageActionsTest.php',
'proves' => 'The view-run surface keeps explicit navigation, triage actions, and capability-sensitive visibility.',
],
[
'kind' => 'guard_test',
'reference' => 'tests/Feature/Guards/Spec194GovernanceActionSemanticsGuardTest.php',
'proves' => 'The retry, cancel, and investigate actions remain part of the governed system action semantics inventory.',
],
],
'followUpAction' => 'add_guard_only',
'mustRemainBaselineExempt' => false,
'mustNotRemainBaselineExempt' => true,
],
Runbooks::class => [
'surfaceKey' => 'system_ops_runbooks',
'surfaceName' => 'System Ops Runbooks',
'pageClass' => Runbooks::class,
'panelPlane' => 'system',
'surfaceKind' => 'system_utility',
'discoveryState' => 'outside_primary_discovery',
'closureDecision' => 'separately_governed',
'reasonCategory' => 'workflow_specific_governance',
'explicitReason' => 'Runbooks is a workflow utility hub with its own trusted-state, authorization, and confirmation semantics rather than a declaration-backed record or table surface.',
'evidence' => [
[
'kind' => 'feature_livewire_test',
'reference' => 'tests/Feature/System/OpsRunbooks/FindingsLifecycleBackfillStartTest.php',
'proves' => 'The runbooks shell enforces preflight-first execution, typed confirmation, and capability-gated run behavior.',
],
[
'kind' => 'authorization_test',
'reference' => 'tests/Feature/System/Spec113/AuthorizationSemanticsTest.php',
'proves' => 'The system plane still returns 403 when runbook-view capabilities are missing.',
],
[
'kind' => 'guard_test',
'reference' => 'tests/Feature/Guards/LivewireTrustedStateGuardTest.php',
'proves' => 'Runbooks keeps its trusted-state policy under explicit guard coverage.',
],
],
'followUpAction' => 'add_guard_only',
'mustRemainBaselineExempt' => false,
'mustNotRemainBaselineExempt' => true,
],
RepairWorkspaceOwners::class => [
'surfaceKey' => 'repair_workspace_owners',
'surfaceName' => 'Repair Workspace Owners',
'pageClass' => RepairWorkspaceOwners::class,
'panelPlane' => 'system',
'surfaceKind' => 'system_utility',
'discoveryState' => 'outside_primary_discovery',
'closureDecision' => 'separately_governed',
'reasonCategory' => 'break_glass_repair_utility',
'explicitReason' => 'Emergency owner repair stays under dedicated break-glass and table guard coverage instead of the generic declaration-backed system-table contract.',
'evidence' => [
[
'kind' => 'feature_livewire_test',
'reference' => 'tests/Feature/Auth/BreakGlassWorkspaceOwnerRecoveryTest.php',
'proves' => 'The repair utility requires break-glass context and records audited recovery behavior.',
],
[
'kind' => 'guard_test',
'reference' => 'tests/Feature/Guards/FilamentTableStandardsGuardTest.php',
'proves' => 'The table shell keeps explicit empty-state and table-standard coverage even while remaining outside the primary declaration path.',
],
],
'followUpAction' => 'add_guard_only',
'mustRemainBaselineExempt' => false,
'mustNotRemainBaselineExempt' => true,
],
SystemDirectoryViewTenant::class => [
'surfaceKey' => 'system_directory_view_tenant',
'surfaceName' => 'System Directory View Tenant',
'pageClass' => SystemDirectoryViewTenant::class,
'panelPlane' => 'system',
'surfaceKind' => 'read_mostly_context',
'discoveryState' => 'outside_primary_discovery',
'closureDecision' => 'harmless_special_case',
'reasonCategory' => 'read_mostly_context_detail',
'explicitReason' => 'The tenant directory detail page is a read-mostly drilldown that links outward to canonical admin and run surfaces without introducing its own mutating controls.',
'evidence' => [
[
'kind' => 'feature_livewire_test',
'reference' => 'tests/Feature/System/Spec195/SystemDirectoryResidualSurfaceTest.php',
'proves' => 'The detail page renders contextual connectivity and recent-run information while staying read-mostly and capability-gated.',
],
[
'kind' => 'authorization_test',
'reference' => 'tests/Feature/System/Spec114/DirectoryTenantsTest.php',
'proves' => 'Directory-view capability remains required before the detail route becomes visible.',
],
],
'followUpAction' => 'add_focused_test',
'mustRemainBaselineExempt' => false,
'mustNotRemainBaselineExempt' => true,
],
SystemDirectoryViewWorkspace::class => [
'surfaceKey' => 'system_directory_view_workspace',
'surfaceName' => 'System Directory View Workspace',
'pageClass' => SystemDirectoryViewWorkspace::class,
'panelPlane' => 'system',
'surfaceKind' => 'read_mostly_context',
'discoveryState' => 'outside_primary_discovery',
'closureDecision' => 'harmless_special_case',
'reasonCategory' => 'read_mostly_context_detail',
'explicitReason' => 'The workspace directory detail page is a read-mostly drilldown that exposes context and links, not a declaration-backed mutable system workbench.',
'evidence' => [
[
'kind' => 'feature_livewire_test',
'reference' => 'tests/Feature/System/Spec195/SystemDirectoryResidualSurfaceTest.php',
'proves' => 'The workspace detail page stays capability-gated and renders contextual tenant and run links without mutating actions.',
],
[
'kind' => 'authorization_test',
'reference' => 'tests/Feature/System/Spec114/DirectoryWorkspacesTest.php',
'proves' => 'Directory-view capability remains required before workspace directory routes become available.',
],
],
'followUpAction' => 'add_focused_test',
'mustRemainBaselineExempt' => false,
'mustNotRemainBaselineExempt' => true,
],
BreakGlassRecovery::class => [
'surfaceKey' => 'break_glass_recovery',
'surfaceName' => 'Break Glass Recovery',
'pageClass' => BreakGlassRecovery::class,
'panelPlane' => 'admin',
'surfaceKind' => 'recovery_flow',
'discoveryState' => 'primary_discovered',
'closureDecision' => 'retired_no_longer_relevant',
'reasonCategory' => 'disabled_or_actionless_surface',
'explicitReason' => 'The page currently denies access and exposes no header actions, so it should not remain a live baseline exemption.',
'evidence' => [
[
'kind' => 'audit_test',
'reference' => 'app/Filament/Pages/BreakGlassRecovery.php',
'proves' => 'The page returns false from canAccess() and exposes no header actions.',
],
[
'kind' => 'feature_livewire_test',
'reference' => 'tests/Feature/Auth/BreakGlassWorkspaceOwnerRecoveryTest.php',
'proves' => 'The active recovery path now lives on the system dashboard and repair utility instead of this retired page shell.',
],
],
'followUpAction' => 'tighten_reason',
'mustRemainBaselineExempt' => false,
'mustNotRemainBaselineExempt' => true,
],
ChooseWorkspace::class => [
'surfaceKey' => 'choose_workspace',
'surfaceName' => 'Choose Workspace',
'pageClass' => ChooseWorkspace::class,
'panelPlane' => 'admin',
'surfaceKind' => 'selector',
'discoveryState' => 'primary_discovered_baseline_exempt',
'closureDecision' => 'harmless_special_case',
'reasonCategory' => 'selector_routing_only',
'explicitReason' => 'The workspace chooser is a routing-only selector with explicit membership checks and audit logging, not a declaration-backed action table.',
'evidence' => [
[
'kind' => 'feature_livewire_test',
'reference' => 'tests/Feature/Workspaces/ChooseWorkspacePageTest.php',
'proves' => 'The chooser keeps membership-scoped selection, redirect behavior, and deny-as-not-found semantics.',
],
[
'kind' => 'audit_test',
'reference' => 'tests/Feature/Workspaces/WorkspaceAuditTrailTest.php',
'proves' => 'Manual workspace selection remains explicitly audited.',
],
],
'followUpAction' => 'none',
'mustRemainBaselineExempt' => true,
'mustNotRemainBaselineExempt' => false,
],
ChooseTenant::class => [
'surfaceKey' => 'choose_tenant',
'surfaceName' => 'Choose Tenant',
'pageClass' => ChooseTenant::class,
'panelPlane' => 'tenant',
'surfaceKind' => 'selector',
'discoveryState' => 'primary_discovered_baseline_exempt',
'closureDecision' => 'harmless_special_case',
'reasonCategory' => 'selector_routing_only',
'explicitReason' => 'The tenant chooser is a selector-only surface that filters operable tenants and routes to the tenant dashboard without its own contract-style action surface.',
'evidence' => [
[
'kind' => 'feature_livewire_test',
'reference' => 'tests/Feature/Auth/TenantChooserSelectionTest.php',
'proves' => 'The chooser redirects only for active selectable tenants and rejects non-operable selections with 404.',
],
[
'kind' => 'authorization_test',
'reference' => 'tests/Feature/TenantRBAC/TenantSwitcherScopeTest.php',
'proves' => 'Selector eligibility remains narrower than global tenant discoverability and stays tenant-scope aware.',
],
],
'followUpAction' => 'none',
'mustRemainBaselineExempt' => true,
'mustNotRemainBaselineExempt' => false,
],
RegisterTenant::class => [
'surfaceKey' => 'register_tenant',
'surfaceName' => 'Register Tenant',
'pageClass' => RegisterTenant::class,
'panelPlane' => 'admin',
'surfaceKind' => 'wizard',
'discoveryState' => 'primary_discovered_baseline_exempt',
'closureDecision' => 'separately_governed',
'reasonCategory' => 'registration_form_with_dedicated_rbac',
'explicitReason' => 'Tenant registration is a dedicated creation workflow with its own visibility rules, bootstrap membership side effects, and audit logging.',
'evidence' => [
[
'kind' => 'authorization_test',
'reference' => 'tests/Feature/Rbac/RegisterTenantAuthorizationTest.php',
'proves' => 'Registration visibility remains explicitly capability-sensitive for owner versus readonly members.',
],
[
'kind' => 'feature_livewire_test',
'reference' => 'tests/Feature/TenantRBAC/TenantBootstrapAssignTest.php',
'proves' => 'Registration still bootstraps tenant ownership and audit behavior through the dedicated flow.',
],
],
'followUpAction' => 'none',
'mustRemainBaselineExempt' => true,
'mustNotRemainBaselineExempt' => false,
],
ManagedTenantOnboardingWizard::class => [
'surfaceKey' => 'managed_tenant_onboarding_wizard',
'surfaceName' => 'Managed Tenant Onboarding Wizard',
'pageClass' => ManagedTenantOnboardingWizard::class,
'panelPlane' => 'admin',
'surfaceKind' => 'wizard',
'discoveryState' => 'primary_discovered_baseline_exempt',
'closureDecision' => 'separately_governed',
'reasonCategory' => 'workflow_specific_governance',
'explicitReason' => 'The onboarding wizard is a workflow-specific surface with draft continuity, capability-gated steps, confirmations, and dedicated audit coverage.',
'evidence' => [
[
'kind' => 'authorization_test',
'reference' => 'tests/Feature/Rbac/OnboardingWizardUiEnforcementTest.php',
'proves' => 'The wizard enforces capability checks on its interactive paths instead of inheriting the generic declaration contract.',
],
[
'kind' => 'authorization_test',
'reference' => 'tests/Feature/Onboarding/OnboardingDraftAccessTest.php',
'proves' => 'Workspace and tenant continuity for onboarding drafts remains guarded by dedicated 404 and 403 semantics.',
],
],
'followUpAction' => 'none',
'mustRemainBaselineExempt' => true,
'mustNotRemainBaselineExempt' => false,
],
ManagedTenantsLanding::class => [
'surfaceKey' => 'managed_tenants_landing',
'surfaceName' => 'Managed Tenants Landing',
'pageClass' => ManagedTenantsLanding::class,
'panelPlane' => 'admin',
'surfaceKind' => 'landing',
'discoveryState' => 'primary_discovered_baseline_exempt',
'closureDecision' => 'harmless_special_case',
'reasonCategory' => 'landing_routing_surface',
'explicitReason' => 'The managed-tenants landing is a workspace routing shell that keeps discoverability and open-tenant navigation explicit without pretending to be a generic declaration-backed table page.',
'evidence' => [
[
'kind' => 'feature_livewire_test',
'reference' => 'tests/Feature/Workspaces/Spec195ManagedTenantsLandingTest.php',
'proves' => 'The landing stays membership-scoped, preserves selector routing, and rejects outsider tenant openings.',
],
[
'kind' => 'feature_livewire_test',
'reference' => 'tests/Feature/Filament/ManagedTenantsLandingLifecycleTest.php',
'proves' => 'The landing intentionally exposes broader administrative discoverability than the tenant chooser.',
],
],
'followUpAction' => 'add_focused_test',
'mustRemainBaselineExempt' => true,
'mustNotRemainBaselineExempt' => false,
],
TenantDashboard::class => [
'surfaceKey' => 'tenant_dashboard',
'surfaceName' => 'Tenant Dashboard',
'pageClass' => TenantDashboard::class,
'panelPlane' => 'tenant',
'surfaceKind' => 'dashboard_shell',
'discoveryState' => 'primary_discovered_baseline_exempt',
'closureDecision' => 'harmless_special_case',
'reasonCategory' => 'dashboard_shell_widget_owned',
'explicitReason' => 'The tenant dashboard is a widget shell whose meaningful mutations and visibility rules live in its widgets and follow-up routes rather than in page-level generic actions.',
'evidence' => [
[
'kind' => 'db_only_surface_test',
'reference' => 'tests/Feature/Filament/TenantDashboardDbOnlyTest.php',
'proves' => 'The dashboard shell renders DB-only and keeps its main behavior in widget rendering rather than page-level actions.',
],
[
'kind' => 'authorization_test',
'reference' => 'tests/Feature/Rbac/TenantDashboardArrivalContextVisibilityTest.php',
'proves' => 'Arrival context CTAs remain permission-aware and deny-as-not-found for non-members.',
],
],
'followUpAction' => 'none',
'mustRemainBaselineExempt' => true,
'mustNotRemainBaselineExempt' => false,
],
];
}
/**
* @return array{
* surfaceKey: string,
* surfaceName: string,
* pageClass: string,
* panelPlane: string,
* surfaceKind: string,
* discoveryState: string,
* closureDecision: string,
* reasonCategory: ?string,
* explicitReason: string,
* evidence: array<int, array{
* kind: string,
* reference: string,
* proves: string
* }>,
* followUpAction: string,
* mustRemainBaselineExempt: bool,
* mustNotRemainBaselineExempt: bool
* }|null
*/
public static function spec195ResidualSurface(string $className): ?array
{
return self::spec195ResidualSurfaceInventory()[$className] ?? null;
}
}

View File

@ -6,6 +6,10 @@
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceInspectAffordance;
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceSlot;
use Filament\Pages\Page;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use SplFileInfo;
final class ActionSurfaceValidator
{
@ -53,9 +57,20 @@ public function validate(): ActionSurfaceValidationResult
public function validateComponents(array $components): ActionSurfaceValidationResult
{
$issues = [];
$discoveredClassNames = array_values(array_unique(array_merge(
array_map(
static fn (ActionSurfaceDiscoveredComponent $component): string => $component->className,
$this->discovery->discover(),
),
array_map(
static fn (ActionSurfaceDiscoveredComponent $component): string => $component->className,
$components,
),
)));
$this->validateSpec193MonitoringSurfaceInventory($issues);
$this->validateSpec192RecordPageInventory($issues);
$this->validateSpec195ResidualSurfaceInventory($issues, $discoveredClassNames);
foreach ($components as $component) {
if (! class_exists($component->className)) {
@ -371,6 +386,341 @@ className: $className,
}
}
/**
* @param array<int, ActionSurfaceValidationIssue> $issues
* @param array<int, string> $discoveredClassNames
*/
private function validateSpec195ResidualSurfaceInventory(array &$issues, array $discoveredClassNames): void
{
$issues = array_merge(
$issues,
self::validateSpec195ResidualInventoryFixture(
inventory: ActionSurfaceExemptions::spec195ResidualSurfaceInventory(),
discoveredClasses: $discoveredClassNames,
baselineExemptions: ActionSurfaceExemptions::baseline()->all(),
residualCandidateClasses: $this->spec195ResidualCandidateClasses($discoveredClassNames),
),
);
}
/**
* @param array<string, array<string, mixed>> $inventory
* @param array<int, string> $discoveredClasses
* @param array<string, string> $baselineExemptions
* @param array<int, string> $residualCandidateClasses
* @return array<int, ActionSurfaceValidationIssue>
*/
public static function validateSpec195ResidualInventoryFixture(
array $inventory,
array $discoveredClasses,
array $baselineExemptions,
array $residualCandidateClasses = [],
): array {
$issues = [];
$allowedDiscoveryStates = [
'primary_discovered',
'primary_discovered_baseline_exempt',
'outside_primary_discovery',
];
$allowedClosureDecisions = [
'generic_contract_enrollment',
'intentional_exemption',
'separately_governed',
'retired_no_longer_relevant',
'harmless_special_case',
];
$allowedReasonCategories = [
'system_triage_surface',
'workflow_specific_governance',
'break_glass_repair_utility',
'read_mostly_context_detail',
'disabled_or_actionless_surface',
'selector_routing_only',
'registration_form_with_dedicated_rbac',
'landing_routing_surface',
'dashboard_shell_widget_owned',
'security_flow_exception',
];
$allowedPanelPlanes = ['admin', 'tenant', 'system'];
$allowedSurfaceKinds = [
'system_detail',
'system_utility',
'selector',
'wizard',
'landing',
'dashboard_shell',
'recovery_flow',
'read_mostly_context',
];
$allowedFollowUpActions = [
'none',
'tighten_reason',
'add_guard_only',
'add_focused_test',
'consider_enrollment',
];
$allowedEvidenceKinds = [
'guard_test',
'feature_livewire_test',
'authorization_test',
'workflow_spec',
'audit_test',
'db_only_surface_test',
];
$discoveredLookup = array_fill_keys($discoveredClasses, true);
$surfaceKeys = [];
foreach ($inventory as $className => $surface) {
if (! class_exists($className)) {
$issues[] = new ActionSurfaceValidationIssue(
className: $className,
message: 'Spec 195 residual inventory references a surface class that does not exist.',
hint: 'Keep ActionSurfaceExemptions::spec195ResidualSurfaceInventory() aligned with the in-scope residual surface classes.',
);
continue;
}
$surfaceKey = (string) ($surface['surfaceKey'] ?? '');
if ($surfaceKey === '') {
$issues[] = new ActionSurfaceValidationIssue(
className: $className,
message: 'Spec 195 residual inventory entry is missing a non-empty surface key.',
hint: 'Provide the stable spec surface key for this residual surface.',
);
} elseif (isset($surfaceKeys[$surfaceKey])) {
$issues[] = new ActionSurfaceValidationIssue(
className: $className,
message: sprintf('Spec 195 residual surface key "%s" is declared more than once.', $surfaceKey),
hint: 'Each residual surface must have a unique stable key.',
);
} else {
$surfaceKeys[$surfaceKey] = true;
}
if (($surface['pageClass'] ?? null) !== $className) {
$issues[] = new ActionSurfaceValidationIssue(
className: $className,
message: 'Spec 195 residual inventory pageClass must exactly match the keyed class.',
hint: 'Keep the array key and pageClass field aligned for reviewer clarity.',
);
}
if (! is_string($surface['surfaceName'] ?? null) || trim((string) $surface['surfaceName']) === '') {
$issues[] = new ActionSurfaceValidationIssue(
className: $className,
message: 'Spec 195 residual inventory surfaceName must be non-empty.',
hint: 'Use a human-readable review label such as "System Ops View Run".',
);
}
if (! in_array($surface['panelPlane'] ?? null, $allowedPanelPlanes, true)) {
$issues[] = new ActionSurfaceValidationIssue(
className: $className,
message: 'Spec 195 panel plane is invalid or missing.',
hint: 'Use admin, tenant, or system.',
);
}
if (! in_array($surface['surfaceKind'] ?? null, $allowedSurfaceKinds, true)) {
$issues[] = new ActionSurfaceValidationIssue(
className: $className,
message: 'Spec 195 surface kind is invalid or missing.',
hint: 'Use one of the documented residual surface kinds from the logical contract.',
);
}
if (! in_array($surface['closureDecision'] ?? null, $allowedClosureDecisions, true)) {
$issues[] = new ActionSurfaceValidationIssue(
className: $className,
message: 'Spec 195 closure decision is invalid or missing.',
hint: 'Use generic_contract_enrollment, intentional_exemption, separately_governed, retired_no_longer_relevant, or harmless_special_case.',
);
}
$expectedDiscoveryState = isset($discoveredLookup[$className])
? (array_key_exists($className, $baselineExemptions) ? 'primary_discovered_baseline_exempt' : 'primary_discovered')
: 'outside_primary_discovery';
if (($surface['discoveryState'] ?? null) !== $expectedDiscoveryState) {
$issues[] = new ActionSurfaceValidationIssue(
className: $className,
message: sprintf(
'Spec 195 discovery state is not truthful. Expected "%s".',
$expectedDiscoveryState,
),
hint: 'Keep discoveryState aligned with the primary validator discovery result and baseline exemption registry.',
);
}
if (! in_array($surface['discoveryState'] ?? null, $allowedDiscoveryStates, true)) {
$issues[] = new ActionSurfaceValidationIssue(
className: $className,
message: 'Spec 195 discovery state is invalid or missing.',
hint: 'Use primary_discovered, primary_discovered_baseline_exempt, or outside_primary_discovery.',
);
}
if (! is_string($surface['explicitReason'] ?? null) || trim((string) $surface['explicitReason']) === '') {
$issues[] = new ActionSurfaceValidationIssue(
className: $className,
message: 'Spec 195 explicit reason must be non-empty.',
hint: 'Document the concrete operator or review reason for the chosen closure decision.',
);
}
$closureDecision = (string) ($surface['closureDecision'] ?? '');
$reasonCategory = $surface['reasonCategory'] ?? null;
if ($closureDecision === 'generic_contract_enrollment') {
if (is_string($reasonCategory) && trim($reasonCategory) !== '') {
$issues[] = new ActionSurfaceValidationIssue(
className: $className,
message: 'Generic-contract enrollment entries must not carry a reason category.',
hint: 'Clear reasonCategory when the residual surface is fully enrolled into the generic contract.',
);
}
if (! method_exists($className, 'actionSurfaceDeclaration')) {
$issues[] = new ActionSurfaceValidationIssue(
className: $className,
message: 'Generic-contract enrollment requires actionSurfaceDeclaration().',
hint: 'Enroll the surface into the declaration-backed contract before classifying it as generic_contract_enrollment.',
);
}
} elseif (! in_array($reasonCategory, $allowedReasonCategories, true)) {
$issues[] = new ActionSurfaceValidationIssue(
className: $className,
message: 'Spec 195 reason category is invalid or missing for a non-enrolled residual surface.',
hint: 'Use one of the allowed Spec 195 reason categories for intentional exemptions, separate governance, retired surfaces, and harmless special cases.',
);
}
if (! in_array($surface['followUpAction'] ?? null, $allowedFollowUpActions, true)) {
$issues[] = new ActionSurfaceValidationIssue(
className: $className,
message: 'Spec 195 follow-up action is invalid or missing.',
hint: 'Use none, tighten_reason, add_guard_only, add_focused_test, or consider_enrollment.',
);
}
if (! is_bool($surface['mustRemainBaselineExempt'] ?? null)) {
$issues[] = new ActionSurfaceValidationIssue(
className: $className,
message: 'Spec 195 mustRemainBaselineExempt must be boolean.',
hint: 'Use true only when the discovered page must remain in baseline().',
);
}
if (! is_bool($surface['mustNotRemainBaselineExempt'] ?? null)) {
$issues[] = new ActionSurfaceValidationIssue(
className: $className,
message: 'Spec 195 mustNotRemainBaselineExempt must be boolean.',
hint: 'Use true when the residual surface must stay out of baseline().',
);
}
if (($surface['mustRemainBaselineExempt'] ?? false) === true && ($surface['mustNotRemainBaselineExempt'] ?? false) === true) {
$issues[] = new ActionSurfaceValidationIssue(
className: $className,
message: 'Spec 195 baseline flags cannot both be true.',
hint: 'A residual surface can either stay in baseline() or be required to stay out of it, but not both.',
);
}
if (($surface['mustRemainBaselineExempt'] ?? false) === true && ! array_key_exists($className, $baselineExemptions)) {
$issues[] = new ActionSurfaceValidationIssue(
className: $className,
message: 'Spec 195 says this residual surface must remain baseline-exempt, but baseline() does not include it.',
hint: 'Keep discovered special surfaces aligned between baseline() and spec195ResidualSurfaceInventory().',
);
}
if (($surface['mustNotRemainBaselineExempt'] ?? false) === true && array_key_exists($className, $baselineExemptions)) {
$issues[] = new ActionSurfaceValidationIssue(
className: $className,
message: 'Spec 195 says this residual surface must not remain baseline-exempt, but baseline() still includes it.',
hint: 'Remove the stale baseline exemption or change the residual closure classification.',
);
}
$evidence = $surface['evidence'] ?? null;
if (! is_array($evidence) || $evidence === []) {
$issues[] = new ActionSurfaceValidationIssue(
className: $className,
message: 'Spec 195 residual surfaces require at least one structured evidence descriptor.',
hint: 'Add one or more evidence entries with kind, reference, and proves.',
);
} else {
foreach ($evidence as $index => $descriptor) {
if (! is_array($descriptor)) {
$issues[] = new ActionSurfaceValidationIssue(
className: $className,
message: sprintf('Spec 195 evidence entry #%d must be an array.', $index + 1),
hint: 'Use structured evidence descriptors with kind, reference, and proves.',
);
continue;
}
if (! in_array($descriptor['kind'] ?? null, $allowedEvidenceKinds, true)) {
$issues[] = new ActionSurfaceValidationIssue(
className: $className,
message: sprintf('Spec 195 evidence entry #%d kind is invalid or missing.', $index + 1),
hint: 'Use guard_test, feature_livewire_test, authorization_test, workflow_spec, audit_test, or db_only_surface_test.',
);
}
if (! is_string($descriptor['reference'] ?? null) || trim((string) $descriptor['reference']) === '') {
$issues[] = new ActionSurfaceValidationIssue(
className: $className,
message: sprintf('Spec 195 evidence entry #%d reference must be non-empty.', $index + 1),
hint: 'Point reviewers at the concrete test file or source artifact that proves the classification.',
);
}
if (! is_string($descriptor['proves'] ?? null) || trim((string) $descriptor['proves']) === '') {
$issues[] = new ActionSurfaceValidationIssue(
className: $className,
message: sprintf('Spec 195 evidence entry #%d proves text must be non-empty.', $index + 1),
hint: 'Explain what the referenced evidence actually proves about the residual surface.',
);
}
}
}
}
$candidateClasses = array_values(array_unique(array_merge(
$residualCandidateClasses,
self::spec195BaselineResidualCandidateClasses($baselineExemptions),
)));
sort($candidateClasses);
foreach ($candidateClasses as $className) {
if (array_key_exists($className, $inventory)) {
continue;
}
$classPath = self::spec195ClassPath($className);
$issues[] = new ActionSurfaceValidationIssue(
className: $className,
message: 'Residual action surface is missing a Spec 195 closure entry.',
hint: $classPath !== null
? sprintf(
'Add %s to ActionSurfaceExemptions::spec195ResidualSurfaceInventory() and classify it via the reviewer workflow. File: %s',
$className,
$classPath,
)
: 'Add the missing residual surface to ActionSurfaceExemptions::spec195ResidualSurfaceInventory() and classify it via the reviewer workflow.',
);
}
return $issues;
}
/**
* @param array<int, ActionSurfaceValidationIssue> $issues
*/
@ -419,10 +769,26 @@ private function validateClassExemptionOrFail(string $className, array &$issues)
$reason = $this->exemptions->reasonForClass($className);
if ($reason === null) {
$residualSurface = ActionSurfaceExemptions::spec195ResidualSurface($className);
if ($residualSurface !== null) {
$closureDecision = (string) ($residualSurface['closureDecision'] ?? '');
if ($closureDecision === 'generic_contract_enrollment') {
$issues[] = new ActionSurfaceValidationIssue(
className: $className,
message: 'Missing action-surface declaration and no component exemption exists.',
hint: 'Add actionSurfaceDeclaration() or register a baseline exemption with a non-empty reason.',
message: 'Residual surface is marked for generic-contract enrollment but still lacks actionSurfaceDeclaration().',
hint: 'Add actionSurfaceDeclaration() or change the Spec 195 closure decision if the surface is intentionally staying separate.',
);
}
return;
}
$issues[] = new ActionSurfaceValidationIssue(
className: $className,
message: 'Missing action-surface declaration, baseline exemption, and Spec 195 residual closure entry.',
hint: 'Add actionSurfaceDeclaration(), register a baseline exemption with a non-empty reason, or classify the surface in spec195ResidualSurfaceInventory().',
);
return;
@ -653,4 +1019,142 @@ className: $className,
hint: 'Keep exportIsDefaultBulkActionForReadOnly=true or exempt ListBulkMoreGroup with a reason.',
);
}
/**
* @param array<int, string> $discoveredClassNames
* @return array<int, string>
*/
private function spec195ResidualCandidateClasses(array $discoveredClassNames): array
{
$candidates = $this->spec195SystemResidualCandidateClasses($discoveredClassNames);
foreach (self::spec195BaselineResidualCandidateClasses(ActionSurfaceExemptions::baseline()->all()) as $className) {
$candidates[] = $className;
}
$candidates = array_values(array_unique($candidates));
sort($candidates);
return $candidates;
}
/**
* @param array<int, string> $discoveredClassNames
* @return array<int, string>
*/
private function spec195SystemResidualCandidateClasses(array $discoveredClassNames): array
{
$discoveredLookup = array_fill_keys($discoveredClassNames, true);
$classes = [];
foreach ($this->collectPhpClasses($this->appFilamentSystemPagesPath()) as $className) {
if (isset($discoveredLookup[$className])) {
continue;
}
if ($className === 'App\\Filament\\System\\Pages\\Auth\\Login') {
continue;
}
if (! class_exists($className) || ! is_subclass_of($className, Page::class)) {
continue;
}
$classes[] = $className;
}
sort($classes);
return $classes;
}
private function appFilamentSystemPagesPath(): string
{
return base_path('app/Filament/System/Pages');
}
/**
* @param array<string, string> $baselineExemptions
* @return array<int, string>
*/
private static function spec195BaselineResidualCandidateClasses(array $baselineExemptions): array
{
$classes = [];
foreach (array_keys($baselineExemptions) as $className) {
if (! self::qualifiesAsSpec195BaselineResidualCandidate($className)) {
continue;
}
$classes[] = $className;
}
sort($classes);
return $classes;
}
private static function qualifiesAsSpec195BaselineResidualCandidate(string $className): bool
{
if (in_array($className, [
'App\\Filament\\Pages\\BreakGlassRecovery',
'App\\Filament\\Pages\\ChooseTenant',
'App\\Filament\\Pages\\ChooseWorkspace',
'App\\Filament\\Pages\\TenantDashboard',
], true)) {
return true;
}
return str_starts_with($className, 'App\\Filament\\Pages\\Tenancy\\')
|| str_starts_with($className, 'App\\Filament\\Pages\\Workspaces\\');
}
/**
* @return array<int, string>
*/
private function collectPhpClasses(string $directory): array
{
if (! is_dir($directory)) {
return [];
}
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($directory, RecursiveDirectoryIterator::SKIP_DOTS),
);
$classes = [];
/** @var SplFileInfo $file */
foreach ($iterator as $file) {
if (! $file->isFile() || ! str_ends_with($file->getFilename(), '.php')) {
continue;
}
$classes[] = $this->classNameFromPath($file->getPathname());
}
sort($classes);
return $classes;
}
private function classNameFromPath(string $path): string
{
$normalizedPath = str_replace('\\', '/', $path);
$normalizedAppPath = str_replace('\\', '/', app_path());
$relativePath = ltrim(substr($normalizedPath, strlen($normalizedAppPath)), '/');
return 'App\\'.str_replace('/', '\\', substr($relativePath, 0, -4));
}
private static function spec195ClassPath(string $className): ?string
{
if (! str_starts_with($className, 'App\\')) {
return null;
}
$path = base_path('app/'.str_replace('\\', '/', substr($className, 4)).'.php');
return is_file($path) ? $path : null;
}
}

View File

@ -10,6 +10,7 @@
use App\Models\BackupSet;
use App\Models\Finding;
use App\Models\OperationRun;
use App\Models\User;
use Filament\Facades\Filament;
use Illuminate\Support\Facades\Bus;
use Livewire\Livewire;
@ -76,3 +77,12 @@
Bus::assertNothingDispatched();
});
it('keeps tenant dashboard access deny-as-not-found for non-members', function (): void {
[, $tenant] = createUserWithTenant(role: 'owner');
$outsider = User::factory()->create();
$this->actingAs($outsider)
->get(TenantDashboard::getUrl(tenant: $tenant))
->assertNotFound();
});

View File

@ -932,6 +932,64 @@ function actionSurfaceSystemPanelContext(array $capabilities): PlatformUser
->toContain('OnboardingVerificationV1_5UxTest');
});
it('documents the spec 195 residual inventory, human-readable names, and baseline alignment', function (): void {
$inventory = ActionSurfaceExemptions::spec195ResidualSurfaceInventory();
$baselineExemptions = ActionSurfaceExemptions::baseline()->all();
expect(array_keys($inventory))->toEqualCanonicalizing([
\App\Filament\System\Pages\Dashboard::class,
\App\Filament\System\Pages\Ops\ViewRun::class,
\App\Filament\System\Pages\Ops\Runbooks::class,
\App\Filament\System\Pages\RepairWorkspaceOwners::class,
\App\Filament\System\Pages\Directory\ViewTenant::class,
\App\Filament\System\Pages\Directory\ViewWorkspace::class,
\App\Filament\Pages\BreakGlassRecovery::class,
\App\Filament\Pages\ChooseWorkspace::class,
\App\Filament\Pages\ChooseTenant::class,
\App\Filament\Pages\Tenancy\RegisterTenant::class,
\App\Filament\Pages\Workspaces\ManagedTenantOnboardingWizard::class,
\App\Filament\Pages\Workspaces\ManagedTenantsLanding::class,
\App\Filament\Pages\TenantDashboard::class,
]);
foreach ($inventory as $className => $surface) {
expect(trim((string) ($surface['surfaceName'] ?? '')))
->not->toBe('', "{$className} must keep a human-readable surfaceName in Spec 195.")
->and($surface['pageClass'] ?? null)->toBe($className)
->and($surface['evidence'] ?? [])->not->toBeEmpty("{$className} must keep structured Spec 195 evidence.");
}
$mustRemainBaselineExempt = collect($inventory)
->filter(fn (array $surface): bool => ($surface['mustRemainBaselineExempt'] ?? false) === true)
->keys()
->values()
->all();
$mustNotRemainBaselineExempt = collect($inventory)
->filter(fn (array $surface): bool => ($surface['mustNotRemainBaselineExempt'] ?? false) === true)
->keys()
->values()
->all();
expect($mustRemainBaselineExempt)->toEqualCanonicalizing([
\App\Filament\Pages\ChooseWorkspace::class,
\App\Filament\Pages\ChooseTenant::class,
\App\Filament\Pages\Tenancy\RegisterTenant::class,
\App\Filament\Pages\Workspaces\ManagedTenantOnboardingWizard::class,
\App\Filament\Pages\Workspaces\ManagedTenantsLanding::class,
\App\Filament\Pages\TenantDashboard::class,
]);
foreach ($mustRemainBaselineExempt as $className) {
expect(array_key_exists($className, $baselineExemptions))
->toBeTrue("{$className} should stay aligned between baseline() and Spec 195.");
}
foreach ($mustNotRemainBaselineExempt as $className) {
expect(array_key_exists($className, $baselineExemptions))
->toBeFalse("{$className} must not keep a stale baseline exemption under Spec 195.");
}
});
it('keeps enrolled system panel pages declaration-backed without stale baseline exemptions', function (): void {
$baselineExemptions = ActionSurfaceExemptions::baseline();
@ -976,6 +1034,50 @@ function actionSurfaceSystemPanelContext(array $capabilities): PlatformUser
}
});
it('keeps residual system pages outside primary discovery but inside the spec 195 closure inventory', function (): void {
$components = collect(ActionSurfaceValidator::withBaselineExemptions()->discoveredComponents())
->keyBy('className');
foreach ([
\App\Filament\System\Pages\Dashboard::class,
\App\Filament\System\Pages\Ops\ViewRun::class,
\App\Filament\System\Pages\Ops\Runbooks::class,
\App\Filament\System\Pages\RepairWorkspaceOwners::class,
\App\Filament\System\Pages\Directory\ViewTenant::class,
\App\Filament\System\Pages\Directory\ViewWorkspace::class,
] as $className) {
expect($components->has($className))
->toBeFalse("{$className} should stay outside the primary declaration-backed discovery scope.")
->and(ActionSurfaceExemptions::spec195ResidualSurface($className))
->not->toBeNull("{$className} must still carry an explicit Spec 195 closure entry.");
}
});
it('reports actionable file context when a residual surface is missing from the spec 195 inventory', function (): void {
$inventory = ActionSurfaceExemptions::spec195ResidualSurfaceInventory();
unset($inventory[\App\Filament\System\Pages\Dashboard::class]);
$issues = ActionSurfaceValidator::validateSpec195ResidualInventoryFixture(
inventory: $inventory,
discoveredClasses: array_map(
static fn ($component): string => $component->className,
ActionSurfaceValidator::withBaselineExemptions()->discoveredComponents(),
),
baselineExemptions: ActionSurfaceExemptions::baseline()->all(),
residualCandidateClasses: [\App\Filament\System\Pages\Dashboard::class],
);
$formattedIssues = implode("\n", array_map(
static fn ($issue): string => $issue->format(),
$issues,
));
expect($formattedIssues)
->toContain(\App\Filament\System\Pages\Dashboard::class)
->toContain('Residual action surface is missing a Spec 195 closure entry')
->toContain('Dashboard.php');
});
it('keeps enrolled relation managers declaration-backed without stale baseline exemptions', function (): void {
$baselineExemptions = ActionSurfaceExemptions::baseline();

View File

@ -122,6 +122,28 @@ className: $className,
);
}
/**
* @return array<int, string>
*/
function repositoryDiscoveredActionSurfaceClasses(): array
{
return array_map(
static fn (ActionSurfaceDiscoveredComponent $component): string => $component->className,
ActionSurfaceValidator::withBaselineExemptions()->discoveredComponents(),
);
}
/**
* @param array<int, \App\Support\Ui\ActionSurface\ActionSurfaceValidationIssue> $issues
*/
function formatActionSurfaceIssues(array $issues): string
{
return implode("\n", array_map(
static fn ($issue): string => $issue->format(),
$issues,
));
}
it('passes when all required slots are declared', function (): void {
$validator = new ActionSurfaceValidator(
profileDefinition: new ActionSurfaceProfileDefinition,
@ -245,3 +267,73 @@ className: $className,
expect($result->hasIssues())->toBeFalse($result->formatForAssertion());
});
it('accepts the repository spec 195 residual inventory even when only inventory validation runs', function (): void {
$validator = ActionSurfaceValidator::withBaselineExemptions();
$result = $validator->validateComponents([]);
expect($result->hasIssues())->toBeFalse($result->formatForAssertion());
});
it('fails when a residual system candidate is missing a spec 195 closure entry', function (): void {
$inventory = ActionSurfaceExemptions::spec195ResidualSurfaceInventory();
unset($inventory[\App\Filament\System\Pages\Dashboard::class]);
$issues = ActionSurfaceValidator::validateSpec195ResidualInventoryFixture(
inventory: $inventory,
discoveredClasses: repositoryDiscoveredActionSurfaceClasses(),
baselineExemptions: ActionSurfaceExemptions::baseline()->all(),
residualCandidateClasses: [\App\Filament\System\Pages\Dashboard::class],
);
expect(formatActionSurfaceIssues($issues))
->toContain(\App\Filament\System\Pages\Dashboard::class)
->toContain('Residual action surface is missing a Spec 195 closure entry');
});
it('fails when a non-enrolled spec 195 residual surface has no reason category', function (): void {
$inventory = ActionSurfaceExemptions::spec195ResidualSurfaceInventory();
$inventory[\App\Filament\Pages\ChooseWorkspace::class]['reasonCategory'] = null;
$issues = ActionSurfaceValidator::validateSpec195ResidualInventoryFixture(
inventory: $inventory,
discoveredClasses: repositoryDiscoveredActionSurfaceClasses(),
baselineExemptions: ActionSurfaceExemptions::baseline()->all(),
);
expect(formatActionSurfaceIssues($issues))
->toContain(\App\Filament\Pages\ChooseWorkspace::class)
->toContain('reason category is invalid or missing');
});
it('fails when a spec 195 residual surface is missing structured evidence', function (): void {
$inventory = ActionSurfaceExemptions::spec195ResidualSurfaceInventory();
$inventory[\App\Filament\Pages\Workspaces\ManagedTenantsLanding::class]['evidence'] = [];
$issues = ActionSurfaceValidator::validateSpec195ResidualInventoryFixture(
inventory: $inventory,
discoveredClasses: repositoryDiscoveredActionSurfaceClasses(),
baselineExemptions: ActionSurfaceExemptions::baseline()->all(),
);
expect(formatActionSurfaceIssues($issues))
->toContain(\App\Filament\Pages\Workspaces\ManagedTenantsLanding::class)
->toContain('require at least one structured evidence descriptor');
});
it('fails when a retired spec 195 residual surface still remains baseline-exempt', function (): void {
$inventory = ActionSurfaceExemptions::spec195ResidualSurfaceInventory();
$baselineExemptions = ActionSurfaceExemptions::baseline()->all();
$baselineExemptions[\App\Filament\Pages\BreakGlassRecovery::class] = 'Stale retired exemption.';
$issues = ActionSurfaceValidator::validateSpec195ResidualInventoryFixture(
inventory: $inventory,
discoveredClasses: repositoryDiscoveredActionSurfaceClasses(),
baselineExemptions: $baselineExemptions,
);
expect(formatActionSurfaceIssues($issues))
->toContain(\App\Filament\Pages\BreakGlassRecovery::class)
->toContain('must not remain baseline-exempt');
});

View File

@ -0,0 +1,156 @@
<?php
declare(strict_types=1);
use App\Support\Ui\ActionSurface\ActionSurfaceDiscoveredComponent;
use App\Support\Ui\ActionSurface\ActionSurfaceExemptions;
use App\Support\Ui\ActionSurface\ActionSurfaceValidator;
/**
* @return array<int, string>
*/
function spec195DiscoveredClasses(): array
{
return array_map(
static fn (ActionSurfaceDiscoveredComponent $component): string => $component->className,
ActionSurfaceValidator::withBaselineExemptions()->discoveredComponents(),
);
}
/**
* @param array<int, \App\Support\Ui\ActionSurface\ActionSurfaceValidationIssue> $issues
*/
function spec195FormattedIssues(array $issues): string
{
return implode("\n", array_map(
static fn ($issue): string => $issue->format(),
$issues,
));
}
it('keeps every spec 195 residual surface classified exactly once with structured evidence', function (): void {
$inventory = ActionSurfaceExemptions::spec195ResidualSurfaceInventory();
expect(array_keys($inventory))->toEqualCanonicalizing([
\App\Filament\System\Pages\Dashboard::class,
\App\Filament\System\Pages\Ops\ViewRun::class,
\App\Filament\System\Pages\Ops\Runbooks::class,
\App\Filament\System\Pages\RepairWorkspaceOwners::class,
\App\Filament\System\Pages\Directory\ViewTenant::class,
\App\Filament\System\Pages\Directory\ViewWorkspace::class,
\App\Filament\Pages\BreakGlassRecovery::class,
\App\Filament\Pages\ChooseWorkspace::class,
\App\Filament\Pages\ChooseTenant::class,
\App\Filament\Pages\Tenancy\RegisterTenant::class,
\App\Filament\Pages\Workspaces\ManagedTenantOnboardingWizard::class,
\App\Filament\Pages\Workspaces\ManagedTenantsLanding::class,
\App\Filament\Pages\TenantDashboard::class,
]);
$surfaceKeys = collect($inventory)->pluck('surfaceKey')->all();
expect($surfaceKeys)->toHaveCount(count(array_unique($surfaceKeys)));
foreach ($inventory as $className => $surface) {
expect($surface['closureDecision'] ?? null)
->not->toBeNull("{$className} must keep a closure decision.")
->and(trim((string) ($surface['surfaceName'] ?? '')))
->not->toBe('', "{$className} must keep a human-readable surfaceName.")
->and($surface['evidence'] ?? [])
->not->toBeEmpty("{$className} must keep structured evidence.");
}
});
it('keeps the residual system tail explicitly classified instead of silently baseline-exempted', function (): void {
$inventory = ActionSurfaceExemptions::spec195ResidualSurfaceInventory();
$baselineExemptions = ActionSurfaceExemptions::baseline()->all();
expect($inventory[\App\Filament\System\Pages\Ops\ViewRun::class]['closureDecision'] ?? null)->toBe('separately_governed')
->and($inventory[\App\Filament\System\Pages\Ops\Runbooks::class]['closureDecision'] ?? null)->toBe('separately_governed')
->and($inventory[\App\Filament\System\Pages\RepairWorkspaceOwners::class]['closureDecision'] ?? null)->toBe('separately_governed')
->and($inventory[\App\Filament\System\Pages\Directory\ViewTenant::class]['closureDecision'] ?? null)->toBe('harmless_special_case')
->and($inventory[\App\Filament\System\Pages\Directory\ViewWorkspace::class]['closureDecision'] ?? null)->toBe('harmless_special_case')
->and($inventory[\App\Filament\System\Pages\Dashboard::class]['closureDecision'] ?? null)->toBe('separately_governed');
foreach ([
\App\Filament\System\Pages\Dashboard::class,
\App\Filament\System\Pages\Ops\ViewRun::class,
\App\Filament\System\Pages\Ops\Runbooks::class,
\App\Filament\System\Pages\RepairWorkspaceOwners::class,
\App\Filament\System\Pages\Directory\ViewTenant::class,
\App\Filament\System\Pages\Directory\ViewWorkspace::class,
] as $className) {
expect(array_key_exists($className, $baselineExemptions))
->toBeFalse("{$className} must not rely on baseline() for Spec 195 closure.");
}
});
it('retires break glass recovery from live baseline handling', function (): void {
$surface = ActionSurfaceExemptions::spec195ResidualSurface(\App\Filament\Pages\BreakGlassRecovery::class);
expect($surface)->not->toBeNull()
->and($surface['closureDecision'] ?? null)->toBe('retired_no_longer_relevant')
->and($surface['reasonCategory'] ?? null)->toBe('disabled_or_actionless_surface')
->and(ActionSurfaceExemptions::baseline()->hasClass(\App\Filament\Pages\BreakGlassRecovery::class))->toBeFalse();
});
it('fails when a residual candidate is missing a closure decision entry', function (): void {
$inventory = ActionSurfaceExemptions::spec195ResidualSurfaceInventory();
unset($inventory[\App\Filament\System\Pages\Dashboard::class]);
$issues = ActionSurfaceValidator::validateSpec195ResidualInventoryFixture(
inventory: $inventory,
discoveredClasses: spec195DiscoveredClasses(),
baselineExemptions: ActionSurfaceExemptions::baseline()->all(),
residualCandidateClasses: [\App\Filament\System\Pages\Dashboard::class],
);
expect(spec195FormattedIssues($issues))
->toContain('Residual action surface is missing a Spec 195 closure entry')
->toContain(\App\Filament\System\Pages\Dashboard::class);
});
it('fails when a discovered residual exemption loses its reason category', function (): void {
$inventory = ActionSurfaceExemptions::spec195ResidualSurfaceInventory();
$inventory[\App\Filament\Pages\ChooseTenant::class]['reasonCategory'] = null;
$issues = ActionSurfaceValidator::validateSpec195ResidualInventoryFixture(
inventory: $inventory,
discoveredClasses: spec195DiscoveredClasses(),
baselineExemptions: ActionSurfaceExemptions::baseline()->all(),
);
expect(spec195FormattedIssues($issues))
->toContain(\App\Filament\Pages\ChooseTenant::class)
->toContain('reason category is invalid or missing');
});
it('fails when a residual surface loses its structured evidence', function (): void {
$inventory = ActionSurfaceExemptions::spec195ResidualSurfaceInventory();
$inventory[\App\Filament\System\Pages\Directory\ViewTenant::class]['evidence'] = [];
$issues = ActionSurfaceValidator::validateSpec195ResidualInventoryFixture(
inventory: $inventory,
discoveredClasses: spec195DiscoveredClasses(),
baselineExemptions: ActionSurfaceExemptions::baseline()->all(),
);
expect(spec195FormattedIssues($issues))
->toContain(\App\Filament\System\Pages\Directory\ViewTenant::class)
->toContain('require at least one structured evidence descriptor');
});
it('fails when a retired surface is reintroduced as a stale baseline exemption', function (): void {
$baselineExemptions = ActionSurfaceExemptions::baseline()->all();
$baselineExemptions[\App\Filament\Pages\BreakGlassRecovery::class] = 'Stale retired page exemption.';
$issues = ActionSurfaceValidator::validateSpec195ResidualInventoryFixture(
inventory: ActionSurfaceExemptions::spec195ResidualSurfaceInventory(),
discoveredClasses: spec195DiscoveredClasses(),
baselineExemptions: $baselineExemptions,
);
expect(spec195FormattedIssues($issues))
->toContain(\App\Filament\Pages\BreakGlassRecovery::class)
->toContain('must not remain baseline-exempt');
});

View File

@ -2,6 +2,7 @@
use App\Filament\Pages\Tenancy\RegisterTenant;
use Filament\Facades\Filament;
use Livewire\Livewire;
describe('Register tenant page authorization', function () {
it('is not visible for readonly members', function () {
@ -29,4 +30,19 @@
Filament::setCurrentPanel(null);
});
it('rejects readonly members when they try to mount the register-tenant page', function () {
[$user, $tenant] = createUserWithTenant(role: 'readonly');
$this->actingAs($user);
$tenant->makeCurrent();
Filament::setCurrentPanel(Filament::getPanel('tenant'));
Livewire::actingAs($user)
->test(RegisterTenant::class)
->assertNotFound();
Filament::setCurrentPanel(null);
});
});

View File

@ -0,0 +1,127 @@
<?php
declare(strict_types=1);
use App\Models\OperationRun;
use App\Models\PlatformUser;
use App\Models\ProviderConnection;
use App\Models\Tenant;
use App\Models\Workspace;
use App\Support\Auth\PlatformCapabilities;
use App\Support\Providers\ProviderConsentStatus;
use App\Support\Providers\ProviderVerificationStatus;
use App\Support\System\SystemDirectoryLinks;
use App\Support\System\SystemOperationRunLinks;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
it('requires directory-view capability on residual system directory detail pages', function (): void {
$workspace = Workspace::factory()->create();
$tenant = Tenant::factory()->create([
'workspace_id' => (int) $workspace->getKey(),
]);
$platformUser = PlatformUser::factory()->create([
'capabilities' => [
PlatformCapabilities::ACCESS_SYSTEM_PANEL,
],
'is_active' => true,
]);
$this->actingAs($platformUser, 'platform')
->get(SystemDirectoryLinks::tenantDetail($tenant))
->assertForbidden();
$this->actingAs($platformUser, 'platform')
->get(SystemDirectoryLinks::workspaceDetail($workspace))
->assertForbidden();
});
it('keeps the residual system tenant detail page read-mostly and contextual', function (): void {
$workspace = Workspace::factory()->create(['name' => 'Residual Directory Workspace']);
$tenant = Tenant::factory()->active()->create([
'workspace_id' => (int) $workspace->getKey(),
'name' => 'Residual Directory Tenant',
]);
ProviderConnection::factory()->create([
'workspace_id' => (int) $workspace->getKey(),
'tenant_id' => (int) $tenant->getKey(),
'provider' => 'microsoft',
'display_name' => 'Residual Default Connection',
'is_default' => true,
'is_enabled' => true,
'consent_status' => ProviderConsentStatus::Granted->value,
'verification_status' => ProviderVerificationStatus::Healthy->value,
]);
$run = OperationRun::factory()->create([
'workspace_id' => (int) $workspace->getKey(),
'tenant_id' => (int) $tenant->getKey(),
]);
$platformUser = PlatformUser::factory()->create([
'capabilities' => [
PlatformCapabilities::ACCESS_SYSTEM_PANEL,
PlatformCapabilities::DIRECTORY_VIEW,
],
'is_active' => true,
]);
$this->actingAs($platformUser, 'platform')
->get(SystemDirectoryLinks::tenantDetail($tenant))
->assertSuccessful()
->assertSee('Residual Directory Tenant')
->assertSee('Residual Directory Workspace')
->assertSee('Connectivity signals')
->assertSee('Residual Default Connection')
->assertSee('Open in /admin')
->assertSee(SystemDirectoryLinks::adminTenant($tenant), false)
->assertSee('Open operations runs')
->assertSee(SystemOperationRunLinks::index(), false)
->assertSee(SystemOperationRunLinks::view($run), false)
->assertDontSee('Enter break-glass mode')
->assertDontSee('Emergency: Assign Owner');
});
it('keeps the residual system workspace detail page read-mostly and link-driven', function (): void {
$workspace = Workspace::factory()->create(['name' => 'Residual Workspace Detail']);
$tenant = Tenant::factory()->active()->create([
'workspace_id' => (int) $workspace->getKey(),
'name' => 'Workspace Detail Tenant',
]);
$run = OperationRun::factory()->create([
'workspace_id' => (int) $workspace->getKey(),
'tenant_id' => (int) $tenant->getKey(),
]);
$platformUser = PlatformUser::factory()->create([
'capabilities' => [
PlatformCapabilities::ACCESS_SYSTEM_PANEL,
PlatformCapabilities::DIRECTORY_VIEW,
],
'is_active' => true,
]);
$response = $this->actingAs($platformUser, 'platform')
->get(SystemDirectoryLinks::workspaceDetail($workspace))
->assertSuccessful()
->assertSee('Residual Workspace Detail')
->assertSee('Tenants summary')
->assertSee('Workspace Detail Tenant')
->assertSee(SystemDirectoryLinks::tenantDetail($tenant), false)
->assertSee('Open in /admin')
->assertSee(SystemDirectoryLinks::adminWorkspace($workspace), false)
->assertSee('Open operations runs')
->assertSee(SystemOperationRunLinks::index(), false)
->assertSee(SystemOperationRunLinks::view($run), false)
->assertDontSee('Enter break-glass mode')
->assertDontSee('Emergency: Assign Owner');
$html = $response->getContent();
expect($html)->toContain('wire:name="Filament\\Livewire\\DatabaseNotifications"');
expect($html)->not->toContain('__lazyLoad');
});

View File

@ -0,0 +1,106 @@
<?php
declare(strict_types=1);
use App\Filament\Pages\ChooseTenant;
use App\Filament\Pages\Workspaces\ManagedTenantsLanding;
use App\Filament\Resources\TenantResource;
use App\Models\Tenant;
use App\Models\User;
use App\Models\Workspace;
use App\Models\WorkspaceMembership;
use App\Support\Workspaces\WorkspaceContext;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;
uses(RefreshDatabase::class);
it('keeps the spec 195 managed-tenants landing available without an active tenant context', function (): void {
$workspace = Workspace::factory()->create(['slug' => 'spec195-managed-tenants']);
$user = User::factory()->create();
WorkspaceMembership::factory()->create([
'workspace_id' => (int) $workspace->getKey(),
'user_id' => (int) $user->getKey(),
'role' => 'owner',
]);
$tenant = Tenant::factory()->active()->create([
'workspace_id' => (int) $workspace->getKey(),
'name' => 'Spec195 Landing Tenant',
]);
$user->tenants()->syncWithoutDetaching([
$tenant->getKey() => ['role' => 'owner'],
]);
$this->actingAs($user)
->withSession([WorkspaceContext::SESSION_KEY => (int) $workspace->getKey()])
->get(route('admin.workspace.managed-tenants.index', ['workspace' => $workspace]))
->assertSuccessful()
->assertSee('Spec195 Landing Tenant')
->assertSee('Managed tenants')
->assertDontSee('No tenant selected');
});
it('routes the managed-tenants landing back into the chooser flow and open-tenant flow', function (): void {
$workspace = Workspace::factory()->create(['slug' => 'spec195-managed-routing']);
$user = User::factory()->create();
WorkspaceMembership::factory()->create([
'workspace_id' => (int) $workspace->getKey(),
'user_id' => (int) $user->getKey(),
'role' => 'owner',
]);
$tenant = Tenant::factory()->active()->create([
'workspace_id' => (int) $workspace->getKey(),
'name' => 'Spec195 Routed Tenant',
]);
$user->tenants()->syncWithoutDetaching([
$tenant->getKey() => ['role' => 'owner'],
]);
$this->withSession([WorkspaceContext::SESSION_KEY => (int) $workspace->getKey()]);
session([WorkspaceContext::SESSION_KEY => (int) $workspace->getKey()]);
$component = Livewire::actingAs($user)
->test(ManagedTenantsLanding::class, ['workspace' => $workspace]);
$component
->call('goToChooseTenant')
->assertRedirect(ChooseTenant::getUrl());
$this->withSession([WorkspaceContext::SESSION_KEY => (int) $workspace->getKey()]);
session([WorkspaceContext::SESSION_KEY => (int) $workspace->getKey()]);
Livewire::actingAs($user)
->test(ManagedTenantsLanding::class, ['workspace' => $workspace])
->call('openTenant', $tenant->getKey())
->assertRedirect(TenantResource::getUrl('view', ['record' => $tenant]));
});
it('rejects opening a tenant from the landing when the actor lacks tenant entitlement', function (): void {
$workspace = Workspace::factory()->create(['slug' => 'spec195-managed-guard']);
$user = User::factory()->create();
WorkspaceMembership::factory()->create([
'workspace_id' => (int) $workspace->getKey(),
'user_id' => (int) $user->getKey(),
'role' => 'owner',
]);
$tenant = Tenant::factory()->active()->create([
'workspace_id' => (int) $workspace->getKey(),
'name' => 'Spec195 Guarded Tenant',
]);
$this->withSession([WorkspaceContext::SESSION_KEY => (int) $workspace->getKey()]);
session([WorkspaceContext::SESSION_KEY => (int) $workspace->getKey()]);
Livewire::actingAs($user)
->test(ManagedTenantsLanding::class, ['workspace' => $workspace])
->call('openTenant', $tenant->getKey())
->assertNotFound();
});

View File

@ -0,0 +1,36 @@
# Specification Quality Checklist: Action Surface Enforcement, Enrollment, and Exception Closure
**Purpose**: Validate specification completeness and quality before proceeding to planning
**Created**: 2026-04-12
**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 on first pass against the spec template and closure requirements.
- No open clarification markers remain.
- The spec stays bounded to residual closure, discovery limits, exemptions, and regression protection after Specs 192 to 194.

View File

@ -0,0 +1,219 @@
openapi: 3.1.0
info:
title: Action Surface Closure Logical Contract
version: 0.1.0
description: >-
Logical design contract for Spec 195 residual action-surface closure.
This is a planning artifact that defines the required reviewable shape for
residual pages that sit outside or alongside the primary action-surface
discovery path.
servers:
- url: https://logical-spec.local
description: Non-runtime planning contract
paths:
/internal/action-surfaces/residual:
get:
summary: List Spec 195 residual action-surface closure entries
operationId: listResidualActionSurfaceClosures
responses:
'200':
description: Residual closure entries in validator order
content:
application/json:
schema:
type: object
required:
- data
properties:
data:
type: array
items:
$ref: '#/components/schemas/ResidualActionSurfaceClosure'
/internal/action-surfaces/residual/{surfaceKey}:
get:
summary: Read one Spec 195 residual action-surface closure entry
operationId: getResidualActionSurfaceClosure
parameters:
- name: surfaceKey
in: path
required: true
schema:
$ref: '#/components/schemas/SurfaceKey'
responses:
'200':
description: Residual closure entry
content:
application/json:
schema:
type: object
required:
- data
properties:
data:
$ref: '#/components/schemas/ResidualActionSurfaceClosure'
components:
schemas:
SurfaceKey:
type: string
pattern: '^[a-z0-9_]+$'
description: Stable machine-readable key for one residual surface. The initial seed list is recorded in x-spec-195-notes.seedSurfaceKeys and may be extended by audit.
DiscoveryState:
type: string
enum:
- primary_discovered
- primary_discovered_baseline_exempt
- outside_primary_discovery
ClosureDecision:
type: string
enum:
- generic_contract_enrollment
- intentional_exemption
- separately_governed
- retired_no_longer_relevant
- harmless_special_case
ReasonCategory:
type: string
enum:
- system_triage_surface
- workflow_specific_governance
- break_glass_repair_utility
- read_mostly_context_detail
- disabled_or_actionless_surface
- selector_routing_only
- registration_form_with_dedicated_rbac
- landing_routing_surface
- dashboard_shell_widget_owned
- security_flow_exception
FollowUpAction:
type: string
enum:
- none
- tighten_reason
- add_guard_only
- add_focused_test
- consider_enrollment
EvidenceDescriptor:
type: object
required:
- reference
- proves
properties:
reference:
type: string
proves:
type: string
kind:
type: string
enum:
- guard_test
- feature_livewire_test
- authorization_test
- workflow_spec
- audit_test
- db_only_surface_test
ResidualActionSurfaceClosureBase:
type: object
required:
- surfaceKey
- surfaceName
- pageClass
- panelPlane
- surfaceKind
- discoveryState
- closureDecision
- explicitReason
- evidence
- followUpAction
- mustRemainBaselineExempt
- mustNotRemainBaselineExempt
properties:
surfaceKey:
$ref: '#/components/schemas/SurfaceKey'
surfaceName:
type: string
description: Human-readable review name for the residual surface
pageClass:
type: string
panelPlane:
type: string
enum:
- admin
- tenant
- system
surfaceKind:
type: string
enum:
- system_detail
- system_utility
- selector
- wizard
- landing
- dashboard_shell
- recovery_flow
- read_mostly_context
discoveryState:
$ref: '#/components/schemas/DiscoveryState'
closureDecision:
$ref: '#/components/schemas/ClosureDecision'
reasonCategory:
anyOf:
- $ref: '#/components/schemas/ReasonCategory'
- type: 'null'
explicitReason:
type: string
evidence:
type: array
minItems: 1
items:
$ref: '#/components/schemas/EvidenceDescriptor'
followUpAction:
$ref: '#/components/schemas/FollowUpAction'
mustRemainBaselineExempt:
type: boolean
mustNotRemainBaselineExempt:
type: boolean
ResidualActionSurfaceClosure:
allOf:
- $ref: '#/components/schemas/ResidualActionSurfaceClosureBase'
- oneOf:
- properties:
closureDecision:
const: generic_contract_enrollment
- required:
- reasonCategory
properties:
closureDecision:
type: string
enum:
- intentional_exemption
- separately_governed
- retired_no_longer_relevant
- harmless_special_case
reasonCategory:
$ref: '#/components/schemas/ReasonCategory'
x-spec-195-notes:
seedSurfaceKeys:
- system_dashboard
- system_ops_view_run
- system_ops_runbooks
- repair_workspace_owners
- system_directory_view_tenant
- system_directory_view_workspace
- break_glass_recovery
- choose_workspace
- choose_tenant
- register_tenant
- managed_tenant_onboarding_wizard
- managed_tenants_landing
- tenant_dashboard
consumers:
- apps/platform/app/Support/Ui/ActionSurface/ActionSurfaceExemptions.php
- apps/platform/app/Support/Ui/ActionSurface/ActionSurfaceValidator.php
- apps/platform/tests/Feature/Guards/ActionSurfaceContractTest.php
- apps/platform/tests/Feature/Guards/ActionSurfaceValidatorTest.php
- apps/platform/tests/Feature/Guards/Spec195ResidualActionSurfaceClosureGuardTest.php
nonGoals:
- runtime API exposure
- new persistence
- new provider or routing structure
- widening primary action-surface discovery to every Filament page class

View File

@ -0,0 +1,149 @@
# Data Model: Action Surface Enforcement, Enrollment, and Exception Closure
## Overview
This feature introduces no new persisted entity, table, or user-facing workflow model. It adds a derived repository-governance model for residual action-bearing surfaces that currently sit outside clearly catalogued generic-contract coverage.
The goal of the model is to answer five questions for every residual surface:
1. Is the surface discovered by the primary validator path?
2. If not, is the gap explicit?
3. What is the final closure decision?
4. Why is that decision justified?
5. Which existing tests or guards prove the decision is real?
Each entry must also stay human-reviewable, so the inventory carries both a stable machine key and a human-readable surface name.
## Existing Source Truths Reused Without Change
The following truths remain authoritative and are not redefined by this feature:
- existing page and route classes
- existing authorization semantics, capability registries, and `UiEnforcement` rules
- existing `OperationRun`, audit, and break-glass behavior
- existing `ActionSurfaceDiscovery` behavior for declaration-backed generic surfaces
- existing baseline exemptions for discovered pages that are intentionally outside the generic contract
- existing system, onboarding, chooser, and dashboard test suites
This feature changes classification and regression proof only.
## New Derived Planning Models
### ResidualSurfaceInventoryEntry
**Type**: Spec 195 inventory entry
**Source**: one structured in-code inventory plus validator checks
| Field | Type | Notes |
|------|------|-------|
| `surfaceKey` | string | Stable identifier such as `system_ops_view_run` or `choose_workspace` |
| `surfaceName` | string | Human-readable review name such as `System Ops View Run` or `Choose Workspace` |
| `pageClass` | string | Concrete Filament page class |
| `panelPlane` | string | `admin`, `tenant`, or `system` |
| `surfaceKind` | string | `system_detail`, `system_utility`, `selector`, `wizard`, `landing`, `dashboard_shell`, `recovery_flow`, or `read_mostly_context` |
| `discoveryState` | string | `primary_discovered`, `primary_discovered_baseline_exempt`, or `outside_primary_discovery` |
| `closureDecision` | string | `generic_contract_enrollment`, `intentional_exemption`, `separately_governed`, `retired_no_longer_relevant`, or `harmless_special_case` |
| `reasonCategory` | string or null | Required for every decision except pure enrollment |
| `explicitReason` | string | Short reviewable explanation |
| `evidence` | array<CoverageEvidenceDescriptor> | Structured evidence descriptors that justify the decision |
| `followUpAction` | string | `none`, `tighten_reason`, `add_guard_only`, `add_focused_test`, or `consider_enrollment` |
| `mustRemainBaselineExempt` | boolean | True when the discovered page must stay in `baseline()` |
| `mustNotRemainBaselineExempt` | boolean | True when the surface must not remain in `baseline()` |
### CoverageEvidenceDescriptor
**Type**: derived proof entry
**Source**: existing test and spec references
| Field | Type | Notes |
|------|------|-------|
| `surfaceKey` | string | Links the evidence to one residual surface |
| `kind` | string | `guard_test`, `feature_livewire_test`, `authorization_test`, `workflow_spec`, `audit_test`, or `db_only_surface_test` |
| `reference` | string | Relative file path or stable spec reference |
| `proves` | string | What the evidence actually proves |
| `gapIfMissing` | boolean | True when Spec 195 should add or tighten coverage |
### DiscoveryBoundaryRule
**Type**: derived validator rule
**Source**: existing `ActionSurfaceDiscovery` behavior plus Spec 195 clarification
| Field | Type | Notes |
|------|------|-------|
| `boundaryKey` | string | Stable identifier for one primary-discovery boundary |
| `appliesTo` | string | `resources`, `relation_managers`, `pages`, `system_table_pages`, or `non_discovered_special_pages` |
| `currentRule` | string | Human-readable statement of what discovery includes or excludes |
| `silentGapRisk` | boolean | Whether a surface can currently evade review if the rule stays implicit |
| `spec195Mitigation` | string | How the residual inventory or guard closes that gap |
### ResidualSurfaceRegressionExpectation
**Type**: guard expectation entry
**Source**: Spec 195 validation rules derived from `mustRemainBaselineExempt`, `mustNotRemainBaselineExempt`, and the other closure-entry fields
| Field | Type | Notes |
|------|------|-------|
| `surfaceKey` | string | Residual surface under guard |
| `mustHaveClosureDecision` | boolean | Always true for in-scope residuals |
| `mustHaveReasonCategory` | boolean | True when not enrolled |
| `mustHaveEvidence` | boolean | True for every non-retired residual |
| `mustRemainInBaselineExemptions` | boolean | True only for discovered pages still intentionally outside the generic contract |
| `mustNotRemainInBaselineExemptions` | boolean | True for retired surfaces and non-discovered system pages |
| `needsFocusedTest` | boolean | True when existing evidence is not yet strong enough |
## Initial Seed Inventory for Spec 195
This is the planned closure inventory derived from current code and test evidence. The implementation audit added `system_dashboard` to the original 12-row seed because it is an action-bearing system surface that also sits outside primary discovery.
| Surface Key | Surface Name | Page Class | Current State | Planned Closure Decision | Reason Category | Strongest Evidence | Planned Follow-up |
|---|---|---|---|---|---|---|---|
| `system_dashboard` | `System Console Dashboard` | `App\Filament\System\Pages\Dashboard` | outside primary discovery, not exempt | `separately_governed` | `workflow_specific_governance` | `tests/Feature/System/Spec114/ControlTowerDashboardTest.php`, `tests/Feature/Auth/BreakGlassModeTest.php` | `add_guard_only` |
| `system_ops_view_run` | `System Ops View Run` | `App\Filament\System\Pages\Ops\ViewRun` | outside primary discovery, not exempt | `separately_governed` | `system_triage_surface` | `tests/Feature/System/Spec114/OpsTriageActionsTest.php`, `tests/Feature/Guards/Spec194GovernanceActionSemanticsGuardTest.php` | `add_guard_only` |
| `system_ops_runbooks` | `System Ops Runbooks` | `App\Filament\System\Pages\Ops\Runbooks` | outside primary discovery, not exempt | `separately_governed` | `workflow_specific_governance` | `tests/Feature/System/OpsRunbooks/FindingsLifecycleBackfillStartTest.php`, `tests/Feature/System/Spec113/AuthorizationSemanticsTest.php`, `tests/Feature/Guards/LivewireTrustedStateGuardTest.php` | `add_guard_only` |
| `repair_workspace_owners` | `Repair Workspace Owners` | `App\Filament\System\Pages\RepairWorkspaceOwners` | outside primary discovery, not exempt | `separately_governed` | `break_glass_repair_utility` | `tests/Feature/Auth/BreakGlassWorkspaceOwnerRecoveryTest.php`, `tests/Feature/Guards/FilamentTableStandardsGuardTest.php` | `add_guard_only` |
| `system_directory_view_tenant` | `System Directory View Tenant` | `App\Filament\System\Pages\Directory\ViewTenant` | outside primary discovery, not exempt | `harmless_special_case` | `read_mostly_context_detail` | current code is read-mostly with contextual links only | `add_focused_test` |
| `system_directory_view_workspace` | `System Directory View Workspace` | `App\Filament\System\Pages\Directory\ViewWorkspace` | outside primary discovery, not exempt | `harmless_special_case` | `read_mostly_context_detail` | current code is read-mostly with contextual links only | `add_focused_test` |
| `break_glass_recovery` | `Break Glass Recovery` | `App\Filament\Pages\BreakGlassRecovery` | primary discovered + baseline exempt, but currently inaccessible and actionless | `retired_no_longer_relevant` | `disabled_or_actionless_surface` | current page code: `canAccess() === false`, empty header actions | `tighten_reason` |
| `choose_workspace` | `Choose Workspace` | `App\Filament\Pages\ChooseWorkspace` | primary discovered + baseline exempt | `harmless_special_case` | `selector_routing_only` | `tests/Feature/Workspaces/ChooseWorkspacePageTest.php`, `tests/Feature/Workspaces/WorkspaceAuditTrailTest.php` | `none` |
| `choose_tenant` | `Choose Tenant` | `App\Filament\Pages\ChooseTenant` | primary discovered + baseline exempt | `harmless_special_case` | `selector_routing_only` | `tests/Feature/Auth/TenantChooserSelectionTest.php`, `tests/Feature/TenantRBAC/TenantSwitcherScopeTest.php` | `none` |
| `register_tenant` | `Register Tenant` | `App\Filament\Pages\Tenancy\RegisterTenant` | primary discovered + baseline exempt | `separately_governed` | `registration_form_with_dedicated_rbac` | `tests/Feature/Rbac/RegisterTenantAuthorizationTest.php`, `tests/Feature/TenantRBAC/TenantBootstrapAssignTest.php` | `none` |
| `managed_tenant_onboarding_wizard` | `Managed Tenant Onboarding Wizard` | `App\Filament\Pages\Workspaces\ManagedTenantOnboardingWizard` | primary discovered + baseline exempt | `separately_governed` | `workflow_specific_governance` | Spec 172 + extensive onboarding, audit, RBAC, and secret-safety tests | `none` |
| `managed_tenants_landing` | `Managed Tenants Landing` | `App\Filament\Pages\Workspaces\ManagedTenantsLanding` | primary discovered + baseline exempt | `harmless_special_case` | `landing_routing_surface` | current page code plus indirect workspace routing coverage | `add_focused_test` |
| `tenant_dashboard` | `Tenant Dashboard` | `App\Filament\Pages\TenantDashboard` | primary discovered + baseline exempt | `harmless_special_case` | `dashboard_shell_widget_owned` | `tests/Feature/Filament/TenantDashboardDbOnlyTest.php`, arrival-context and visibility tests | `none` |
## Discovery Boundary Rules
### Rule 1 — Generic primary discovery stays declaration-first
- Resources, relation managers, and normal pages remain discovered through the existing primary validator path.
- System pages remain discovered only when they are table-backed and declaration-backed.
- Spec 195 does not change that rule; it documents and guards it.
### Rule 2 — Residual non-discovered system/detail/workflow pages require supplemental closure inventory
- Any in-scope residual page outside primary discovery must still appear in the Spec 195 inventory.
- Being outside primary discovery no longer implies being outside governance.
### Rule 3 — Baseline exemptions remain only for discovered pages still intentionally outside the generic contract
- `baseline()` remains the compatibility mechanism for discovered pages without generic declarations.
- Spec 195 inventory adds the stronger closure semantics and structured evidence.
- Retired pages should leave `baseline()`.
## Resolution Rules
1. Every residual surface gets exactly one closure decision.
2. Non-enrolled residual surfaces must include a reason category, explicit reason, and at least one structured evidence descriptor.
3. Discovered pages that stay outside the generic contract may still remain in `baseline()`, but only if the Spec 195 inventory explains why.
4. Non-discovered pages must never rely on `baseline()` alone for closure; the residual inventory is the authoritative closure record.
5. `retired_no_longer_relevant` surfaces must not keep active baseline exemptions.
6. `harmless_special_case` is reserved for routing-only, read-mostly, or shell-like surfaces whose risk stays low and explicit.
7. `separately_governed` is reserved for surfaces with dedicated workflow rules, tests, or guards that already meaningfully constrain behavior.
8. If audit reveals a new residual surface outside the initial seed, it must be added to the inventory, contract surface key set, and guard expectations before the feature is considered complete. `system_dashboard` is the first such audited addition in this spec.
## Safety Rules
- No residual closure entry may weaken existing route scope, capability enforcement, audit behavior, or confirmation semantics.
- No residual surface may be marked harmless merely because it has low coverage; low coverage requires new tests, not a softer category.
- No special workflow may remain exempt only by historical memory; explicit evidence is required.
- No new residual page in the relevant namespaces may merge without a closure decision and structured evidence.

View File

@ -0,0 +1,286 @@
# Implementation Plan: Action Surface Enforcement, Enrollment, and Exception Closure
**Branch**: `195-action-surface-closure` | **Date**: 2026-04-12 | **Spec**: `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/195-action-surface-closure/spec.md`
**Input**: Feature specification from `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/195-action-surface-closure/spec.md`
**Note**: This plan keeps the implementation inside the existing Filament v5 / Livewire v4 page layer, the current `ActionSurfaceDiscovery` + `ActionSurfaceValidator` + `ActionSurfaceExemptions` infrastructure, and the current focused RBAC, system-ops, onboarding, chooser, and dashboard test suites. It explicitly avoids adding a new runtime action-surface framework or new persistence.
## Summary
Close the residual action-surface governance gap left after Specs 192 to 194 by preserving the current primary discovery boundary, adding one explicit residual-closure inventory for non-discovered and baseline-exempt special surfaces, assigning every remaining residual page exactly one closure decision, tightening stale exemptions, and extending guard coverage so no new action-bearing residual surface can enter the repo without an explicit decision. The plan favors explicit inventory plus focused tests over forcing every system, wizard, selector, or dashboard surface into the generic `actionSurfaceDeclaration()` contract.
## Technical Context
**Language/Version**: PHP 8.4.15
**Primary Dependencies**: Laravel 12, Filament v5, Livewire v4, Pest v4, Tailwind CSS v4, existing `ActionSurfaceDiscovery`, `ActionSurfaceValidator`, `ActionSurfaceExemptions`, `GovernanceActionCatalog`, `UiEnforcement`, `WorkspaceContext`, and existing system/onboarding/auth helpers
**Storage**: PostgreSQL through existing workspace-owned, tenant-owned, and system-visible models; no schema change planned
**Testing**: Pest feature tests, existing guard tests, existing Livewire page tests, and focused browser smoke only if a residual surface genuinely needs it; all run through Laravel Sail
**Target Platform**: Laravel monolith web application under `apps/platform`, spanning admin `/admin`, tenant-context `/admin/t/{tenant}/...`, and system `/system` surfaces
**Project Type**: web application
**Performance Goals**: Keep residual-surface validation repo-local and deterministic, preserve DB-only render behavior on existing monitoring and dashboard surfaces, avoid new render-time outbound I/O, and avoid extra polling or runtime indirection
**Constraints**: No new persistence, no new action-surface runtime framework, no provider or route-family changes, no authorization-plane changes, no silent exemptions, no weakening of 404/403 semantics, no change to existing destructive-action confirmation or audit behavior, and no new PHP enum unless validator-checked strings prove insufficient
**Scale/Scope**: an initial seed of 12 residual target surfaces, plus the additionally audited `system_dashboard` residual and any future residual pages uncovered by audit, including 6 current system/detail or utility surfaces outside primary discovery and not baseline-exempt, plus 7 currently baseline-exempt special flows or dashboard-like surfaces with uneven dedicated coverage
## Constitution Check
*GATE: Passed before Phase 0 research. Re-checked after Phase 1 design and still passing.*
| Principle | Pre-Research | Post-Design | Notes |
|-----------|--------------|-------------|-------|
| Inventory-first / snapshots-second | PASS | PASS | The feature governs residual UI enforcement only and does not change inventory, backup, or snapshot truth. |
| Read/write separation | PASS | PASS | Residual surfaces reuse existing writes only; confirmation, audit, and focused tests remain unchanged. |
| Graph contract path | N/A | N/A | No Graph contract or provider endpoint change is introduced. |
| Deterministic capabilities | PASS | PASS | Existing capability registries and server-side checks stay authoritative. |
| Workspace + tenant isolation | PASS | PASS | Closure decisions do not widen scope; non-member access remains `404`, member-without-capability remains `403`. |
| RBAC-UX authorization semantics | PASS | PASS | Existing Gates, Policies, capability helpers, and destructive confirmations remain in force. |
| Run observability / Ops-UX | PASS | PASS | Existing `OperationRun` surfaces and DB-only repairs remain governed exactly as they are today. |
| Data minimization | PASS | PASS | No new persistence or mirrored truth is planned; all closure metadata stays derived in code and tests. |
| Proportionality / anti-bloat | PASS | PASS | The plan adds one bounded residual inventory and validator pass, not a new framework. |
| UI semantics / few layers | PASS | PASS | The solution uses explicit inventory records and tests rather than presenters or a new semantic stack. |
| Filament-native UI | PASS | PASS | Existing Filament pages, actions, tables, and page tests remain the implementation path. |
| Surface taxonomy / action-surface discipline | PASS | PASS | The plan closes uncatalogued residuals explicitly without redefining Specs 192 to 194. |
| Filament v5 / Livewire v4 compliance | PASS | PASS | All touched surfaces remain inside the current Filament v5 + Livewire v4 stack. |
| Provider registration location | PASS | PASS | No panel/provider registration change is planned; Laravel 11+ provider registration remains in `bootstrap/providers.php`. |
| Global search hard rule | PASS | PASS | No globally searchable resource is added or modified. |
| Destructive action safety | PASS | PASS | Existing destructive or recovery actions keep `->requiresConfirmation()` and current authorization. |
| Asset strategy | PASS | PASS | No new global or on-demand assets are required; existing `cd apps/platform && php artisan filament:assets` deploy handling remains sufficient. |
## Filament-Specific Compliance Notes
- **Livewire v4.0+ compliance**: The plan stays entirely on Filament v5 + Livewire v4 and introduces no legacy API mix.
- **Provider registration location**: No provider changes are required; Laravel 11+ panel providers remain in `bootstrap/providers.php`.
- **Global search**: No resource search behavior changes. Residual surfaces are pages, dashboards, selectors, or system utilities, not new searchable resources.
- **Destructive actions**: Existing dangerous actions such as `Cancel`, `Repair owner state`, onboarding completion steps, and registration or recovery mutations remain routed through confirmed Filament actions with server-side authorization and existing audit behavior.
- **Asset strategy**: No new assets are planned. Existing deployment handling of `cd apps/platform && php artisan filament:assets` remains unchanged.
- **Testing plan**: Extend the current guard layer, reuse the existing focused system, auth, onboarding, dashboard, and RBAC suites as explicit coverage evidence, and add only the minimum new tests needed to close weak or currently uncatalogued residuals.
## Phase 0 Research
Research outcomes are captured in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/195-action-surface-closure/research.md`.
Key decisions:
- Preserve the current primary discovery boundary instead of auto-discovering every system or workflow page.
- Add one parallel `spec195ResidualSurfaceInventory()` to `ActionSurfaceExemptions` rather than rewriting `baseline()` or stretching `ActionSurfaceDeclaration()` to every surface type.
- Model closure decisions and reason categories as validator-checked strings in the inventory instead of adding new PHP enums or persistence.
- Default residual system pages to `separately_governed` or `harmless_special_case` unless a surface already fits the existing declaration-backed list/detail contract naturally.
- Reuse existing dedicated tests as coverage evidence for onboarding, selectors, runbooks, system triage, and dashboard shells; add focused tests only for weakly covered pages such as `ManagedTenantsLanding` and system directory detail pages.
- Treat `BreakGlassRecovery` as a stale exemption candidate and retire it if it remains inaccessible and actionless.
## Phase 1 Design
Design artifacts are created under `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/195-action-surface-closure/`:
- `research.md`: decisions and rejected alternatives for residual closure, discovery boundaries, and exemption cleanup
- `data-model.md`: derived closure inventory, evidence, and regression-expectation models
- `contracts/action-surface-closure.logical.openapi.yaml`: internal logical contract for residual-surface closure decisions and guard expectations
- `quickstart.md`: implementation and verification sequence for the feature
Design highlights:
- Keep the generic `ActionSurfaceDeclaration()` system limited to the surfaces it already fits well: declaration-backed resources, pages, relation managers, and explicitly enrolled system table pages.
- Represent every residual surface through one explicit closure inventory entry recording class, plane, discovery status, closure decision, reason category, explicit reason, structured evidence, and follow-up testing needs.
- Keep `ActionSurfaceDiscovery` explicit about what it does and does not discover; close the gap through supplemental validator inventory rather than broad auto-discovery.
- Use existing page-local behavior and focused tests for system triage, runbooks, onboarding, chooser flows, and dashboard shells instead of creating shared runtime resolvers.
- Remove or reclassify stale baseline exemptions rather than renaming historical drift.
## Phase 1 — Agent Context Update
Planned command:
- `.specify/scripts/bash/update-agent-context.sh copilot`
This feature does not introduce a new technology stack, but the required agent-context refresh still runs after the technical context and design artifacts are complete.
## Project Structure
### Documentation (this feature)
```text
specs/195-action-surface-closure/
├── plan.md
├── research.md
├── data-model.md
├── quickstart.md
├── spec.md
├── contracts/
│ └── action-surface-closure.logical.openapi.yaml
└── checklists/
└── requirements.md
```
### Source Code (repository root)
```text
apps/platform/
├── app/
│ ├── Filament/
│ │ ├── Pages/
│ │ │ ├── BreakGlassRecovery.php # AUDIT / likely retire as stale exemption
│ │ │ ├── ChooseWorkspace.php # REUSE / classify as harmless special case
│ │ │ ├── ChooseTenant.php # REUSE / classify as harmless special case
│ │ │ ├── TenantDashboard.php # REUSE / classify page shell explicitly
│ │ │ ├── Tenancy/
│ │ │ │ └── RegisterTenant.php # REUSE / separately governed
│ │ │ └── Workspaces/
│ │ │ ├── ManagedTenantOnboardingWizard.php # REUSE / separately governed
│ │ │ └── ManagedTenantsLanding.php # AUDIT / likely add focused coverage
│ │ └── System/
│ │ └── Pages/
│ │ ├── RepairWorkspaceOwners.php # AUDIT / separately governed closure
│ │ ├── Directory/
│ │ │ ├── ViewTenant.php # AUDIT / likely harmless or separate governance
│ │ │ └── ViewWorkspace.php # AUDIT / likely harmless or separate governance
│ │ └── Ops/
│ │ ├── Runbooks.php # REUSE / separately governed closure
│ │ └── ViewRun.php # REUSE / separately governed closure
│ └── Support/
│ └── Ui/
│ └── ActionSurface/
│ ├── ActionSurfaceDiscovery.php # REUSE / boundary remains explicit
│ ├── ActionSurfaceExemptions.php # MODIFY
│ └── ActionSurfaceValidator.php # MODIFY
└── tests/
└── Feature/
├── Guards/
│ ├── ActionSurfaceContractTest.php # MODIFY
│ ├── ActionSurfaceValidatorTest.php # MODIFY
│ ├── Spec194GovernanceActionSemanticsGuardTest.php # REUSE
│ └── Spec195ResidualActionSurfaceClosureGuardTest.php # NEW
├── Auth/
│ ├── BreakGlassWorkspaceOwnerRecoveryTest.php # REUSE / possible extend
│ └── TenantChooserSelectionTest.php # REUSE
├── Workspaces/
│ ├── ChooseWorkspacePageTest.php # REUSE
│ ├── ManagedTenantsWorkspaceRoutingTest.php # REUSE / possible extend
│ └── Spec195ManagedTenantsLandingTest.php # NEW
├── Rbac/
│ ├── RegisterTenantAuthorizationTest.php # REUSE
│ ├── OnboardingWizardUiEnforcementTest.php # REUSE
│ └── TenantDashboardArrivalContextVisibilityTest.php # REUSE
├── System/
│ ├── Spec113/AuthorizationSemanticsTest.php # REUSE / runbooks auth semantics
│ ├── Spec114/OpsTriageActionsTest.php # REUSE / system triage semantics
│ ├── OpsRunbooks/FindingsLifecycleBackfillStartTest.php # REUSE
│ └── Spec195/SystemDirectoryResidualSurfaceTest.php # NEW
├── Filament/
│ └── TenantDashboardDbOnlyTest.php # REUSE
└── Onboarding/
└── OnboardingDraftAccessTest.php # REUSE / explicit wizard governance evidence
```
**Structure Decision**: Keep all work inside the existing Laravel/Filament monolith under `apps/platform`. Modify only the existing action-surface support layer plus targeted tests. Do not create a new runtime registry, new persistence, or new shared page abstraction.
## Complexity Tracking
| Violation | Why Needed | Simpler Alternative Rejected Because |
|-----------|------------|-------------------------------------|
| Cross-surface residual closure inventory and reason-category vocabulary (BLOAT-001 trigger) | The feature must explicitly distinguish enrolled, intentionally exempt, separately governed, retired, and harmless residual surfaces across code paths that the primary discovery system does not cover. | Leaving only free-form baseline reason strings and scattered tests would not let CI distinguish stale exemptions, uncatalogued system pages, or legitimate separately governed workflows. |
## Proportionality Review
- **Current operator problem**: Reviewers cannot tell whether residual system, utility, workflow, selector, landing, and dashboard surfaces are intentionally outside the generic contract or simply missed by discovery.
- **Existing structure is insufficient because**: `ActionSurfaceDiscovery` plus `baseline()` exemptions cover declaration-backed surfaces and a small discovered-exempt set, but they do not explain non-discovered system/detail pages or distinguish harmless, separate, retired, and true exemption states.
- **Narrowest correct implementation**: Add one bounded residual inventory plus validator checks, keep the current discovery boundary explicit, reuse existing dedicated test suites as evidence, and add only the minimum new tests for weakly covered residuals.
- **Ownership cost created**: One more derived inventory in the action-surface support layer, one new guard test, a few focused closure tests, and ongoing review discipline for future residual pages.
- **Alternative intentionally rejected**: Auto-discovering every system and workflow page or forcing every residual surface into `actionSurfaceDeclaration()` was rejected because the current contract is list/detail-oriented and many residual surfaces are legitimate special workflows rather than malformed generic surfaces.
- **Release truth**: current-release governance closure and regression prevention
## Implementation Strategy
### Phase A — Codify the residual closure inventory and explicit discovery boundary
Goal: make every residual surface reviewable in CI without widening the runtime framework.
Changes:
- Add `spec195ResidualSurfaceInventory()` to `ActionSurfaceExemptions` with one entry per seeded or newly audited residual target surface.
- Extend `ActionSurfaceValidator` with Spec 195 validation for allowed closure decisions, allowed reason categories, duplicate keys, required structured evidence, and explicit primary-discovery status.
- Keep `ActionSurfaceDiscovery` behavior unchanged, but make the validator assert when a residual surface is outside primary discovery and lacks a supplemental closure decision.
- Add `Spec195ResidualActionSurfaceClosureGuardTest.php` and extend existing guard tests so the residual inventory becomes mandatory.
- Keep `baseline()` for backward-compatible discovered-page exemptions, but align its live entries with the new Spec 195 inventory.
Tests:
- Extend `ActionSurfaceContractTest.php` and `ActionSurfaceValidatorTest.php` with Spec 195 expectations.
- Add `Spec195ResidualActionSurfaceClosureGuardTest.php`.
### Phase B — Close uncatalogued system and utility surfaces
Goal: explicitly classify the system pages that are currently neither discovered nor baseline-exempt.
Changes:
- Classify `Dashboard` as `separately_governed`, backed by the existing control-tower and break-glass test suites rather than forcing it into the generic declaration contract.
- Classify `ViewRun` as `separately_governed`, backed by Spec 114 triage tests and Spec 194 governance-action guards.
- Classify `Runbooks` as `separately_governed`, backed by Spec 113 auth semantics, runbook start/preflight tests, trusted-state guards, and Ops-UX coverage.
- Classify `RepairWorkspaceOwners` as `separately_governed`, backed by break-glass recovery and table-standard tests.
- Classify `System Directory ViewTenant` and `System Directory ViewWorkspace` as `harmless_special_case` if they remain read-mostly contextual drilldowns; otherwise promote them to `separately_governed` with focused tests.
- Do not force these pages into `actionSurfaceDeclaration()` unless implementation audit finds a natural, already-fitting declaration shape.
Tests:
- Reuse `Spec114/OpsTriageActionsTest.php`, `Spec113/AuthorizationSemanticsTest.php`, `FindingsLifecycleBackfillStartTest.php`, and `BreakGlassWorkspaceOwnerRecoveryTest.php`.
- Add `Spec195/SystemDirectoryResidualSurfaceTest.php` for the current weakest system-detail coverage.
### Phase C — Reclassify special workflows, selectors, landings, and dashboard shells
Goal: turn existing baseline exemptions into explicit closure decisions rather than historical placeholders.
Changes:
- Reclassify `BreakGlassRecovery` as `retired_no_longer_relevant` if it remains inaccessible and actionless; otherwise keep it as `intentional_exemption` with a security-flow reason category.
- Classify `ChooseWorkspace` and `ChooseTenant` as `harmless_special_case` routing surfaces.
- Classify `RegisterTenant` as `separately_governed` because its mutation path, authorization, and bootstrap audit behavior already have focused coverage.
- Keep `ManagedTenantOnboardingWizard` as `separately_governed` and explicitly bind it to Spec 172 plus onboarding/RBAC/audit suites.
- Classify `ManagedTenantsLanding` explicitly and add focused coverage because it currently has the weakest dedicated test evidence among the special surfaces.
- Classify `TenantDashboard` as a `harmless_special_case` page shell or a light `separately_governed` shell, while leaving widget-level governance to the existing widget and arrival-context tests.
Tests:
- Reuse chooser, registration, onboarding, and dashboard tests already in `Auth`, `Workspaces`, `Rbac`, `Onboarding`, and `Filament` suites.
- Add `Spec195ManagedTenantsLandingTest.php` if current routing coverage is insufficiently explicit for the closure inventory.
### Phase D — Final guard hardening and verification flow
Goal: ensure new residual surfaces cannot appear silently after Spec 195 lands.
Changes:
- Fail CI when a new residual surface in the relevant namespaces is action-bearing but has no Spec 195 closure inventory entry.
- Fail CI when a discovered baseline exemption lacks a reason category or explicit evidence reference in the Spec 195 inventory.
- Fail CI when a retired or no-longer-relevant surface still keeps a live baseline exemption.
- Run focused verification through Sail and format touched files with Pint.
Tests:
- New residual closure guard plus the focused reused suites above.
- No full test suite is required to complete the planning phase, but the implementation quickstart defines the minimum targeted verification pack.
## Risk Assessment
| Risk | Impact | Likelihood | Mitigation |
|------|--------|------------|------------|
| Residual closure turns into a second UI framework | Medium | Low | Keep the solution to one derived inventory plus validator checks and focused tests. |
| Old exemptions survive with new labels only | High | Medium | Require explicit reason categories, explicit reasons, structured evidence, and stale-entry cleanup in the guard. |
| Special workflows are over-normalized into the wrong contract | Medium | Medium | Default special workflows to `separately_governed` or `harmless_special_case` unless the existing declaration model already fits. |
| System detail pages remain invisible to reviewers because discovery still skips them | High | Medium | Add explicit residual inventory entries and validator assertions for all out-of-discovery targets. |
| ManagedTenantsLanding remains weakly covered and ambiguous | Medium | Medium | Add a focused Spec 195 landing test and explicit classification. |
## Test Strategy
- Extend `ActionSurfaceContractTest.php` and `ActionSurfaceValidatorTest.php` so Spec 195 becomes an explicit CI-enforced rule instead of an informal review note.
- Add `Spec195ResidualActionSurfaceClosureGuardTest.php` to validate closure completeness, reason-category presence, discovery-state truth, and stale-exemption cleanup.
- Reuse existing system triage, runbook, break-glass, chooser, registration, onboarding, and dashboard tests as named coverage evidence for separately governed or harmless surfaces.
- Add only the minimum new targeted tests needed for current coverage gaps, expected to be `SystemDirectoryResidualSurfaceTest.php` and `Spec195ManagedTenantsLandingTest.php`.
- Keep all verification through Sail and run Pint after focused tests.
## Constitution Check (Post-Design)
Re-check result: PASS.
- Livewire v4.0+ compliance remains intact because all touched surfaces stay inside the existing Filament v5 + Livewire v4 stack.
- Provider registration remains unchanged in `bootstrap/providers.php`.
- Global search behavior is unchanged because no searchable resource is added or modified.
- Destructive and recovery actions keep `->requiresConfirmation()` plus current authorization and audit behavior.
- No new assets are introduced; existing `filament:assets` deployment behavior remains sufficient.

View File

@ -0,0 +1,160 @@
# Quickstart: Action Surface Enforcement, Enrollment, and Exception Closure
## Goal
Implement Spec 195 by making every residual action-bearing surface explicitly reviewable without widening the generic action-surface runtime contract.
## Implementation Sequence
### 1. Add the Spec 195 residual closure inventory
Touch:
- `apps/platform/app/Support/Ui/ActionSurface/ActionSurfaceExemptions.php`
- `apps/platform/app/Support/Ui/ActionSurface/ActionSurfaceValidator.php`
Do:
- Add `spec195ResidualSurfaceInventory()` with one entry per residual target surface.
- Add validator support for:
- allowed closure decisions
- allowed reason categories
- required structured evidence descriptors
- duplicate key detection
- truthful `discoveryState`
- stale baseline-exemption detection
Do not:
- auto-discover every system page
- introduce new persistence
- add a new runtime resolver or registry outside the existing action-surface support layer
### 2. Close the non-discovered system/detail surfaces explicitly
Audit and classify:
- `App\Filament\System\Pages\Dashboard`
- `App\Filament\System\Pages\Ops\ViewRun`
- `App\Filament\System\Pages\Ops\Runbooks`
- `App\Filament\System\Pages\RepairWorkspaceOwners`
- `App\Filament\System\Pages\Directory\ViewTenant`
- `App\Filament\System\Pages\Directory\ViewWorkspace`
Expected direction:
- `Dashboard`, `ViewRun`, `Runbooks`, `RepairWorkspaceOwners` => `separately_governed`
- `ViewTenant`, `ViewWorkspace` => `harmless_special_case` if they remain read-mostly contextual drilldowns
Only move one of these pages into `actionSurfaceDeclaration()` if the implementation audit shows it already fits the existing declaration-backed list/detail model naturally.
### 3. Reclassify the currently baseline-exempt special pages
Audit and classify:
- `App\Filament\Pages\BreakGlassRecovery`
- `App\Filament\Pages\ChooseWorkspace`
- `App\Filament\Pages\ChooseTenant`
- `App\Filament\Pages\Tenancy\RegisterTenant`
- `App\Filament\Pages\Workspaces\ManagedTenantOnboardingWizard`
- `App\Filament\Pages\Workspaces\ManagedTenantsLanding`
- `App\Filament\Pages\TenantDashboard`
Expected direction:
- `BreakGlassRecovery` => retire if it remains inaccessible and actionless; otherwise keep it as `intentional_exemption` with `security_flow_exception`
- `ChooseWorkspace`, `ChooseTenant` => `harmless_special_case`
- `RegisterTenant`, `ManagedTenantOnboardingWizard` => `separately_governed`
- `ManagedTenantsLanding` => explicit closure plus focused test
- `TenantDashboard` => `harmless_special_case` for the page shell while widget behavior stays governed by existing tests
### 4. Add CI regression protection
Touch:
- `tests/Feature/Guards/ActionSurfaceContractTest.php`
- `tests/Feature/Guards/ActionSurfaceValidatorTest.php`
- `tests/Feature/Guards/Spec195ResidualActionSurfaceClosureGuardTest.php`
Do:
- fail when a residual target has no closure entry
- fail when a non-enrolled residual target has no reason category
- fail when a discovered residual page remains in `baseline()` but has no matching Spec 195 inventory entry
- fail when a retired residual surface still keeps a baseline exemption
### Reviewer Classification Workflow
Use this path whenever an audit uncovers a residual page that is not already in the seed inventory.
1. Identify whether the page is inside primary discovery or outside it.
2. Add or update the residual inventory entry with `surfaceKey`, `surfaceName`, `pageClass`, `panelPlane`, `surfaceKind`, `discoveryState`, `closureDecision`, `reasonCategory` where relevant, `explicitReason`, structured `evidence`, and `followUpAction`.
3. If the page legitimately fits the generic contract, extend the existing contract tests instead of inventing a new local rule.
4. If the page remains separate, harmless, exempt, or retired, add or update focused guard and page-level evidence before merge.
5. Run the focused verification pack and confirm the reviewer can classify the page from guard output alone.
6. Treat the validator failure output as part of the workflow: missing Spec 195 entries should name the class and the concrete file path reviewers must classify next.
### 5. Fill current coverage gaps only where needed
Likely new tests:
- `tests/Feature/System/Spec195/SystemDirectoryResidualSurfaceTest.php`
- `tests/Feature/Workspaces/Spec195ManagedTenantsLandingTest.php`
Reuse existing evidence instead of duplicating it for:
- `ViewRun`
- `Runbooks`
- `RepairWorkspaceOwners`
- chooser flows
- tenant registration
- onboarding wizard
- tenant dashboard shell
## Suggested Test Pack
Run the minimum targeted verification pack through Sail.
```bash
export PATH="/bin:/usr/bin:/usr/local/bin:$PATH"
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Guards/ActionSurfaceContractTest.php
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Guards/ActionSurfaceValidatorTest.php
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Guards/Spec195ResidualActionSurfaceClosureGuardTest.php
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Guards/Spec194GovernanceActionSemanticsGuardTest.php
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Guards/LivewireTrustedStateGuardTest.php
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Guards/FilamentTableStandardsGuardTest.php
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/System/Spec113/AuthorizationSemanticsTest.php
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/System/Spec114/OpsTriageActionsTest.php
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/System/OpsRunbooks/FindingsLifecycleBackfillStartTest.php
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Auth/BreakGlassWorkspaceOwnerRecoveryTest.php
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Auth/TenantChooserSelectionTest.php
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Workspaces/ChooseWorkspacePageTest.php
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Workspaces/WorkspaceAuditTrailTest.php
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/TenantRBAC/TenantSwitcherScopeTest.php
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Rbac/RegisterTenantAuthorizationTest.php
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/TenantRBAC/TenantBootstrapAssignTest.php
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Rbac/OnboardingWizardUiEnforcementTest.php
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Onboarding/OnboardingDraftAccessTest.php
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Filament/TenantDashboardDbOnlyTest.php
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Rbac/TenantDashboardArrivalContextVisibilityTest.php
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/System/Spec195/SystemDirectoryResidualSurfaceTest.php
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Workspaces/Spec195ManagedTenantsLandingTest.php
cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent
```
## Manual Review Checklist
1. Confirm that the primary discovery count does not grow unexpectedly when Spec 195 lands.
2. Confirm that every in-scope residual surface has exactly one closure decision.
3. Confirm that non-discovered system/detail pages are now reviewable through the residual inventory and guard output.
4. Confirm that discovered special pages still exempted from the generic contract now carry explicit reason categories, explicit reasons, and structured evidence.
5. Confirm that `BreakGlassRecovery` is either retired from active exemptions or explicitly kept as `intentional_exemption` with `security_flow_exception` and current evidence.
6. Confirm that the weakest surfaces, expected to be `ManagedTenantsLanding` and system directory detail pages, have explicit focused tests.
7. Confirm that any newly audited residual surface beyond the initial seed was added to the inventory, contract, and guard expectations.
## Deployment Notes
- No migration is expected.
- No provider registration change is expected.
- No new assets are expected.
- Existing `cd apps/platform && php artisan filament:assets` deploy handling remains sufficient but unchanged.

View File

@ -0,0 +1,103 @@
# Research: Action Surface Enforcement, Enrollment, and Exception Closure
## Decision: Preserve the current primary discovery boundary and close the gap with a supplemental residual inventory
### Rationale
`ActionSurfaceDiscovery` already has a clear shape: it discovers resources, relation managers, normal pages, and system pages only when those system pages are declaration-backed table surfaces. The real problem in Spec 195 is not that this boundary exists, but that the repo does not currently make the boundary explicit enough for residual system/detail/workflow surfaces that live outside it.
Keeping the primary discovery boundary stable avoids turning the generic contract into a catch-all framework and lets Spec 195 solve the actual problem: uncatalogued outliers.
### Alternatives considered
- Auto-discover every class under `app/Filament/System/Pages`: rejected because many of those pages are not list/detail contract surfaces and would force the generic contract into shapes it does not currently model well.
- Expand `ActionSurfaceDiscovery` to scan every wizard, dashboard, and selector page in all namespaces: rejected because it would blur the difference between generic contract coverage and legitimate special workflows.
## Decision: Add a parallel `spec195ResidualSurfaceInventory()` instead of refactoring `baseline()` or stretching `ActionSurfaceDeclaration()`
### Rationale
The existing `baseline()` API is a simple string-reason allowlist for discovered pages that intentionally lack declarations. It is useful, but too narrow for Spec 195 because Spec 195 must also classify non-discovered system/detail pages and distinguish closure outcomes like `separately_governed`, `harmless_special_case`, and `retired_no_longer_relevant`.
The narrowest solution is to add one parallel inventory specifically for residual closure. That keeps the current baseline exemption behavior stable while giving the validator the structured data it needs.
### Alternatives considered
- Change `baseline()` into a structured object registry: rejected because it would create avoidable churn in existing tests and validator behavior for a problem that only Spec 195 needs to solve.
- Encode the entire Spec 195 closure model inside `ActionSurfaceDeclaration()`: rejected because many residual surfaces are not natural declaration-backed list/detail surfaces.
## Decision: Represent closure decisions and reason categories as validator-checked strings, not new PHP enums or persisted data
### Rationale
Spec 195 adds review and CI truth, not product-domain behavior. The closure states matter for planning and enforcement, but they do not need to become persisted entities or first-class runtime business state.
Using validator-checked string values in the inventory mirrors the existing Spec 192 and Spec 193 inventory style and avoids adding new runtime types whose only purpose would be internal categorization.
### Alternatives considered
- Add new PHP enums for closure decisions and reason categories: rejected because the validator can enforce the allowed string values without importing extra runtime structure.
- Persist residual closure rows in the database: rejected because this is repository governance truth, not user-facing data truth.
## Decision: Default residual system pages to `separately_governed` or `harmless_special_case` instead of forcing generic contract enrollment
### Rationale
The system residuals called out by the spec do not currently behave like the declaration-backed table/resource surfaces that Specs 192 and 193 govern. `ViewRun` is a system decision detail page, `Runbooks` is a workflow utility hub, `RepairWorkspaceOwners` is a break-glass repair utility, and the directory detail pages are read-mostly context pages.
Existing focused tests already exercise many of these surfaces directly. The narrowest correct implementation is therefore explicit classification plus explicit evidence, not generic normalization for its own sake.
### Alternatives considered
- Enroll `ViewRun`, `Runbooks`, and `RepairWorkspaceOwners` into the current `actionSurfaceDeclaration()` system immediately: rejected because the current contract is list/detail-slot oriented and would fit some of these surfaces awkwardly.
- Leave the system pages uncatalogued because dedicated tests already exist: rejected because that is exactly the gray zone Spec 195 exists to close.
## Decision: Use existing focused test suites as closure evidence and add only gap tests
### Rationale
The repo already has strong dedicated coverage for `Runbooks`, `ViewRun`, `RepairWorkspaceOwners`, registration, choosers, onboarding, and dashboard behavior. Spec 195 should leverage that fact instead of duplicating equivalent tests under a new surface framework.
The only clear weak spot from current evidence is `ManagedTenantsLanding`, and system directory detail pages also need more explicit closure-level assertions than they have today.
### Alternatives considered
- Add one brand-new comprehensive browser suite over every residual surface: rejected because many of these surfaces are already deeply covered through feature or Livewire tests.
- Add only inventory validation with no page-level follow-up: rejected because the weakest residuals still need focused proof.
## Decision: Treat `BreakGlassRecovery` as a stale-exemption candidate rather than assuming it is still an active governed surface
### Rationale
The current `BreakGlassRecovery` page has `canAccess()` returning false and no header actions. That is strong evidence that it may no longer be an action-bearing surface in the way the old baseline exemption reason implies.
Spec 195 should explicitly verify whether it is still a live residual surface. If not, it should be retired from the active exemption set instead of remaining as historical noise.
### Alternatives considered
- Keep the existing exemption reason unchanged because it references dedicated security specs: rejected because the current code suggests the page may no longer be a live action surface.
- Force the page into the residual inventory as a live intentional exemption without re-auditing the code: rejected because stale exemptions are one of the specs explicit problems.
## Decision: Keep selectors and dashboard shells outside the generic contract but classify them explicitly
### Rationale
`ChooseWorkspace`, `ChooseTenant`, and `TenantDashboard` are real operator surfaces, but they are not contract-style list/detail pages in the sense governed by the earlier specs. Selectors are routing surfaces; the dashboard page is a shell whose meaningful actions live in widgets and downstream routes.
Spec 195 should classify them explicitly so they are no longer invisible to review, while still avoiding artificial normalization.
### Alternatives considered
- Treat selectors and dashboards as non-surfaces and ignore them: rejected because they clearly influence operator workflows and currently appear in the residual exemption tail.
- Enroll them in the generic contract anyway: rejected because the generic contract is not the right fit for routing-only or widget-shell surfaces.
## Decision: No new provider, asset, route, or persistence work is needed
### Rationale
All evidence points to Spec 195 being a repository-governance and test-hardening slice. The existing pages, routes, panels, and tests already provide the runtime behavior. The missing part is explicit closure inventory and regression enforcement.
### Alternatives considered
- Add a new service provider or config file just for residual-surface closure: rejected because the existing action-surface support layer already provides the correct home.
- Add new assets or UI primitives for special surfaces: rejected because the implementation does not need new rendering infrastructure.

View File

@ -0,0 +1,323 @@
# Feature Specification: Action Surface Enforcement, Enrollment, and Exception Closure
**Feature Branch**: `195-action-surface-closure`
**Created**: 2026-04-12
**Status**: Proposed
**Input**: User description: "Spec 195 - Action Surface Enforcement, Enrollment & Exception Closure"
## Spec Candidate Check *(mandatory — SPEC-GATE-001)*
- **Problem**: After Specs 192, 193, and 194, the remaining action-bearing system, utility, flow, landing, and special workflow surfaces are not all clearly inside one reviewable regime. Some are covered by the generic action-surface contract, some are protected by focused rules elsewhere, and some still sit in historical gray zones.
- **Today's failure**: Reviewers cannot always tell whether a residual surface is intentionally enrolled, intentionally exempt, separately governed, retired, or simply missed by discovery. This leaves room for silent outliers and historical exemptions to bypass the central guard.
- **User-visible improvement**: Operators and reviewers get a clean closure state for every remaining residual surface. Sensitive system and utility actions stop living in undocumented gray zones, while already good special surfaces remain allowed without being silently outside discipline.
- **Smallest enterprise-capable version**: Build one residual inventory, assign exactly one closure decision to every remaining outlier, harden discovery and exemption boundaries, and add lightweight regression protection so new outliers cannot appear silently.
- **Explicit non-goals**: No new header hierarchy rules, no new monitoring semantics, no new governance-friction taxonomy beyond what Spec 194 already owns, no universal dashboard or widget framework, no large UI redesign, and no blanket removal of all exemptions without surface-by-surface review.
- **Permanent complexity imported**: A small closure-decision vocabulary, explicit exemption reason categories, a reviewable residual inventory, clearer discovery boundaries, and focused regression tests.
- **Why now**: The UX and action semantics block is already defined in Specs 192 to 194. What remains is the technical governance closure needed so the repo can stop carrying silent edge cases.
- **Why not local**: Local cleanup on one page at a time would not prevent future surfaces, hidden discovery gaps, or inherited exemptions from drifting outside the contract again.
- **Approval class**: Core Enterprise
- **Red flags triggered**: Cross-surface governance breadth risk and taxonomy creep risk. Defense: the spec is explicitly limited to residual closure, does not introduce a new product workflow, and preserves legitimate separately governed surfaces instead of forcing uniformity.
- **Score**: Nutzen: 2 | Dringlichkeit: 2 | Scope: 2 | Komplexitaet: 1 | Produktnaehe: 2 | Wiederverwendung: 2 | **Gesamt: 11/12**
- **Decision**: approve
## Spec Scope Fields *(mandatory)*
- **Scope**: canonical-view
- **Primary Routes**:
- Existing system operations run detail and runbooks surfaces
- Existing system repair utilities for workspace owner recovery
- Existing system directory tenant and workspace detail surfaces
- Existing break-glass recovery, chooser, and registration flows
- Existing managed-tenant onboarding, landing, and dashboard surfaces
- Any residual surface still represented in the current action-surface discovery, exemption, or guard inventory after Specs 192 to 194
- **Data Ownership**:
- This feature introduces no new domain tables, records, or business entities.
- Existing tenant-owned, workspace-owned, and system-visible records remain owned exactly as they are today.
- Closure decisions, exemption reasons, and discovery boundaries are review and enforcement truth only; they do not introduce a new user-facing data model.
- **RBAC**:
- Tenant admin surfaces continue to require tenant membership plus the existing tenant-scoped capabilities.
- Workspace admin surfaces continue to require workspace membership plus the existing workspace-scoped capabilities.
- System surfaces continue to require platform or system capabilities.
- This feature does not widen access; it only forces explicit governance classification for residual action-bearing surfaces.
For canonical-view specs, the spec MUST define:
- **Default filter behavior when tenant-context is active**: Tenant-context surfaces remain strictly bound to the active tenant. Workspace surfaces may retain an entitled tenant context as a quiet scope signal or filter, but the closure decision for a surface must never depend on a hidden tenant context. System surfaces never inherit tenant context as an authorization expansion.
- **Explicit entitlement checks preventing cross-tenant leakage**: Discovery, enrollment, or exemption review may inventory surfaces across tenant, workspace, and system planes, but any enrolled or separately governed surface must continue to use the current deny-as-not-found and capability checks. Review artifacts must classify surfaces without exposing inaccessible tenant or workspace detail.
## 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 |
|---|---|---|---|---|---|---|---|
| System Ops ViewRun | Primary Decision Surface | Decide whether a system run needs retry, cancellation, investigation, or only inspection | Run identity, current outcome, intervention availability, and safety context | Runbooks, deep diagnostics, related operational history | Primary because this is the system-level intervention point for one active run | Follows operations triage, not generic record browsing | Removes ambiguity about whether run interventions are governed or accidental utilities |
| System Ops Runbooks | Secondary Context Surface | Choose an approved operational response path after diagnosis | Available runbook choices, current system scope, and whether action launch is allowed | Procedure detail and historical execution context | Not primary because it supports a later intervention decision instead of owning the first diagnosis | Follows guided operational response | Prevents runbook launch actions from living as undocumented side utilities |
| Repair Workspace Owners | Primary Decision Surface | Repair a broken ownership state such as missing or duplicate ownership | Workspace identity, defect state, and the currently allowed repair | Membership evidence, prior repair history, and audit context | Primary because the surface exists to support a sensitive repair decision | Follows workspace recovery workflow | Prevents dangerous repair actions from living in historical exception space |
| System Directory tenant and workspace detail | Secondary Context Surface | Inspect system-level directory context and open the correct downstream surface | Scope identity, current state, and whether the page is read-only or action-bearing | Related downstream admin context and supporting diagnostics | Not primary because these pages are mostly inspection-first unless a small safe action exists | Follows inspection and routing workflow | Keeps read-mostly system detail pages calm while still classifying any light actions |
| Break Glass Recovery | Primary Decision Surface | Recover operator access during an exceptional lockout or recovery event | Recovery state, available recovery path, and safety warnings | Supporting diagnostics and deeper recovery evidence | Primary because the whole surface exists for a formal exceptional-access decision | Follows emergency access recovery workflow | Makes clear that this is a governed exception rather than an accidental bypass surface |
| ChooseWorkspace and ChooseTenant | Secondary Context Surface | Choose the correct scope before continuing work | Available scopes and why each scope is selectable | Downstream page detail only after selection | Not primary because selection is routing, not governance mutation | Follows scope entry workflow | Prevents selector surfaces from being silently ignored just because they are not record pages |
| RegisterTenant and ManagedTenantOnboardingWizard | Primary Decision Surface | Advance or complete tenant registration and onboarding safely | Current step, blocking prerequisites, and next required action | Supporting evidence, validation detail, and downstream setup context | Primary because the operator is making guided setup decisions step by step | Follows onboarding workflow | Keeps wizard exceptions explicit instead of forcing them into a record-page model |
| ManagedTenantsLanding and TenantDashboard | Secondary Context Surface | Open the right tenant or next task from a scoped landing surface | Scope summary, key status, and next safe navigation choice | Deeper evidence and operational detail only after drilldown | Not primary because these are arrival and routing surfaces, not the final decision point | Follows landing and context-setting workflow | Prevents dashboard and landing actions from remaining unclassified just because they are broad entry points |
## 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 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| System Ops ViewRun | Detail / Decision | System run triage detail | Retry, cancel, mark investigated, or open related runbook | Direct detail page for one run | forbidden | Supporting links and runbooks stay secondary or grouped by meaning | Cancel and other strongest interventions stay visibly separated and confirmed | Existing system operations collection route | Existing system run detail route | System scope, run state, intervention eligibility | Operations / system run | Whether intervention is possible now and which intervention is justified | Must end as enrolled or separately governed; no silent special case |
| System Ops Runbooks | Utility / Workflow hub | Guided system intervention utility | Open the appropriate runbook | Direct runbooks surface with explicit open action | forbidden | Supporting utilities stay grouped and subordinate to runbook choice | Any dangerous launch or escalation remains separated within the workflow | Existing system runbooks entry route | Same runbooks surface or guided runbook drilldown | System scope and current operational context | Runbooks / runbook | Which governed intervention paths are available | Separately governed is allowed if it is explicit and tested |
| Repair Workspace Owners | Utility / Repair | Sensitive repair utility | Repair missing or duplicate ownership state | Direct repair utility page | forbidden | Refresh and diagnostics stay quiet and secondary | Repair mutations remain isolated, confirmed, and capability-gated | Existing system repair utilities entry route | Same repair surface | Workspace identity and defect state | Workspace ownership repair | Whether a dangerous repair is warranted | Cannot remain a historical or implicit exemption |
| System Directory ViewTenant | Detail / Context | System directory tenant detail | Open related admin context or inspect state | Direct tenant detail page | forbidden | Context links remain secondary | No destructive action unless separately justified | Existing system directory tenant collection route | Existing system directory tenant detail route | System scope and tenant identity | Directory tenant | Whether the page is read-only, safe, or mutation-bearing | Harmless special case or separately governed if a light action remains |
| System Directory ViewWorkspace | Detail / Context | System directory workspace detail | Open related admin context or inspect state | Direct workspace detail page | forbidden | Context links remain secondary | No destructive action unless separately justified | Existing system directory workspace collection route | Existing system directory workspace detail route | System scope and workspace identity | Directory workspace | Whether the page is read-only, safe, or mutation-bearing | Harmless special case or separately governed if a light action remains |
| Break Glass Recovery | Workflow / Exception | Exceptional recovery workflow | Continue governed access recovery | Guided recovery flow | forbidden | Supporting diagnostics and help links stay secondary | Recovery mutations remain isolated and confirmed | Existing break-glass recovery entry route | Same recovery workflow route | Recovery state and access block reason | Break glass recovery | Whether safe recovery is available now | Separate governance is acceptable because this is an exceptional workflow |
| ChooseWorkspace and ChooseTenant | Workflow / Selector | Scope selection surface | Choose scope and continue | Row, tile, or explicit select action | required | Navigation and help stay secondary | none | Existing chooser route | Downstream chosen workspace or tenant route | Available scope only | Workspace chooser / tenant chooser | Which scopes are available and selectable | Intentional exemption or harmless special case is acceptable if explicitly recorded |
| RegisterTenant and ManagedTenantOnboardingWizard | Workflow / Wizard | Guided onboarding workflow | Continue the next required setup step | Step-based wizard progression | forbidden | Supporting validation and help stay secondary | Any irreversible setup step remains confirmed and authorized | Existing registration or onboarding entry route | Existing onboarding wizard route | Workspace or tenant scope, current step, prerequisite state | Tenant registration / onboarding | Which prerequisite blocks the next step | Separate governance is acceptable because the wizard already owns focused workflow rules |
| ManagedTenantsLanding and TenantDashboard | Landing / Dashboard | Scoped landing and routing surface | Open the next tenant or next task | Card, row, or explicit open action | allowed | Navigation shortcuts remain contextual and subordinate to scope summary | none unless a direct mutation exists and is explicitly governed | Existing managed-tenants landing or dashboard route | Downstream tenant detail, onboarding, or task route | Active workspace or tenant scope | Managed tenants / tenant dashboard | What needs attention next without pretending the landing is the final decision point | Separately governed or harmless special case, but never silent |
## 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 |
|---|---|---|---|---|---|---|---|---|---|---|
| System Ops ViewRun | Platform operator | Decide whether to intervene on one run | System run triage detail | Does this run need action now, and which action is justified? | Run identity, outcome, retryability, cancellation state, investigation need | Runbooks, linked runs, and deeper diagnostics | execution outcome, lifecycle attention, retryability | Existing run mutation scope only | Retry, Resume, Mark investigated when allowed | Cancel and equivalent hard interventions |
| System Ops Runbooks | Platform operator | Choose an approved intervention path | Guided system utility | Which runbook matches the current operational need? | Runbook choices and current context | Procedure detail and downstream execution evidence | intervention readiness, operational context | Existing system utility scope only | Open runbook or start guided intervention | Any launch that performs a strong intervention remains separated |
| Repair Workspace Owners | Platform or workspace recovery operator | Repair broken workspace ownership | Repair utility | Is the workspace ownership state broken enough to justify repair? | Missing-owner or duplicate-owner truth and currently allowed repair | Membership detail and prior recovery evidence | defect state, repair eligibility | TenantPilot only | Repair or merge actions when justified | All repair mutations are dangerous and confirmed |
| System Directory ViewTenant | Platform operator | Inspect system-level tenant context | Context detail | Is this a read-only inspection surface or a small safe context surface? | Tenant identity, state, and related context | Deeper downstream admin detail | lifecycle or presence state only if relevant | read-only unless a light action is explicitly allowed | Open related admin context | none by default |
| System Directory ViewWorkspace | Platform operator | Inspect system-level workspace context | Context detail | Is this a read-only inspection surface or a small safe context surface? | Workspace identity, state, and related context | Deeper downstream admin detail | lifecycle or presence state only if relevant | read-only unless a light action is explicitly allowed | Open related admin context | none by default |
| Break Glass Recovery | Exceptional access operator | Recover access safely during lockout or recovery | Exceptional recovery workflow | Which recovery step is allowed now, and what risk does it carry? | Recovery state, available path, and safety warning | Deeper evidence and supporting diagnostics | recovery readiness, access state | TenantPilot only | Continue recovery or confirm recovery step | Any access-granting recovery mutation |
| ChooseWorkspace and ChooseTenant | Operator entering scope | Choose the correct scope and continue | Selector surface | Which scope can I enter now? | Available scopes and selection affordance | none until downstream navigation | scope availability only | no mutation beyond selection | Select scope | none |
| RegisterTenant and ManagedTenantOnboardingWizard | Workspace operator | Advance registration and onboarding | Guided onboarding workflow | What step is blocked, and what must I do next? | Current step, missing prerequisite, next allowed action | Supporting validation detail | prerequisite readiness, onboarding progress | TenantPilot only | Continue, validate, submit when allowed | Any irreversible registration or setup completion step |
| ManagedTenantsLanding and TenantDashboard | Workspace or tenant operator | Route into the right next task | Landing and dashboard surface | What needs attention next, and where should I open it? | Scope summary, next task, current state highlights | Deeper operational evidence after drilldown | readiness, attention state, lifecycle highlights | Usually read-only routing; explicit if any direct mutation exists | Open tenant, continue onboarding, open next task | none by default |
## Proportionality Review *(mandatory when structural complexity is introduced)*
- **New source of truth?**: no
- **New persisted entity/table/artifact?**: no
- **New abstraction?**: yes
- **New enum/state/reason family?**: yes
- **New cross-domain UI framework/taxonomy?**: yes
- **Current operator problem**: The repo still has a small but important tail of residual action-bearing surfaces where nobody can quickly tell whether the generic contract applies, whether a focused exception is legitimate, or whether a surface simply escaped discovery.
- **Existing structure is insufficient because**: Specs 192 to 194 define the behavior of governed surfaces, but they do not close the remaining gap between rulebook and discovery coverage. Without explicit closure decisions, historical exemptions and discovery limits can continue to create silent outliers.
- **Narrowest correct implementation**: Add one residual inventory, one explicit closure-decision matrix, minimal exemption reason categories, and lightweight regression guards. Do not create a new runtime workflow engine, persistence model, or broad UI framework.
- **Ownership cost**: Ongoing review of new residual surfaces, upkeep of explicit exemption reasons, a small amount of CI guard maintenance, and focused tests that prove closure decisions remain intentional.
- **Alternative intentionally rejected**: Pure page-by-page cleanup was rejected because it would leave discovery boundaries, exemptions, and future outliers unresolved. Blanket enrollment of every special surface was rejected because it would destroy legitimate separately governed patterns without improving safety.
- **Release truth**: current-release governance closure and regression prevention
## User Scenarios & Testing *(mandatory)*
### User Story 1 - Close the residual inventory (Priority: P1)
As a reviewer, I want every remaining action-bearing surface after Specs 192 to 194 to appear in one closure inventory with exactly one final decision, so no gray zone remains.
**Why this priority**: This is the core purpose of the spec. If any residual surface stays ambiguous, the closure is incomplete.
**Independent Test**: Review the residual inventory alone and verify that every listed surface has exactly one closure decision plus a rationale or coverage note.
**Acceptance Scenarios**:
1. **Given** the repo after Specs 192 to 194, **When** the residual inventory is reviewed, **Then** every in-scope residual surface appears exactly once.
2. **Given** an inventoried residual surface, **When** a reviewer checks its record, **Then** the surface is classified as enrolled, intentionally exempt, separately governed, retired, or harmless special case with no ambiguity.
---
### User Story 2 - Remove silent system and utility exceptions (Priority: P1)
As a platform operator, I want sensitive system and utility surfaces to have an explicit governance status, so dangerous actions do not live outside review discipline by accident.
**Why this priority**: The riskiest remaining residuals are not ordinary record pages. If these surfaces remain implicit exceptions, the main safety gap stays open.
**Independent Test**: Review the named high-risk system and utility surfaces and verify that each one is either enrolled in the generic contract or explicitly handled as a justified separate case.
**Acceptance Scenarios**:
1. **Given** a sensitive residual system or utility surface, **When** its closure decision is reviewed, **Then** the surface no longer relies on an implied or historical exemption.
2. **Given** a residual surface stays outside the generic contract, **When** the reviewer checks why, **Then** the reason and separate coverage are explicit and reviewable.
---
### User Story 3 - Keep only justified exemptions (Priority: P2)
As a code reviewer, I want every remaining exemption to carry a reason category and coverage note, so I can distinguish a justified special case from leftover drift.
**Why this priority**: Exemptions are acceptable only when they are conscious, minimal, and reviewable.
**Independent Test**: Review the exemption list alone and confirm that each remaining exemption has a short reason category, a clear closure decision, and a note about where its safety or behavior is covered.
**Acceptance Scenarios**:
1. **Given** an existing exemption entry, **When** it is reviewed under Spec 195, **Then** it either gains an explicit reason category and coverage note or it is removed.
2. **Given** an exemption no longer reflects a real residual surface, **When** the closure pass is complete, **Then** that exemption no longer remains in the active inventory.
---
### User Story 4 - Block future unclassified residuals (Priority: P2)
As a future implementer, I want a lightweight guard and review path that fail fast when a new residual surface or exemption appears without classification, so the repo cannot drift back into silent exceptions.
**Why this priority**: The spec only closes the block if it stays closed.
**Independent Test**: Add a representative new residual surface or exemption in a controlled test path and verify that review guidance or CI fails until a closure decision is supplied.
**Acceptance Scenarios**:
1. **Given** a newly added action-bearing residual surface, **When** it lacks enrollment or closure classification, **Then** the guard fails.
2. **Given** a newly added exemption entry, **When** it lacks a reason category or coverage note, **Then** the guard fails.
---
### User Story 5 - Preserve good special surfaces without forced churn (Priority: P3)
As a product reviewer, I want already well-governed special surfaces to remain outside the generic contract when that is the cleanest choice, so closure does not become needless uniformity.
**Why this priority**: The spec is about removing gray zones, not punishing legitimate special workflows.
**Independent Test**: Review an existing wizard, recovery workflow, or dashboard surface that already has focused coverage and verify that it can remain separately governed without being mislabeled as a defect.
**Acceptance Scenarios**:
1. **Given** a residual surface already covered by dedicated rules and focused tests, **When** Spec 195 is applied, **Then** the surface may remain separately governed with an explicit note.
2. **Given** a harmless read-mostly special case, **When** it is reviewed, **Then** it is recorded as such instead of being forced into the generic contract for aesthetic symmetry.
### Edge Cases
- A surface may expose only one apparently harmless action such as routing or light inspection; it still must be explicitly classified and cannot stay outside the inventory by assumption alone.
- A wizard, landing, or dashboard may live outside the default discovery path; if it is action-bearing, the discovery boundary must still explain how it is classified.
- A historical exemption may point to a surface that is no longer action-bearing or no longer exists; it must be retired instead of remaining as dead noise.
- A read-mostly system detail surface may later gain a mutating action; the regression guard must force a new closure review at that moment.
- A surface may appear to fit multiple closure categories; the final inventory must still choose exactly one category.
- A surface may be separately governed because its actions already have focused tests; that must be recorded explicitly rather than inferred from institutional memory.
## Requirements *(mandatory)*
**Constitution alignment (required):** This feature introduces no new Microsoft Graph contract, no new user-facing workflow domain, and no new persisted truth. It classifies and hardens existing action-bearing surfaces and their review path. Existing write actions keep their current preview, confirmation, audit, and run-observability behavior. Any sensitive DB-only repair or recovery action that intentionally skips `OperationRun` must remain auditable and explicitly recorded as separately governed or intentionally exempt.
**Constitution alignment (PROP-001 / ABSTR-001 / PERSIST-001 / STATE-001 / BLOAT-001):** This feature deliberately adds only the smallest cross-cutting layer required now: a residual closure inventory, a closure-decision vocabulary, explicit exemption reason categories, and regression guards. It does not add new persistence, a generic action framework, or new business states.
**Constitution alignment (OPS-UX):** Existing actions that already create or reuse `OperationRun` keep their current run lifecycle and notification semantics. Spec 195 only requires that any residual surface remaining outside the generic contract still points to focused run or audit coverage where relevant, so separate governance is never a trust gap.
**Constitution alignment (RBAC-UX):** The affected authorization planes are workspace admin `/admin`, tenant-context admin `/admin/t/{tenant}/...`, and platform `/system`. Non-members or users lacking entitled scope remain `404`, members lacking capability remain `403`, and moving a surface into enrolled, exempt, or separately governed status does not change server-side authorization. Destructive actions remain confirmed and capability-gated. Regression coverage must include at least one positive and one negative authorization path across residual surfaces touched by the closure pass.
**Constitution alignment (OPS-EX-AUTH-001):** Not applicable. This feature does not change authentication handshake behavior.
**Constitution alignment (BADGE-001):** This feature does not introduce a new badge domain. Existing badge semantics remain centralized. Any residual surface that displays status continues to use the current centralized status language.
**Constitution alignment (UI-FIL-001):** Any UI remediation under this spec continues to use native Filament pages, actions, grouped actions, and existing enforcement helpers. The feature must not introduce a local button framework, page-local danger language, or new styling vocabulary. Approved exceptions remain explicit special workflows such as wizards, recovery flows, or guided utilities.
**Constitution alignment (UI-NAMING-001):** Existing operator verbs such as `Retry`, `Cancel`, `Mark investigated`, `Repair`, `Merge`, `Recover access`, `Select`, `Register tenant`, and `Continue onboarding` must remain domain-first and consistent across buttons, confirmation copy, notifications, and audit prose. Closure categories such as enrolled or separately governed are review vocabulary, not new operator-facing nouns.
**Constitution alignment (DECIDE-001):** Every affected residual surface must declare whether it is a Primary Decision Surface, Secondary Context Surface, or a low-risk special surface. The first-decision information for each primary surface must remain visible by default, while diagnostics and deep evidence remain on demand. Separate governance is valid only when one operator task still remains clear inside that surface's own workflow.
**Constitution alignment (UI-CONST-001 / UI-SURF-001 / ACTSURF-001 / UI-HARD-001 / UI-EX-001 / UI-REVIEW-001 / HDR-001):** This spec classifies each residual surface by action-surface class, detailed surface type, likely next action, inspect model, row-click policy, action placement, canonical route family, scope signals, canonical noun, and exception rationale. Residual surfaces may differ by type, but none may remain uncatalogued.
**Constitution alignment (ACTSURF-001 - action hierarchy):** Where residual surfaces expose header, row, bulk, selector, or workflow actions, navigation, mutation, contextual signals, and dangerous actions must remain structurally separated. Any grouped action set must remain meaningful rather than a mixed catch-all. System, wizard, recovery, and dashboard exceptions are acceptable only when they are genuine workflow types, not convenience shortcuts.
**Constitution alignment (OPSURF-001):** Default-visible content on residual surfaces must stay operator-first. System and utility surfaces must show the current operational truth before asking the operator to act. Any mutating action must continue to communicate its scope before execution, and dangerous actions must keep the existing safe-execution pattern of context, safety checks, confirmation, and execution.
**Constitution alignment (UI-SEM-001 / LAYER-001 / TEST-TRUTH-001):** Direct mapping from page class to contract coverage is insufficient because residual surfaces can fall outside the generic discovery path. The new closure layer must therefore remain explicit and reviewable, but it must avoid duplicating runtime truth or creating a separate user-facing model. Tests should prove that residual surfaces are classified and guarded, not just that a thin registry exists.
**Constitution alignment (Filament Action Surfaces):** The Action Surface Contract must be explicitly marked as satisfied for any residual surface that enters the generic contract. Surfaces that remain outside it must cite an explicit exemption or separate-governance rationale. Each affected surface must still have one primary inspect or open model, no redundant View affordance, no empty grouped-action placeholder, and destructive actions that continue to use confirmed execution. UI-FIL-001 remains satisfied because the spec reuses native Filament surfaces and explicit exception handling rather than inventing new local primitives.
**Constitution alignment (UX-001 — Layout & Information Architecture):** This feature is a governance and closure spec, not a layout rewrite. Existing views, forms, wizards, and dashboards keep their current page composition unless a residual surface needs a small targeted alignment to meet the already established action-surface rules. No surface is rebuilt purely for visual symmetry.
### Closure Principles
1. **No silent residual surfaces**: Every action-bearing residual surface must be enrolled, intentionally exempt, separately governed, retired, or harmless.
2. **Exemption is a decision, not an accident**: An exemption is valid only when it is explicit, justified, minimal, and reviewable.
3. **Discovery boundaries must be explicit**: If the generic discovery path cannot see a surface class, that limit must be modeled and protected rather than treated as a repo accident.
4. **Separate coverage counts**: A surface may stay outside the generic contract if dedicated specs, tests, or focused guards already protect it sufficiently.
5. **No forced sameness**: Good special workflows do not need to be flattened into the generic contract when separate governance is the cleaner and safer answer.
6. **Closure over expansion**: This spec closes the remaining block after Specs 192 to 194; it does not open a new architecture program.
### Closure Decision Matrix
- **Generic contract enrollment**: The surface is brought into the generic action-surface contract and must satisfy the earlier rules directly.
- **Intentional exemption**: The surface stays outside the generic contract with a short, reviewable reason category.
- **Separately governed**: The surface stays outside the generic contract because focused specs, tests, or guards already govern it sufficiently.
- **Retired / no longer relevant**: The surface is no longer an active residual and must leave the live inventory.
- **Harmless special case**: The surface is action-bearing but small, low-risk, and intentionally classified as not needing the full generic contract.
### Functional Requirements
- **FR-195-001 Residual inventory**: The repo MUST maintain one complete inventory of every remaining action-bearing residual surface still outside clearly settled coverage from Specs 192 to 194.
- **FR-195-002 Exact closure decision**: Every inventoried residual surface MUST have exactly one closure decision: `generic contract enrollment`, `intentional exemption`, `separately governed`, `retired / no longer relevant`, or `harmless special case`.
- **FR-195-003 No gray-zone rule**: No action-bearing residual surface in scope MAY remain undocumented, unclassified, or implicitly outside governance.
- **FR-195-004 Residual review scope**: The closure pass MUST explicitly review the known residual areas, including system run detail, system runbooks, workspace-owner repair, system directory detail surfaces, break-glass recovery, chooser flows, registration and onboarding flows, landing surfaces, dashboard surfaces, and any equivalent residual surface represented in the current inventory or guard path.
- **FR-195-005 System and utility decision rule**: Every residual system or utility surface with mutating actions MUST be either enrolled in the generic contract or explicitly recorded as intentionally exempt or separately governed with clear rationale.
- **FR-195-006 Exemption minimization**: Historical or diffuse exemptions that no longer represent a justified residual surface MUST be removed or reclassified.
- **FR-195-007 Exemption reason categories**: Every remaining exemption MUST carry one short reason category explaining why enrollment is not the correct closure decision.
- **FR-195-008 Separate-governance recognition**: A residual surface MAY remain outside the generic contract only when the inventory explicitly records the dedicated spec, focused tests, or guard path that already govern it.
- **FR-195-009 Harmless special-case discipline**: A surface MAY be classified as a harmless special case only when its action scope is limited, its operational risk is low, and that judgment is explicit in the inventory.
- **FR-195-010 Discovery-boundary explicitness**: The generic discovery path MUST define its boundaries clearly enough that a reviewer can tell which surface classes are discovered automatically and which require supplemental enrollment or explicit outside-contract classification.
- **FR-195-011 No discovery accident**: A residual surface MUST NOT bypass governance solely because it lives in a system panel, wizard, selector, recovery flow, landing surface, dashboard, or another namespace outside the default discovery roots.
- **FR-195-012 System-panel closure**: Residual system-panel surfaces MUST explicitly declare whether they are decision surfaces, context surfaces, or separately governed utilities, and any dangerous actions on them MUST remain classified and reviewable.
- **FR-195-013 Flow and wizard closure**: Registration, onboarding, chooser, and recovery flows MAY remain outside the generic contract, but their closure decision and focused coverage MUST be explicit.
- **FR-195-014 Landing and dashboard closure**: Landing and dashboard surfaces with actions MUST be explicitly classified as enrolled, separately governed, or harmless; being broad or summary-oriented is not itself an exemption.
- **FR-195-015 Retired-surface cleanup**: If an exempted or inventoried surface is retired or no longer action-bearing, it MUST be removed from the live residual inventory so dead entries cannot mask real gaps.
- **FR-195-016 Review artifact completeness**: Each residual inventory entry MUST record the surface name, authorization plane, surface type, closure decision, reason category where relevant, and where its coverage lives.
- **FR-195-017 Guard against unclassified surfaces**: The repo MUST add or extend a lightweight guard that fails when a new action-bearing residual surface appears without an explicit closure decision.
- **FR-195-018 Guard against reasonless exemptions**: The repo MUST add or extend a lightweight guard that fails when a new exemption appears without a reason category and coverage note.
- **FR-195-019 Review path clarity**: Contributor and reviewer workflows MUST make it obvious how to classify a new residual surface before merge.
- **FR-195-020 No forced normalization**: Residual surfaces that are already safely separately governed MUST NOT be forced into the generic contract solely to reduce taxonomy variety.
- **FR-195-021 Contract continuity for enrolled surfaces**: Any residual surface moved into the generic contract MUST satisfy the already established contract from the earlier action-surface specs rather than introducing a new local rule set.
- **FR-195-022 Dedicated coverage proof**: Any surface marked intentionally exempt, separately governed, or harmless special case MUST have explicit rationale and enough focused coverage to make the decision reviewable in CI and code review.
- **FR-195-023 Authorization continuity**: Closure classification, discovery hardening, or exemption cleanup MUST NOT weaken route scope, capability enforcement, deny-as-not-found behavior, or destructive-action confirmation.
- **FR-195-024 Audit and safety continuity**: Residual destructive or recovery actions MUST remain auditable, confirmed, and safety-gated regardless of whether the surface is enrolled, exempt, or separately governed.
- **FR-195-025 Block completion**: After Spec 195, the residual action-surface block following Specs 192 to 194 MUST have no remaining action-bearing surface without a visible closure decision.
## Target Outcomes by Key Residual Area
- **System Ops ViewRun**: No longer a silent special case. It ends either as a generic-contract surface or as an explicitly separately governed system triage surface with focused coverage.
- **System Ops Runbooks**: Operational utilities are classified intentionally instead of existing as factual but ungoverned launch points.
- **Repair Workspace Owners**: A risky repair utility receives an explicit closure state and does not remain as an inherited historical exception.
- **System Directory tenant and workspace detail**: Read-mostly detail pages are explicitly classified as harmless, enrolled, or separately governed rather than left unreviewed.
- **Break Glass Recovery**: The exceptional-access workflow remains legitimate only if its special handling is explicit and reviewable.
- **Chooser, registration, onboarding, landing, and dashboard surfaces**: Every such surface receives a closure decision so routing and workflow entry points are no longer missing from the action-surface map.
## Non-Goals
- Creating a new fourth main rule set after Specs 192 to 194
- Redefining header hierarchy, monitoring semantics, or governance-friction classes
- Building a universal widget, dashboard, or workflow meta-contract
- Rewriting already calm surfaces for stylistic consistency alone
- Removing every exemption without checking whether the exemption is legitimate
- Introducing new business entities, new workflow states, or new operator concepts unrelated to closure
## Assumptions
- Specs 192, 193, and 194 remain the authoritative sources for surface behavior; Spec 195 only closes the residual governance gap around discovery, enrollment, exemptions, and regression protection.
- Some recovery, wizard, selector, and dashboard surfaces will remain outside the generic contract because they are inherently special workflow types.
- Existing action semantics, authorization logic, audit behavior, and run-observability rules on the underlying actions are already correct and will be preserved rather than redesigned here.
- The cleanest outcome is explicit closure, not universal enrollment.
## Dependencies
- Constitution rule set for Action Surface Discipline
- Spec 192 - Record Page Header Discipline and Contextual Navigation
- Spec 193 - Monitoring Surface Action Hierarchy and Workbench Semantics
- Spec 194 - Governance Friction Hardening and Operator Vocabulary
## 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 |
|---|---|---|---|---|---|---|---|---|---|---|
| System Ops ViewRun | Existing system run detail page | `Refresh`, `Retry` or `Resume` when applicable, `Open runbooks`, `Mark investigated`, `Cancel` when applicable | not applicable | none | none | not applicable | One run intervention may be primary if justified; strongest intervention stays separated | not applicable | yes for interventions | Must be enrolled or separately governed; no silent exemption |
| System Ops Runbooks | Existing system runbooks surface | `Open runbook`, guided intervention entry points, quiet navigation | Explicit open action or card selection | `Open runbook` | none | Context-specific single CTA if no runbooks apply | Same guided utility actions | not applicable | yes when a launch mutates state | Separate governance is acceptable if explicit and tested |
| Repair Workspace Owners | Existing system repair utility | `Refresh diagnosis`, `Repair owner state`, `Merge duplicate ownership` | not applicable | none | none | not applicable | Repair actions remain grouped and confirmed | not applicable | yes | High-risk utility; must not remain historical exception |
| System Directory ViewTenant and ViewWorkspace | Existing system directory detail pages | Quiet contextual links only unless a light safe action exists | not applicable | none | none | not applicable | Read-mostly contextual actions only | not applicable | no for read-only, yes if mutation exists | Candidate harmless special case or separate governance |
| Break Glass Recovery | Existing break-glass workflow | `Continue recovery`, `Confirm recovery step`, `Cancel` or equivalent safe exit | Guided step progression | none | none | Single recovery CTA when entering the flow | Recovery-step actions only | Wizard navigation rather than save/cancel | yes | Explicit separate governance expected |
| ChooseWorkspace and ChooseTenant | Existing selector surfaces | No competing header mutations; quiet back/help only | Row, tile, or select action is the primary inspect/open model | `Select` | none | Single CTA only when no accessible scopes are available | not applicable | not applicable | no | Intentional exemption or harmless special case acceptable if explicit |
| RegisterTenant and ManagedTenantOnboardingWizard | Existing registration and onboarding surfaces | `Continue`, `Validate`, `Submit`, `Cancel` as step-appropriate | Step progression inside the wizard | none | none | Single start CTA from entry state | Wizard step actions only | Save/continue and cancel where the workflow uses form steps | yes for mutating steps | Separate governance expected because the workflow already owns its path |
| ManagedTenantsLanding and TenantDashboard | Existing landing and dashboard surfaces | `Open tenant`, `Continue onboarding`, `Open next task` where present | Card, row, or explicit open affordance | One safe shortcut at most | none | One CTA when the landing is otherwise empty | Contextual navigation only | not applicable | usually no, unless a direct mutation exists | Must be explicitly classified as separately governed or harmless if not enrolled |
### Key Entities *(include if feature involves data)*
- **Residual Action Surface**: An operator-facing surface that still carries actions after Specs 192 to 194 and therefore requires an explicit closure decision.
- **Closure Decision**: The single final classification for a residual surface: generic contract enrollment, intentional exemption, separately governed, retired / no longer relevant, or harmless special case.
- **Exemption Reason Category**: A short explanation that makes an exemption reviewable and keeps it from becoming a catch-all for unresolved drift.
- **Discovery Boundary**: The explicit statement of which surface classes the generic discovery path can see automatically and which require supplemental handling.
## Success Criteria *(mandatory)*
### Measurable Outcomes
- **SC-001**: 100% of residual action-bearing surfaces remaining after Specs 192 to 194 are listed in the closure inventory with exactly one final closure decision.
- **SC-002**: 0 active exemption entries remain without a reason category and explicit coverage note.
- **SC-003**: The regression guard fails on every representative test case where a new residual surface or exemption is introduced without classification.
- **SC-004**: A reviewer can determine, from the inventory and focused coverage alone, whether any residual surface is enrolled, intentionally exempt, separately governed, retired, or harmless without reconstructing intent from code history.

View File

@ -0,0 +1,183 @@
---
description: "Task list for Spec 195 action-surface closure implementation"
---
# Tasks: Action Surface Enforcement, Enrollment, and Exception Closure
**Input**: Design documents from `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/195-action-surface-closure/`
**Prerequisites**: plan.md, spec.md, research.md, data-model.md, quickstart.md, contracts/action-surface-closure.logical.openapi.yaml
**Tests**: Runtime behavior changes in this repo require Pest coverage. This task list includes guard, feature, and focused residual-surface tests.
**Operations**: This feature does not add new long-running or queued work. Existing `OperationRun` and audit behavior on residual surfaces must remain unchanged.
**RBAC**: Existing 404 vs 403 semantics, capability checks, and destructive-action confirmations remain mandatory across the admin, tenant-context admin, and system planes.
**UI Naming**: No new operator-facing vocabulary is introduced; existing action copy remains domain-first and consistent.
**Operator Surfaces**: The affected surfaces are already classified in the spec and must be kept aligned with those classifications during implementation.
**Filament UI Action Surfaces**: This feature governs existing Filament pages and utilities without introducing a new page framework.
**Proportionality / Anti-Bloat**: Keep the implementation to one bounded residual inventory, validator checks, and focused tests. Do not add new persistence, enums, or runtime registries unless the implementation proves they are unavoidable.
## Phase 1: Setup (Shared Review Inputs)
**Purpose**: Confirm the current residual-surface scope and evidence sources before editing the shared support layer.
- [X] T001 Audit the current primary discovery and exemption entry points in `apps/platform/app/Support/Ui/ActionSurface/ActionSurfaceDiscovery.php`, `apps/platform/app/Support/Ui/ActionSurface/ActionSurfaceExemptions.php`, and `apps/platform/app/Support/Ui/ActionSurface/ActionSurfaceValidator.php`
- [X] T002 [P] Audit the existing focused evidence sources for residual surfaces in `apps/platform/tests/Feature/System/Spec114/OpsTriageActionsTest.php`, `apps/platform/tests/Feature/System/OpsRunbooks/FindingsLifecycleBackfillStartTest.php`, `apps/platform/tests/Feature/Auth/BreakGlassWorkspaceOwnerRecoveryTest.php`, `apps/platform/tests/Feature/Auth/TenantChooserSelectionTest.php`, `apps/platform/tests/Feature/Rbac/RegisterTenantAuthorizationTest.php`, `apps/platform/tests/Feature/Rbac/OnboardingWizardUiEnforcementTest.php`, and `apps/platform/tests/Feature/Filament/TenantDashboardDbOnlyTest.php`
---
## Phase 2: Foundational (Blocking Prerequisites)
**Purpose**: Add the shared Spec 195 inventory and validation scaffolding that all user stories depend on.
**⚠️ CRITICAL**: No user story work should begin until this phase is complete.
- [X] T003 [P] Add shared residual-inventory schema assertions for Spec 195 in `apps/platform/tests/Feature/Guards/ActionSurfaceValidatorTest.php`
- [X] T004 [P] Add shared contract-harness expectations for Spec 195 residual inventory consumption in `apps/platform/tests/Feature/Guards/ActionSurfaceContractTest.php`
- [X] T005 Implement the `spec195ResidualSurfaceInventory()` skeleton, required `surfaceName` field, and allowed closure vocabulary in `apps/platform/app/Support/Ui/ActionSurface/ActionSurfaceExemptions.php`
- [X] T006 Implement a validator-side residual-candidate boundary helper that preserves the existing primary discovery contract in `apps/platform/app/Support/Ui/ActionSurface/ActionSurfaceValidator.php`
- [X] T007 Extend `apps/platform/app/Support/Ui/ActionSurface/ActionSurfaceValidator.php` with reusable Spec 195 schema validation for allowed values, duplicate keys, evidence presence, and truthful discovery state
**Checkpoint**: Shared residual inventory and validator scaffolding are ready; user story work can now proceed.
---
## Phase 3: User Story 1 - Close the residual inventory (Priority: P1) 🎯 MVP
**Goal**: Make every in-scope residual surface appear exactly once with one closure decision, reason category where needed, and explicit evidence.
**Independent Test**: Review the final residual inventory and guard output alone and verify that the initial seed plus any additional audited residual surfaces have exactly one decision, no duplicates, and no missing evidence.
### Tests for User Story 1
- [X] T008 [P] [US1] Add completeness expectations for all Spec 195 residual surface keys in `apps/platform/tests/Feature/Guards/ActionSurfaceValidatorTest.php`
- [X] T009 [P] [US1] Add contract assertions for unique closure decisions, structured evidence descriptors, and baseline-alignment expectations in `apps/platform/tests/Feature/Guards/ActionSurfaceContractTest.php`
### Implementation for User Story 1
- [X] T010 [US1] Populate the initial seed and any newly audited Spec 195 residual closure entries with `surfaceName`, discovery state, closure decision, reason category, `explicitReason`, structured `evidence`, and `followUpAction` in `apps/platform/app/Support/Ui/ActionSurface/ActionSurfaceExemptions.php`
- [X] T011 [US1] Wire Spec 195 residual inventory consumption into `apps/platform/app/Support/Ui/ActionSurface/ActionSurfaceValidator.php` so every residual surface is checked exactly once
**Checkpoint**: User Story 1 is complete when the repo has a single complete Spec 195 inventory and its completeness is guard-tested.
---
## Phase 4: User Story 2 - Remove silent system and utility exceptions (Priority: P1)
**Goal**: Explicitly classify the currently uncatalogued system and utility surfaces so dangerous or decision-bearing system pages no longer live outside review discipline.
**Independent Test**: Review the system residual entries plus focused system tests and verify that `ViewRun`, `Runbooks`, `RepairWorkspaceOwners`, `ViewTenant`, and `ViewWorkspace` all have explicit closure states and no silent fallback path.
### Tests for User Story 2
- [X] T012 [P] [US2] Add residual guard coverage for `App\Filament\System\Pages\Ops\ViewRun`, `App\Filament\System\Pages\Ops\Runbooks`, and `App\Filament\System\Pages\RepairWorkspaceOwners` in `apps/platform/tests/Feature/Guards/Spec195ResidualActionSurfaceClosureGuardTest.php`
- [X] T013 [P] [US2] Add focused read-mostly closure coverage for `App\Filament\System\Pages\Directory\ViewTenant` and `App\Filament\System\Pages\Directory\ViewWorkspace` in `apps/platform/tests/Feature/System/Spec195/SystemDirectoryResidualSurfaceTest.php`
### Implementation for User Story 2
- [X] T014 [US2] Audit `apps/platform/app/Filament/System/Pages/Ops/ViewRun.php`, `apps/platform/app/Filament/System/Pages/Ops/Runbooks.php`, and `apps/platform/app/Filament/System/Pages/RepairWorkspaceOwners.php` and, if implementation reality differs from the design seed, update the matching entries in `apps/platform/app/Support/Ui/ActionSurface/ActionSurfaceExemptions.php`, `apps/platform/app/Support/Ui/ActionSurface/ActionSurfaceValidator.php`, and `apps/platform/tests/Feature/Guards/Spec195ResidualActionSurfaceClosureGuardTest.php`
- [X] T015 [US2] Audit `apps/platform/app/Filament/System/Pages/Directory/ViewTenant.php` and `apps/platform/app/Filament/System/Pages/Directory/ViewWorkspace.php` and, if the pages are not truly read-mostly harmless cases, update the matching entries in `apps/platform/app/Support/Ui/ActionSurface/ActionSurfaceExemptions.php`, `apps/platform/app/Support/Ui/ActionSurface/ActionSurfaceValidator.php`, and `apps/platform/tests/Feature/System/Spec195/SystemDirectoryResidualSurfaceTest.php`
- [X] T016 [US2] Tighten `apps/platform/app/Support/Ui/ActionSurface/ActionSurfaceValidator.php` so outside-primary-discovery system and utility pages cannot pass without Spec 195 closure data and, if any audited system page is promoted into generic contract enrollment, extend `apps/platform/tests/Feature/Guards/ActionSurfaceContractTest.php` and `apps/platform/tests/Feature/Guards/ActionSurfaceValidatorTest.php` to enforce inherited contract continuity
**Checkpoint**: User Story 2 is complete when the system/detail residual tail is explicitly classified and validated instead of relying on discovery accidents.
---
## Phase 5: User Story 3 - Keep only justified exemptions (Priority: P2)
**Goal**: Remove stale exemptions and require every remaining discovered exception to carry an explicit reason category and coverage note.
**Independent Test**: Review the discovered-page exemption set alone and verify that every remaining entry has a reason category, explicit reason, structured evidence, and no stale `BreakGlassRecovery` carry-over.
### Tests for User Story 3
- [X] T017 [P] [US3] Add stale-exemption and reason-category failure cases in `apps/platform/tests/Feature/Guards/Spec195ResidualActionSurfaceClosureGuardTest.php`
- [X] T018 [P] [US3] Add stale-exemption and baseline-removal assertions for `apps/platform/app/Filament/Pages/BreakGlassRecovery.php` in `apps/platform/tests/Feature/Guards/Spec195ResidualActionSurfaceClosureGuardTest.php` and `apps/platform/tests/Feature/Guards/ActionSurfaceContractTest.php`
### Implementation for User Story 3
- [X] T019 [US3] Audit `apps/platform/app/Filament/Pages/BreakGlassRecovery.php` and either retire it from live baseline handling or keep it as a live `intentional_exemption` with `security_flow_exception` in `apps/platform/app/Support/Ui/ActionSurface/ActionSurfaceExemptions.php`, `apps/platform/app/Support/Ui/ActionSurface/ActionSurfaceValidator.php`, and `apps/platform/tests/Feature/Guards/Spec195ResidualActionSurfaceClosureGuardTest.php`
- [X] T020 [US3] Tighten stale-baseline cleanup rules for retired or reasonless discovered exceptions in `apps/platform/app/Support/Ui/ActionSurface/ActionSurfaceExemptions.php` and `apps/platform/app/Support/Ui/ActionSurface/ActionSurfaceValidator.php`
- [X] T021 [US3] Extend `apps/platform/app/Support/Ui/ActionSurface/ActionSurfaceValidator.php` to reject reasonless discovered exemptions and retired surfaces that still remain in `baseline()`
**Checkpoint**: User Story 3 is complete when every surviving discovered-page exception is explicit, reasoned, and evidence-backed.
---
## Phase 6: User Story 4 - Block future unclassified residuals (Priority: P2)
**Goal**: Fail fast when future residual surfaces or exemptions appear without an explicit closure decision.
**Independent Test**: Introduce representative missing-classification and missing-reason cases in the guard suite and verify CI-style failures occur with actionable output.
### Tests for User Story 4
- [X] T022 [P] [US4] Create regression cases for missing closure decision, missing reason category, missing structured evidence, and stale baseline entries in `apps/platform/tests/Feature/Guards/Spec195ResidualActionSurfaceClosureGuardTest.php`
- [X] T023 [P] [US4] Extend `apps/platform/tests/Feature/Guards/ActionSurfaceContractTest.php` with a representative uncatalogued residual-surface failure path
### Implementation for User Story 4
- [X] T024 [US4] Extend `apps/platform/app/Support/Ui/ActionSurface/ActionSurfaceValidator.php` with namespace-scoped residual-candidate checks that preserve the existing generic discovery boundary and require Spec 195 inventory coverage for qualifying pages
- [X] T025 [US4] Improve `apps/platform/app/Support/Ui/ActionSurface/ActionSurfaceValidator.php` failure output and update `specs/195-action-surface-closure/quickstart.md` so missing Spec 195 classifications report actionable class and file context plus the reviewer classification workflow
**Checkpoint**: User Story 4 is complete when new residual surfaces or exemptions cannot merge silently without a closure decision.
---
## Phase 7: User Story 5 - Preserve good special surfaces without forced churn (Priority: P3)
**Goal**: Keep legitimately special selector, registration, onboarding, landing, and dashboard surfaces outside the generic contract where that is the safer and clearer choice.
**Independent Test**: Review the special-surface entries plus focused tests and verify that selectors, onboarding, registration, landing, and dashboard shells can remain separately governed or harmless without being mislabeled as defects.
### Tests for User Story 5
- [X] T026 [P] [US5] Create focused landing-surface coverage in `apps/platform/tests/Feature/Workspaces/Spec195ManagedTenantsLandingTest.php`
- [X] T027 [P] [US5] Extend special-surface evidence coverage with explicit positive and negative authorization paths in `apps/platform/tests/Feature/Rbac/RegisterTenantAuthorizationTest.php`, `apps/platform/tests/Feature/Rbac/OnboardingWizardUiEnforcementTest.php`, and `apps/platform/tests/Feature/Filament/TenantDashboardDbOnlyTest.php`
- [X] T028 [P] [US5] Extend selector-surface evidence coverage in `apps/platform/tests/Feature/Workspaces/ChooseWorkspacePageTest.php` and `apps/platform/tests/Feature/Auth/TenantChooserSelectionTest.php`
### Implementation for User Story 5
- [X] T029 [US5] If audit changes the design-seed classification for `apps/platform/app/Filament/Pages/ChooseWorkspace.php`, `apps/platform/app/Filament/Pages/ChooseTenant.php`, `apps/platform/app/Filament/Pages/TenantDashboard.php`, `apps/platform/app/Filament/Pages/Tenancy/RegisterTenant.php`, `apps/platform/app/Filament/Pages/Workspaces/ManagedTenantOnboardingWizard.php`, or `apps/platform/app/Filament/Pages/Workspaces/ManagedTenantsLanding.php`, update the matching entry in `apps/platform/app/Support/Ui/ActionSurface/ActionSurfaceExemptions.php` and the paired expectation in `apps/platform/tests/Feature/Guards/Spec195ResidualActionSurfaceClosureGuardTest.php`
- [X] T030 [US5] Align `apps/platform/app/Support/Ui/ActionSurface/ActionSurfaceValidator.php` so discovered special surfaces can remain `separately_governed` or `harmless_special_case` without forced `actionSurfaceDeclaration()` enrollment
**Checkpoint**: User Story 5 is complete when good special surfaces remain explicitly governed without unnecessary normalization.
---
## Phase 8: Polish & Cross-Cutting Verification
**Purpose**: Run the focused verification pack, format the touched files, and confirm the implementation matches the planning contract.
- [X] T031 Run the focused Spec 195 Sail verification pack from `specs/195-action-surface-closure/quickstart.md` against `apps/platform/tests/Feature/Guards/ActionSurfaceContractTest.php`, `apps/platform/tests/Feature/Guards/ActionSurfaceValidatorTest.php`, `apps/platform/tests/Feature/Guards/Spec195ResidualActionSurfaceClosureGuardTest.php`, `apps/platform/tests/Feature/Guards/Spec194GovernanceActionSemanticsGuardTest.php`, `apps/platform/tests/Feature/Guards/LivewireTrustedStateGuardTest.php`, `apps/platform/tests/Feature/Guards/FilamentTableStandardsGuardTest.php`, `apps/platform/tests/Feature/System/Spec113/AuthorizationSemanticsTest.php`, `apps/platform/tests/Feature/System/Spec114/OpsTriageActionsTest.php`, `apps/platform/tests/Feature/System/OpsRunbooks/FindingsLifecycleBackfillStartTest.php`, `apps/platform/tests/Feature/Auth/BreakGlassWorkspaceOwnerRecoveryTest.php`, `apps/platform/tests/Feature/Auth/TenantChooserSelectionTest.php`, `apps/platform/tests/Feature/Workspaces/ChooseWorkspacePageTest.php`, `apps/platform/tests/Feature/Workspaces/WorkspaceAuditTrailTest.php`, `apps/platform/tests/Feature/TenantRBAC/TenantSwitcherScopeTest.php`, `apps/platform/tests/Feature/Rbac/RegisterTenantAuthorizationTest.php`, `apps/platform/tests/Feature/TenantRBAC/TenantBootstrapAssignTest.php`, `apps/platform/tests/Feature/Rbac/OnboardingWizardUiEnforcementTest.php`, `apps/platform/tests/Feature/Onboarding/OnboardingDraftAccessTest.php`, `apps/platform/tests/Feature/Filament/TenantDashboardDbOnlyTest.php`, `apps/platform/tests/Feature/Rbac/TenantDashboardArrivalContextVisibilityTest.php`, `apps/platform/tests/Feature/System/Spec195/SystemDirectoryResidualSurfaceTest.php`, and `apps/platform/tests/Feature/Workspaces/Spec195ManagedTenantsLandingTest.php`
- [X] T032 Run `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent` and resolve formatting issues in `apps/platform/app/Support/Ui/ActionSurface/ActionSurfaceDiscovery.php`, `apps/platform/app/Support/Ui/ActionSurface/ActionSurfaceExemptions.php`, `apps/platform/app/Support/Ui/ActionSurface/ActionSurfaceValidator.php`, `apps/platform/tests/Feature/Guards/ActionSurfaceContractTest.php`, `apps/platform/tests/Feature/Guards/ActionSurfaceValidatorTest.php`, `apps/platform/tests/Feature/Guards/Spec195ResidualActionSurfaceClosureGuardTest.php`, `apps/platform/tests/Feature/System/Spec195/SystemDirectoryResidualSurfaceTest.php`, and `apps/platform/tests/Feature/Workspaces/Spec195ManagedTenantsLandingTest.php`
- [X] T033 Verify the final residual inventory matches `specs/195-action-surface-closure/contracts/action-surface-closure.logical.openapi.yaml`, includes human-readable `surfaceName` values, and covers the initial seed plus any newly audited residual surfaces described in `specs/195-action-surface-closure/data-model.md`
---
## Dependencies
- Setup tasks T001-T002 precede all implementation work.
- Foundational tasks T003-T007 block all user stories.
- User Story 1 depends on Phase 2 and unlocks the actual residual inventory.
- User Story 2, User Story 3, and User Story 5 depend on User Story 1 because they classify concrete residual entries inside the shared inventory.
- User Story 4 depends on User Story 1, User Story 2, User Story 3, and User Story 5 because the regression guard must validate the final closure state.
- Polish tasks T031-T033 depend on all user stories being complete.
## Parallel Execution Examples
- After T001, run T002 in parallel with any remaining setup review.
- In Phase 2, T003 and T004 can run in parallel before T005-T007 land.
- In User Story 1, T008 and T009 can run in parallel.
- In User Story 2, T012 and T013 can run in parallel.
- In User Story 3, T017 and T018 can run in parallel.
- In User Story 4, T022 and T023 can run in parallel.
- In User Story 5, T026, T027, and T028 can run in parallel.
## Implementation Strategy
- Start with Phase 2 and User Story 1 to establish the residual inventory and validator contract.
- Deliver User Story 2 next to close the highest-risk system and utility surfaces first.
- Follow with User Story 3 and User Story 5 to normalize discovered exceptions and preserve legitimate special workflows.
- Complete User Story 4 only after the closure states are finalized so the new guard enforces the finished model instead of a moving target.
- Finish with the focused Sail verification pack and Pint formatting from Phase 8.