Compare commits

..

3 Commits

Author SHA1 Message Date
a2a42d4e5f Spec 196: finalize hard Filament nativity cleanup artifacts (#231)
## Summary
- add the complete Spec 196 artifact set for hard Filament nativity cleanup
- include spec, requirements checklist, plan, research, data model, logical contract, quickstart, and executable tasks
- update agent context after planning
- resolve all cross-artifact consistency issues so the feature package is implementation-ready

## Included artifacts
- specs/196-hard-filament-nativity-cleanup/spec.md
- specs/196-hard-filament-nativity-cleanup/checklists/requirements.md
- specs/196-hard-filament-nativity-cleanup/plan.md
- specs/196-hard-filament-nativity-cleanup/research.md
- specs/196-hard-filament-nativity-cleanup/data-model.md
- specs/196-hard-filament-nativity-cleanup/contracts/filament-nativity-cleanup.logical.openapi.yaml
- specs/196-hard-filament-nativity-cleanup/quickstart.md
- specs/196-hard-filament-nativity-cleanup/tasks.md

## Notes
- no runtime code paths were changed
- no application tests were run because this change set is spec and planning documentation only
- the artifact set was re-analyzed until no consistency issues remained

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #231
2026-04-13 10:26:27 +00:00
1c291fb9fe feat: close spec 195 action surface residuals (#230)
## Summary
- add the full Spec 195 residual action-surface design package under `specs/195-action-surface-closure`
- implement residual surface inventory and validator enforcement for uncatalogued system and special Filament pages
- add focused regression coverage for residual guards, system directory pages, managed-tenants landing, and readonly register-tenant / tenant-dashboard access
- fix the system workspace detail surface by loading tenant route keys and disabling lazy system database notifications to avoid the Livewire 404 on `/system/directory/workspaces/{workspace}`

## Testing
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/System/Spec195/SystemDirectoryResidualSurfaceTest.php tests/Feature/Filament/DatabaseNotificationsPollingTest.php`
- `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent`

## Notes
- branch: `195-action-surface-closure`
- target: `dev`
- no new assets, migrations, or provider-registration changes

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #230
2026-04-13 07:47:58 +00:00
acc8947384 feat: harden governance action semantics (#229)
## Summary
- add the Spec 194 governance action catalog, friction classes, reason policies, and regression guards
- align exception, review, evidence, finding, tenant, provider connection, and system run actions to the shared semantics model
- add focused feature, RBAC, audit, unit, and browser coverage, including the tenant detail triage header consistency update

## Verification
- ran the focused Spec 194 verification pack from the quickstart and task plan
- ran targeted tenant triage coverage after the detail-header update
- ran `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent`

## Filament Notes
- Filament v5 / Livewire v4 compliance preserved
- provider registration remains in `apps/platform/bootstrap/providers.php`
- globally searchable resources were not changed
- destructive actions remain confirmation-gated and server-authorized
- no new Filament assets were introduced; the existing `cd apps/platform && php artisan filament:assets` deploy step stays unchanged

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #229
2026-04-12 21:21:44 +00:00
28 changed files with 4602 additions and 8 deletions

View File

@ -174,6 +174,10 @@ ## 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) - 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) - 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 `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 + Laravel 12, Filament v5, Livewire v4, Pest v4, Tailwind CSS v4, existing `DependencyQueryService`, `DependencyTargetResolver`, `TenantRequiredPermissionsViewModelBuilder`, `ArtifactTruthPresenter`, `WorkspaceContext`, Filament `InteractsWithTable`, Filament `TableComponent`, and existing badge and action-surface helpers (196-hard-filament-nativity-cleanup)
- PostgreSQL through existing tenant-owned and workspace-context models (`InventoryItem`, `InventoryLink`, `TenantPermission`, `EvidenceSnapshot`, `TenantReview`); no schema change planned (196-hard-filament-nativity-cleanup)
- PHP 8.4.15 (feat/005-bulk-operations) - PHP 8.4.15 (feat/005-bulk-operations)
@ -208,8 +212,8 @@ ## Code Style
PHP 8.4.15: Follow standard conventions PHP 8.4.15: Follow standard conventions
## Recent Changes ## Recent Changes
- 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`) - 196-hard-filament-nativity-cleanup: Added PHP 8.4.15 + Laravel 12, Filament v5, Livewire v4, Pest v4, Tailwind CSS v4, existing `DependencyQueryService`, `DependencyTargetResolver`, `TenantRequiredPermissionsViewModelBuilder`, `ArtifactTruthPresenter`, `WorkspaceContext`, Filament `InteractsWithTable`, Filament `TableComponent`, and existing badge and action-surface helpers
- 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 - 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
- 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 - 195-action-surface-closure: Added PostgreSQL through existing workspace-owned, tenant-owned, and system-visible models; no schema change planned
<!-- MANUAL ADDITIONS START --> <!-- MANUAL ADDITIONS START -->
<!-- MANUAL ADDITIONS END --> <!-- MANUAL ADDITIONS END -->

View File

@ -49,7 +49,7 @@ public function workspaceTenants(): Collection
->where('workspace_id', (int) $this->workspace->getKey()) ->where('workspace_id', (int) $this->workspace->getKey())
->orderBy('name') ->orderBy('name')
->limit(10) ->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([ ->colors([
'primary' => Color::Blue, 'primary' => Color::Blue,
]) ])
->databaseNotifications() ->databaseNotifications(isLazy: false)
->databaseNotificationsPolling(null) ->databaseNotificationsPolling(null)
->renderHook( ->renderHook(
PanelsRenderHook::BODY_START, PanelsRenderHook::BODY_START,

View File

@ -4,6 +4,9 @@
namespace App\Support\Ui\ActionSurface; 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\BaselineCompareLanding;
use App\Filament\Pages\BaselineCompareMatrix; use App\Filament\Pages\BaselineCompareMatrix;
use App\Filament\Pages\Monitoring\Alerts; use App\Filament\Pages\Monitoring\Alerts;
@ -13,7 +16,11 @@
use App\Filament\Pages\Monitoring\Operations; use App\Filament\Pages\Monitoring\Operations;
use App\Filament\Pages\Operations\TenantlessOperationRunViewer; use App\Filament\Pages\Operations\TenantlessOperationRunViewer;
use App\Filament\Pages\Reviews\ReviewRegister; use App\Filament\Pages\Reviews\ReviewRegister;
use App\Filament\Pages\TenantDashboard;
use App\Filament\Pages\TenantDiagnostics; 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\AlertDeliveryResource\Pages\ListAlertDeliveries;
use App\Filament\Resources\AlertDestinationResource\Pages\ViewAlertDestination; use App\Filament\Resources\AlertDestinationResource\Pages\ViewAlertDestination;
use App\Filament\Resources\BackupSetResource\Pages\ViewBackupSet; use App\Filament\Resources\BackupSetResource\Pages\ViewBackupSet;
@ -29,6 +36,12 @@
use App\Filament\Resources\TenantResource\Pages\ViewTenant; use App\Filament\Resources\TenantResource\Pages\ViewTenant;
use App\Filament\Resources\TenantReviewResource\Pages\ViewTenantReview; use App\Filament\Resources\TenantReviewResource\Pages\ViewTenantReview;
use App\Filament\Resources\Workspaces\Pages\ViewWorkspace; 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; use App\Support\WorkspaceIsolation\TenantOwnedModelFamilies;
final class ActionSurfaceExemptions final class ActionSurfaceExemptions
@ -46,7 +59,6 @@ public static function baseline(): self
// Baseline allowlist for legacy surfaces. Keep shrinking this list. // 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. // 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\\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\\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\\ChooseWorkspace' => 'Workspace chooser has no contract-style table action surface.',
'App\\Filament\\Pages\\Tenancy\\RegisterTenant' => 'Tenant onboarding route is covered by onboarding/RBAC specs.', '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 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\ActionSurfaceInspectAffordance;
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceSlot; use App\Support\Ui\ActionSurface\Enums\ActionSurfaceSlot;
use Filament\Pages\Page;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use SplFileInfo;
final class ActionSurfaceValidator final class ActionSurfaceValidator
{ {
@ -53,9 +57,20 @@ public function validate(): ActionSurfaceValidationResult
public function validateComponents(array $components): ActionSurfaceValidationResult public function validateComponents(array $components): ActionSurfaceValidationResult
{ {
$issues = []; $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->validateSpec193MonitoringSurfaceInventory($issues);
$this->validateSpec192RecordPageInventory($issues); $this->validateSpec192RecordPageInventory($issues);
$this->validateSpec195ResidualSurfaceInventory($issues, $discoveredClassNames);
foreach ($components as $component) { foreach ($components as $component) {
if (! class_exists($component->className)) { 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 * @param array<int, ActionSurfaceValidationIssue> $issues
*/ */
@ -419,10 +769,26 @@ private function validateClassExemptionOrFail(string $className, array &$issues)
$reason = $this->exemptions->reasonForClass($className); $reason = $this->exemptions->reasonForClass($className);
if ($reason === null) { if ($reason === null) {
$residualSurface = ActionSurfaceExemptions::spec195ResidualSurface($className);
if ($residualSurface !== null) {
$closureDecision = (string) ($residualSurface['closureDecision'] ?? '');
if ($closureDecision === 'generic_contract_enrollment') {
$issues[] = new ActionSurfaceValidationIssue( $issues[] = new ActionSurfaceValidationIssue(
className: $className, className: $className,
message: 'Missing action-surface declaration and no component exemption exists.', message: 'Residual surface is marked for generic-contract enrollment but still lacks actionSurfaceDeclaration().',
hint: 'Add actionSurfaceDeclaration() or register a baseline exemption with a non-empty reason.', 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; return;
@ -653,4 +1019,142 @@ className: $className,
hint: 'Keep exportIsDefaultBulkActionForReadOnly=true or exempt ListBulkMoreGroup with a reason.', 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\BackupSet;
use App\Models\Finding; use App\Models\Finding;
use App\Models\OperationRun; use App\Models\OperationRun;
use App\Models\User;
use Filament\Facades\Filament; use Filament\Facades\Filament;
use Illuminate\Support\Facades\Bus; use Illuminate\Support\Facades\Bus;
use Livewire\Livewire; use Livewire\Livewire;
@ -76,3 +77,12 @@
Bus::assertNothingDispatched(); 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'); ->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 { it('keeps enrolled system panel pages declaration-backed without stale baseline exemptions', function (): void {
$baselineExemptions = ActionSurfaceExemptions::baseline(); $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 { it('keeps enrolled relation managers declaration-backed without stale baseline exemptions', function (): void {
$baselineExemptions = ActionSurfaceExemptions::baseline(); $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 { it('passes when all required slots are declared', function (): void {
$validator = new ActionSurfaceValidator( $validator = new ActionSurfaceValidator(
profileDefinition: new ActionSurfaceProfileDefinition, profileDefinition: new ActionSurfaceProfileDefinition,
@ -245,3 +267,73 @@ className: $className,
expect($result->hasIssues())->toBeFalse($result->formatForAssertion()); 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 App\Filament\Pages\Tenancy\RegisterTenant;
use Filament\Facades\Filament; use Filament\Facades\Filament;
use Livewire\Livewire;
describe('Register tenant page authorization', function () { describe('Register tenant page authorization', function () {
it('is not visible for readonly members', function () { it('is not visible for readonly members', function () {
@ -29,4 +30,19 @@
Filament::setCurrentPanel(null); 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.

View File

@ -0,0 +1,36 @@
# Specification Quality Checklist: Hard Filament Nativity Cleanup
**Purpose**: Validate specification completeness and quality before proceeding to planning
**Created**: 2026-04-13
**Feature**: [spec.md](../spec.md)
## Content Quality
- [x] No implementation details (languages, frameworks, APIs)
- [x] Focused on user value and business needs
- [x] Written for non-technical stakeholders
- [x] All mandatory sections completed
## Requirement Completeness
- [x] No [NEEDS CLARIFICATION] markers remain
- [x] Requirements are testable and unambiguous
- [x] Success criteria are measurable
- [x] Success criteria are technology-agnostic (no implementation details)
- [x] All acceptance scenarios are defined
- [x] Edge cases are identified
- [x] Scope is clearly bounded
- [x] Dependencies and assumptions identified
## Feature Readiness
- [x] All functional requirements have clear acceptance criteria
- [x] User scenarios cover primary flows
- [x] Feature meets measurable outcomes defined in Success Criteria
- [x] No implementation details leak into specification
## Notes
- Validated after initial draft on 2026-04-13.
- Framework-specific language appears only where the feature itself and constitution require naming the native admin contract; the spec does not prescribe code-level implementation choices, new abstractions, or dependency changes.
- No clarification questions were required from the user because scope, non-goals, and acceptance expectations were already explicit.

View File

@ -0,0 +1,395 @@
openapi: 3.1.0
info:
title: Filament Nativity Cleanup Logical Contract
version: 0.1.0
description: >-
Logical planning contract for Spec 196. This artifact defines the expected
state ownership, filter semantics, scope guarantees, and row projections for
the three cleaned UI surfaces. It is not a runtime API definition.
servers:
- url: https://logical-spec.local
description: Non-runtime planning contract
paths:
/internal/ui/inventory-items/{inventoryItemId}/dependencies:
get:
summary: Read dependency section state for one inventory item detail surface
operationId: getInventoryItemDependenciesView
parameters:
- name: inventoryItemId
in: path
required: true
schema:
type: integer
responses:
'200':
description: Dependency detail-surface state and rows
content:
application/json:
schema:
type: object
required:
- data
properties:
data:
$ref: '#/components/schemas/DependencyEdgesView'
'404':
description: Returned when the actor is not entitled to the tenant or inventory-item scope.
/internal/ui/tenants/{tenantExternalId}/required-permissions:
get:
summary: Read required-permissions page state for one route-scoped tenant
operationId: getTenantRequiredPermissionsView
parameters:
- name: tenantExternalId
in: path
required: true
schema:
type: string
- name: status
in: query
required: false
schema:
$ref: '#/components/schemas/RequiredPermissionsStatus'
- name: type
in: query
required: false
schema:
$ref: '#/components/schemas/PermissionTypeFilter'
- name: features
in: query
required: false
schema:
type: array
items:
type: string
- name: search
in: query
required: false
schema:
type: string
responses:
'200':
description: Required-permissions page state, summary, and rows
content:
application/json:
schema:
type: object
required:
- data
properties:
data:
$ref: '#/components/schemas/RequiredPermissionsView'
'404':
description: Returned when workspace or tenant membership is absent for the route-scoped tenant.
/internal/ui/evidence-overview:
get:
summary: Read workspace evidence overview table state and rows
operationId: getEvidenceOverviewView
parameters:
- name: tenantId
in: query
required: false
description: Optional entitled tenant prefilter; unauthorized tenant identifiers must not reveal row existence.
schema:
anyOf:
- type: integer
- type: 'null'
- name: search
in: query
required: false
schema:
type: string
responses:
'200':
description: Workspace evidence overview state and rows
content:
application/json:
schema:
type: object
required:
- data
properties:
data:
$ref: '#/components/schemas/EvidenceOverviewView'
'404':
description: Returned when workspace membership is absent for the evidence overview surface.
components:
schemas:
DependencyDirection:
type: string
enum:
- all
- inbound
- outbound
RelationshipTypeKey:
type: string
description: Recognized relationship type key from the existing dependency domain.
RequiredPermissionsStatus:
type: string
enum:
- missing
- present
- error
- all
PermissionTypeFilter:
type: string
enum:
- all
- application
- delegated
DependencyEdgesState:
type: object
required:
- inventoryItemId
- tenantId
- direction
properties:
inventoryItemId:
type: integer
tenantId:
type: integer
direction:
$ref: '#/components/schemas/DependencyDirection'
relationshipType:
anyOf:
- $ref: '#/components/schemas/RelationshipTypeKey'
- type: 'null'
DependencyEdgeRow:
type: object
required:
- relationshipType
- targetType
- renderedTarget
- isMissing
- missingTitle
properties:
relationshipType:
type: string
targetType:
type: string
targetId:
anyOf:
- type: string
- type: 'null'
renderedTarget:
type: object
additionalProperties: true
isMissing:
type: boolean
missingTitle:
type: string
DependencyEdgesView:
type: object
required:
- state
- rows
properties:
state:
$ref: '#/components/schemas/DependencyEdgesState'
rows:
type: array
items:
$ref: '#/components/schemas/DependencyEdgeRow'
RequiredPermissionsState:
type: object
required:
- routeTenantExternalId
- status
- type
- features
- search
- routeTenantAuthoritative
- seededFromQuery
properties:
routeTenantExternalId:
type: string
status:
$ref: '#/components/schemas/RequiredPermissionsStatus'
type:
$ref: '#/components/schemas/PermissionTypeFilter'
features:
type: array
uniqueItems: true
description: Normalized unique list of known feature keys.
items:
type: string
search:
type: string
routeTenantAuthoritative:
type: boolean
const: true
seededFromQuery:
type: boolean
RequiredPermissionsSummary:
type: object
required:
- counts
- freshness
- featureImpacts
- copyPayloads
- issues
properties:
counts:
type: object
additionalProperties:
type: integer
overall:
anyOf:
- type: string
- type: 'null'
freshness:
type: object
additionalProperties: true
featureImpacts:
type: array
items:
type: object
additionalProperties: true
copyPayloads:
type: object
additionalProperties:
type: string
issues:
type: array
items:
type: object
additionalProperties: true
PermissionReviewRow:
type: object
required:
- permissionKey
- type
- status
properties:
permissionKey:
type: string
type:
type: string
status:
type: string
description:
type: string
features:
type: array
items:
type: string
details:
type: object
additionalProperties: true
RequiredPermissionsView:
type: object
required:
- state
- summary
- rows
properties:
state:
$ref: '#/components/schemas/RequiredPermissionsState'
summary:
$ref: '#/components/schemas/RequiredPermissionsSummary'
rows:
type: array
items:
$ref: '#/components/schemas/PermissionReviewRow'
EvidenceOverviewState:
type: object
required:
- workspaceId
- authorizedTenantIds
- tenantFilter
- search
- seededFromQuery
properties:
workspaceId:
type: integer
authorizedTenantIds:
type: array
items:
type: integer
tenantFilter:
anyOf:
- type: integer
- type: 'null'
search:
type: string
seededFromQuery:
type: boolean
EvidenceOverviewRow:
type: object
required:
- tenantId
- tenantName
- snapshotId
- artifactTruth
- freshness
- missingDimensions
- staleDimensions
- nextStep
- viewUrl
properties:
tenantId:
type: integer
tenantName:
type: string
snapshotId:
type: integer
artifactTruth:
type: object
additionalProperties: true
freshness:
type: object
additionalProperties: true
generatedAt:
anyOf:
- type: string
- type: 'null'
missingDimensions:
type: integer
staleDimensions:
type: integer
nextStep:
type: string
viewUrl:
type: string
EvidenceOverviewView:
type: object
required:
- state
- rows
properties:
state:
$ref: '#/components/schemas/EvidenceOverviewState'
rows:
type: array
items:
$ref: '#/components/schemas/EvidenceOverviewRow'
x-spec-196-notes:
consumerScope: illustrative core consumers only; Blade views and focused verification files are tracked in plan.md, quickstart.md, and tasks.md
consumers:
- apps/platform/app/Filament/Resources/InventoryItemResource.php
- apps/platform/app/Livewire/InventoryItemDependencyEdgesTable.php
- apps/platform/app/Filament/Pages/TenantRequiredPermissions.php
- apps/platform/app/Filament/Pages/Monitoring/EvidenceOverview.php
- apps/platform/app/Services/Intune/TenantRequiredPermissionsViewModelBuilder.php
- apps/platform/tests/Feature/InventoryItemDependenciesTest.php
- apps/platform/tests/Feature/Rbac/TenantRequiredPermissionsTrustedStateTest.php
- apps/platform/tests/Feature/Evidence/EvidenceOverviewPageTest.php
invariants:
- route tenant stays authoritative on required-permissions
- evidence overview only exposes entitled tenant rows
- dependency rendering remains tenant-isolated and DB-only
- query values may seed initial state but not stay the primary contract
nonGoals:
- runtime API exposure
- new persistence
- new provider or route families
- global context shell redesign
- monitoring page-state architecture rewrite
- audit log selected-record or inspect duality cleanup
- finding exceptions queue dual-inspect cleanup
- baseline compare matrix or other special-visualization work
- verification report viewer families or onboarding verification report variants
- normalized diff or settings viewer families
- restore preview, restore results, or enterprise-detail layout rework
- raw anchor-to-component link consistency sweeps
- badge-only, banner-only, or style-only polish work
- new CI guardrail, review-enforcement, or constitution frameworks

View File

@ -0,0 +1,212 @@
# Data Model: Hard Filament Nativity Cleanup
## Overview
This feature introduces no new persisted entity, table, enum, or product-domain source of truth. It refactors three existing UI surfaces by replacing pseudo-native interaction contracts with native page-owned or component-owned state.
The data model for planning is therefore a set of derived UI-state and row-projection models that answer four questions:
1. What state is authoritative for each cleaned surface?
2. Which source truths continue to produce the rows and summaries?
3. Which values may be seeded from deeplinks, and which values must remain route- or entitlement-authoritative?
4. Which invariants must remain true after the cleanup?
## Existing Source Truths Reused Without Change
The following truths remain authoritative and are not redefined by this feature:
- `InventoryItem`, `InventoryLink`, `DependencyQueryService`, and `DependencyTargetResolver` for dependency edges and rendered targets
- the current tenant-context inventory route and inventory-record scope rules
- `TenantRequiredPermissionsViewModelBuilder`, `TenantPermission`, permission configuration, and provider guidance links for required-permissions truth
- the route-scoped tenant on `/admin/tenants/{tenant:external_id}/required-permissions`
- `EvidenceSnapshot`, `TenantReview`, `ArtifactTruthPresenter`, and the current workspace-context entitlement rules for evidence overview rows
- existing capability registries, `WorkspaceContext`, tenant membership checks, and current deny-as-not-found boundaries
This feature changes how these truths are controlled and rendered, not what they mean.
## New Derived Planning Models
### DependencyEdgesTableState
**Type**: embedded detail-surface state
**Source**: Livewire component state on inventory item detail
| Field | Type | Notes |
|------|------|-------|
| `inventoryItemId` | int | Required current detail record key |
| `tenantId` | int | Required tenant-context key derived from the current panel or record scope |
| `direction` | string | Allowed values: `all`, `inbound`, `outbound`; default `all` |
| `relationshipType` | string or null | Null means all relationship types; otherwise one allowed relationship type key |
**Validation rules**
- `inventoryItemId` must resolve to the current authorized record.
- `tenantId` must match the current tenant-context scope.
- `direction` must stay inside the three allowed values.
- `relationshipType` must be null or a recognized relationship type value.
### DependencyEdgeRow
**Type**: derived row projection
**Source**: `DependencyQueryService` plus `DependencyTargetResolver`
| Field | Type | Notes |
|------|------|-------|
| `relationshipType` | string | Canonical relationship family for grouping or filter matching |
| `targetType` | string | Current target kind, including `missing` when unresolved |
| `targetId` | string or null | External or internal target identifier |
| `renderedTarget` | array | Existing rendered badge and link payload |
| `isMissing` | boolean | Derived from `targetType === missing` |
| `missingTitle` | string | Existing descriptive fallback text for unresolved targets |
**Invariants**
- Row membership must stay tenant-isolated.
- Missing-target rendering must preserve current operator hints.
- Render-time behavior must remain DB-only with no Graph access.
### RequiredPermissionsTableState
**Type**: page-owned derived table state
**Source**: native Filament table filters and search on `TenantRequiredPermissions`
| Field | Type | Notes |
|------|------|-------|
| `routeTenantExternalId` | string | Authoritative tenant scope from the route |
| `status` | string | Allowed values: `missing`, `present`, `error`, `all` |
| `type` | string | Allowed values: `all`, `application`, `delegated` |
| `features` | list<string> | Zero or more selected feature keys |
| `search` | string | Native table search text |
| `seededFromQuery` | boolean | True only during initial mount when deeplink values were present |
**Validation rules**
- The route tenant always wins over tenant-like query values.
- Query values may seed `status`, `type`, `features`, and `search` only at initial mount.
- `features` must be a normalized unique list of known feature keys.
### RequiredPermissionsSummaryProjection
**Type**: derived page summary model
**Source**: `TenantRequiredPermissionsViewModelBuilder` evaluated against the currently active normalized filter state
| Field | Type | Notes |
|------|------|-------|
| `counts` | object | Existing counts for missing application, missing delegated, present, and error rows |
| `overall` | string or null | Existing overall readiness state |
| `freshness` | object | Existing freshness payload including stale or not stale |
| `featureImpacts` | list<object> | Existing per-feature impact summary |
| `copyPayloads` | object | Existing application and delegated copy payloads |
| `issues` | list<object> | Existing derived guidance and next-step content |
**Invariants**
- Summary and table rows must be derived from the same active filter state.
- Copy payload semantics must remain consistent with current expectations.
- Tenant scope must not be mutable through filter state.
### PermissionReviewRow
**Type**: derived table row
**Source**: `TenantRequiredPermissionsViewModelBuilder`
| Field | Type | Notes |
|------|------|-------|
| `permissionKey` | string | Stable permission identifier |
| `type` | string | `application` or `delegated` |
| `status` | string | Current permission review status |
| `description` | string | Human-readable permission description |
| `features` | list<string> | Feature tags associated with the permission |
| `details` | object | Existing supporting metadata used for inline review only |
### EvidenceOverviewTableState
**Type**: workspace-context table state
**Source**: native Filament table search and optional query-seeded entitled tenant prefilter
| Field | Type | Notes |
|------|------|-------|
| `workspaceId` | int | Required current workspace context |
| `authorizedTenantIds` | list<int> | Entitled tenant ids available to the actor |
| `tenantFilter` | int or null | Current entitled tenant prefilter, nullable when not active |
| `search` | string | Native table search across tenant-facing row labels |
| `seededFromQuery` | boolean | True only when the initial request carried a prefilter |
**Validation rules**
- `tenantFilter` must be null or one of the actor's entitled tenant ids.
- Missing workspace membership continues to produce `404`.
- Non-entitled tenant ids must not leak through filter state, row counts, or drilldowns.
### EvidenceOverviewRow
**Type**: derived workspace report row
**Source**: current snapshot query plus `ArtifactTruthPresenter`
| Field | Type | Notes |
|------|------|-------|
| `tenantId` | int | Entitled tenant identifier |
| `tenantName` | string | Current display label |
| `snapshotId` | int | Current active snapshot id for drilldown |
| `artifactTruth` | object | Existing truth badge and explanation payload |
| `freshness` | object | Existing freshness badge payload |
| `generatedAt` | string or null | Timestamp label |
| `missingDimensions` | int | Existing burden metric |
| `staleDimensions` | int | Existing burden metric |
| `nextStep` | string | Existing next-step text |
| `viewUrl` | string | Current tenant evidence drilldown URL |
**Invariants**
- Row drilldowns must stay workspace-safe and tenant-entitlement-safe.
- Derived-state memoization must remain effective.
- Render-time behavior must remain DB-only.
### CleanupAdmissionCandidate
**Type**: planning-only admission check
**Source**: implementation audit only when a possible extra hit is discovered
| Field | Type | Notes |
|------|------|-------|
| `surfaceKey` | string | Stable human-readable identifier |
| `path` | string | File or route path for the potential extra surface |
| `matchesProblemClass` | boolean | Must be true to qualify |
| `opensArchitectureQuestion` | boolean | Must be false to qualify |
| `decision` | string | `include` or `defer` |
| `reason` | string | Explicit justification for the decision |
## State Transition Rules
### Rule 1 - Deeplink seed to native active state
- Initial request query values may seed filter state on `TenantRequiredPermissions` and `EvidenceOverview`.
- After initial mount, active state belongs to the native page table or component, not to `request()`.
### Rule 2 - Route scope remains authoritative
- `TenantRequiredPermissions` may never replace its route tenant from query values.
- Inventory dependency state may never replace the current detail record or tenant context.
- Evidence overview may never reveal non-entitled tenant rows through a prefilter.
### Rule 3 - No new persistence or mirrored helper truth
- Filter state stays session-backed or Livewire-backed only where Filament already provides that behavior.
- No new database table, JSON helper artifact, or persisted UI-state mirror is introduced.
## Safety Rules
- No cleaned surface may introduce a second wrapper contract that simply restyles the current non-native behavior.
- No cleaned surface may widen current workspace or tenant scope behavior.
- No cleaned surface may lose current empty-state meaning, next-step clarity, or inspect destination correctness.
- No page or component may call Graph or other remote APIs during render as part of this cleanup.
## Planned Test Mapping
| Model / Rule | Existing Coverage | Planned Additions |
|---|---|---|
| `DependencyEdgesTableState` | `tests/Feature/InventoryItemDependenciesTest.php`, dependency tenant-isolation and query-service tests | native component test for direction and relationship interaction |
| `RequiredPermissionsTableState` | `tests/Feature/Rbac/TenantRequiredPermissionsTrustedStateTest.php`, unit filter normalization tests | page-level native table test |
| `RequiredPermissionsSummaryProjection` | current unit tests for freshness, overall state, feature impacts, and copy payloads | page-level summary consistency assertions |
| `EvidenceOverviewTableState` | `tests/Feature/Evidence/EvidenceOverviewPageTest.php` | native table assertions and any new table-standard guard alignment |
| `EvidenceOverviewRow` DB-only invariant | `tests/Feature/Filament/EvidenceOverviewDerivedStateMemoizationTest.php` | update assertions to reflect native table rendering without losing memoization guarantees |

View File

@ -0,0 +1,296 @@
# Implementation Plan: Hard Filament Nativity Cleanup
**Branch**: `196-hard-filament-nativity-cleanup` | **Date**: 2026-04-13 | **Spec**: `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/196-hard-filament-nativity-cleanup/spec.md`
**Input**: Feature specification from `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/196-hard-filament-nativity-cleanup/spec.md`
**Note**: This plan keeps the work inside the existing Filament v5 / Livewire v4 page layer, the current derived view-model services, the existing dependency query and target-resolution services, and the current focused RBAC and reporting tests. It explicitly avoids adding a new runtime UI framework, new persistence, or a broader shell or monitoring-state architecture.
## Summary
Remove the three hard nativity bypasses called out by Spec 196 by reusing repo-proven native Filament patterns. Convert `EvidenceOverview` and `TenantRequiredPermissions` into page-owned native table surfaces with native filter state and unchanged scope semantics. Replace the GET-form dependency micro-UI on inventory item detail with an embedded Livewire table component that owns direction and relationship state inside the current detail surface. Preserve existing domain truth, authorization, empty states, and drilldowns, and prove the cleanup through focused feature, Livewire, RBAC, and Filament guard coverage.
## Technical Context
**Language/Version**: PHP 8.4.15
**Primary Dependencies**: Laravel 12, Filament v5, Livewire v4, Pest v4, Tailwind CSS v4, existing `DependencyQueryService`, `DependencyTargetResolver`, `TenantRequiredPermissionsViewModelBuilder`, `ArtifactTruthPresenter`, `WorkspaceContext`, Filament `InteractsWithTable`, Filament `TableComponent`, and existing badge and action-surface helpers
**Storage**: PostgreSQL through existing tenant-owned and workspace-context models (`InventoryItem`, `InventoryLink`, `TenantPermission`, `EvidenceSnapshot`, `TenantReview`); no schema change planned
**Testing**: Pest feature, Livewire, unit, and existing guard tests run through Laravel Sail; browser smoke only if an implementation detail proves impossible to cover with existing feature or Livewire layers
**Target Platform**: Laravel monolith web application under `apps/platform`, spanning tenant-context admin routes under `/admin/t/{tenant}/...`, tenant-specific admin routes under `/admin/tenants/{tenant:external_id}/...`, and workspace-context canonical admin routes under `/admin/...`
**Project Type**: web application
**Performance Goals**: Preserve DB-only render behavior, keep dependency and evidence rendering free of Graph calls, avoid request-reload control flows, preserve current row-count and summary derivation cost, and avoid introducing extra persistence or polling
**Constraints**: No new persistence, no new enum or status family, no new wrapper microframework, no global shell or monitoring-state refactor, no provider or panel registration changes, no weakening of current 404 or 403 semantics, no destructive-action expansion, and no new asset pipeline work
**Scale/Scope**: 3 core surfaces, 1 embedded tenant detail micro-surface, 1 tenant workflow page, 1 workspace report page, and a focused verification pack touching roughly 12 existing or new test files; optional extra hits are allowed only if no new architecture question opens
## 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 | Inventory dependencies and evidence overview remain read-only views over existing inventory and evidence truth. |
| Read/write separation | PASS | PASS | The cleanup changes interaction contracts only. Existing follow-up writes remain on their current confirmed destinations. |
| Graph contract path | N/A | N/A | No new Graph calls or contract-registry changes are introduced. |
| Deterministic capabilities | PASS | PASS | Existing capability registries, tenant access checks, and page authorization remain authoritative. |
| Workspace + tenant isolation | PASS | PASS | Tenant required permissions keeps the route tenant authoritative; evidence overview keeps workspace-context entitlement filtering; inventory detail remains tenant-context scoped. |
| RBAC-UX authorization semantics | PASS | PASS | Non-members remain `404`, in-scope capability denial remains unchanged, and no new mutation path bypasses server-side authorization. |
| Run observability / Ops-UX | PASS | PASS | No new `OperationRun` flow is introduced. Existing run-linked destinations remain unchanged. |
| Data minimization | PASS | PASS | No new persisted UI-state mirror or helper artifact is added, and DB-only rendering remains required. |
| Proportionality / anti-bloat | PASS | PASS | The design reuses existing Filament patterns and adds no new persistence or generic UI layer. |
| UI semantics / few layers | PASS | PASS | The plan maps directly from current domain truth to native UI primitives without a new presenter framework. |
| Filament-native UI | PASS | PASS | All three target surfaces move toward native Filament tables, filters, or shared primitives and away from pseudo-native contracts. |
| Surface taxonomy / decision-first roles | PASS | PASS | Inventory dependencies remains a secondary context sub-surface; tenant required permissions and evidence overview remain primary decision surfaces. |
| 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 provider changes are required; Laravel 11+ provider registration remains in `apps/platform/bootstrap/providers.php`. |
| Global search hard rule | PASS | PASS | No searchable resource is added or modified. `TenantRequiredPermissions` and `EvidenceOverview` are pages, and inventory resource search behavior is unchanged. |
| Destructive action safety | PASS | PASS | No new destructive action is introduced. Existing destructive follow-up actions remain on their current confirmed surfaces. |
| Asset strategy | PASS | PASS | No new global or on-demand assets are required. Existing deployment handling of `cd apps/platform && php artisan filament:assets` remains unchanged. |
## Filament-Specific Compliance Notes
- **Livewire v4.0+ compliance**: The implementation remains entirely inside Filament v5 + Livewire v4 and does not introduce legacy Filament or Livewire APIs.
- **Provider registration location**: No provider changes are required; panel providers remain registered in `apps/platform/bootstrap/providers.php`.
- **Global search**: No resource search behavior changes. `InventoryItemResource` already has a view page, but this spec does not change its global-search status. `TenantRequiredPermissions` and `EvidenceOverview` remain pages, not searchable resources.
- **Destructive actions**: No new destructive actions are added. Existing linked destinations retain their current confirmation and authorization behavior.
- **Asset strategy**: No new assets are planned. Existing deployment handling of `cd apps/platform && php artisan filament:assets` remains sufficient and unchanged.
- **Testing plan**: Cover the cleanup through `InventoryItemDependenciesTest`, a new Livewire or table-component dependency test, `TenantRequiredPermissionsTrustedStateTest`, a new required-permissions page-table test, `EvidenceOverviewPageTest`, `EvidenceOverviewDerivedStateMemoizationTest`, and guard coverage such as `FilamentTableStandardsGuardTest` where native table adoption becomes guardable.
## Phase 0 Research
Research outcomes are captured in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/196-hard-filament-nativity-cleanup/research.md`.
Key decisions:
- Reuse the repo's existing native page-table pattern from `ReviewRegister` and `InventoryCoverage` for `TenantRequiredPermissions` and `EvidenceOverview`.
- Keep `TenantRequiredPermissions` and `EvidenceOverview` on derived data and current services instead of adding new projections, tables, or materialized helper models.
- Replace inventory dependency GET-form controls with an embedded Livewire `TableComponent` because the surface is detail-context and not a true relation manager or a standalone page.
- Treat query parameters as one-time seed or deeplink inputs only; after mount, native page or component state owns filter interaction.
- No additional low-risk same-class hit is confirmed in planning; default implementation scope stays at the three named core surfaces unless implementation audit finds one trivial match that does not widen scope.
- Extend existing focused tests and the current Filament table guard where possible instead of introducing a new browser-only verification layer.
## Phase 1 Design
Design artifacts are created under `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/196-hard-filament-nativity-cleanup/`:
- `research.md`: implementation-shape decisions and rejected alternatives for each surface
- `data-model.md`: derived UI-state and row-projection models for dependency scope, required-permissions filtering, and evidence overview rows
- `contracts/filament-nativity-cleanup.logical.openapi.yaml`: internal logical contract for page state, derived rows, scope rules, and deeplink semantics
- `quickstart.md`: implementation and verification sequence for the feature
Design highlights:
- `EvidenceOverview` adopts `InteractsWithTable` + `HasTable` and keeps derived rows via a records callback similar to `InventoryCoverage`.
- `TenantRequiredPermissions` adopts a native table and native table-owned filter state while keeping summary, copy, and guidance sections above the table body.
- Inventory dependencies stays embedded on inventory detail but moves its interactive controls into a dedicated Livewire table component rather than a request-driven Blade fragment.
- Existing domain services stay authoritative: dependency rows still come from `DependencyQueryService` and `DependencyTargetResolver`; permission truth still comes from `TenantRequiredPermissionsViewModelBuilder` when an adapter is needed; evidence truth still comes from `ArtifactTruthPresenter` and current snapshot queries.
- No new schema, enum, or shared microframework is introduced.
## Project Structure
### Documentation (this feature)
```text
specs/196-hard-filament-nativity-cleanup/
├── plan.md
├── research.md
├── data-model.md
├── quickstart.md
├── spec.md
├── contracts/
│ └── filament-nativity-cleanup.logical.openapi.yaml
└── checklists/
└── requirements.md
```
### Source Code (repository root)
```text
apps/platform/
├── app/
│ ├── Filament/
│ │ ├── Pages/
│ │ │ ├── TenantRequiredPermissions.php # MODIFY
│ │ │ └── Monitoring/
│ │ │ └── EvidenceOverview.php # MODIFY
│ │ └── Resources/
│ │ └── InventoryItemResource.php # MODIFY
│ ├── Livewire/
│ │ └── InventoryItemDependencyEdgesTable.php # NEW
│ └── Services/
│ └── Intune/
│ └── TenantRequiredPermissionsViewModelBuilder.php # MODIFY or REVIEW FOR ADAPTERS
├── resources/
│ └── views/
│ └── filament/
│ ├── components/
│ │ └── dependency-edges.blade.php # MODIFY
│ └── pages/
│ ├── tenant-required-permissions.blade.php # MODIFY
│ └── monitoring/
│ └── evidence-overview.blade.php # MODIFY
└── tests/
├── Feature/
│ ├── InventoryItemDependenciesTest.php # MODIFY
│ ├── Evidence/
│ │ └── EvidenceOverviewPageTest.php # MODIFY
│ ├── Filament/
│ │ ├── EvidenceOverviewDerivedStateMemoizationTest.php # MODIFY
│ │ ├── InventoryItemDependencyEdgesTableTest.php # NEW
│ │ └── TenantRequiredPermissionsPageTest.php # NEW
│ ├── Guards/
│ │ └── FilamentTableStandardsGuardTest.php # MODIFY
│ └── Rbac/
│ └── TenantRequiredPermissionsTrustedStateTest.php # MODIFY
└── Unit/
├── TenantRequiredPermissionsFilteringTest.php # REUSE
├── TenantRequiredPermissionsCopyPayloadTest.php # REUSE
├── TenantRequiredPermissionsOverallStatusTest.php # REUSE
├── TenantRequiredPermissionsFeatureImpactTest.php # REUSE
└── TenantRequiredPermissionsFreshnessTest.php # REUSE
```
**Structure Decision**: Keep the work entirely inside the existing Laravel/Filament monolith under `apps/platform`. Add at most one new Livewire table component for the dependency sub-surface, then modify the three target page or resource files and focused tests. Do not add a new service layer, persistence shape, or cross-surface UI abstraction.
## Complexity Tracking
No constitution violation or BLOAT-triggered structural expansion is planned. The feature deliberately avoids new persistence, new enums, new UI taxonomies, or new cross-page infrastructure.
## Proportionality Review
Not triggered beyond the spec-level review already completed. The implementation plan adds no new enum, presenter framework, persisted entity, or registry. The narrowest correct implementation is to reuse native Filament tables and one embedded `TableComponent`.
## Implementation Strategy
Execution sequence for this plan is test-first at two levels: complete the shared test and guard scaffolding before story work starts, then land each story's focused tests before its implementation changes.
### Phase 0.5 - Establish shared test and guard scaffolding
Goal: create the blocking Spec 196 test entry points and shared guard coverage before surface refactors begin.
Changes:
- Create the new focused test entry points for the dependency table component and required-permissions page table.
- Extend shared guard coverage for new native page-table expectations and faux-control regressions.
- Add shared regression coverage for mount-only query seeding versus authoritative scope on required permissions and evidence overview.
Tests:
- This phase establishes the focused test harness and is itself the blocking prerequisite for later story delivery.
### Phase A - Replace the inventory dependency GET form with an embedded Livewire table component
Goal: keep the dependencies surface on inventory item detail, but move direction and relationship controls into native component state instead of a request-driven Blade fragment.
Changes:
- Introduce `App\Livewire\InventoryItemDependencyEdgesTable` as a Filament `TableComponent` that owns direction and relationship filter state.
- Keep the surface embedded in the current `InventoryItemResource` detail section rather than moving it to a standalone route or relation manager.
- Move the current request-query dependency fetch into the component so the Blade fragment no longer parses `request()` or submits a GET form.
- Preserve existing target rendering, missing-target labels, and tenant-isolated dependency resolution through `DependencyQueryService` and `DependencyTargetResolver`.
- Keep render-time behavior DB-only and preserve the no-Graph-call guard.
Tests:
- Extend the listed story tests before landing implementation changes.
- Modify `tests/Feature/InventoryItemDependenciesTest.php` to assert the preserved result logic while removing dependence on manual query-string filter submission.
- Add `tests/Feature/Filament/InventoryItemDependencyEdgesTableTest.php` to cover direction changes, relationship narrowing, missing-target rendering, and tenant isolation through the native component.
- Reuse existing unit and feature tests around `DependencyQueryService`, `DependencyTargetResolver`, and tenant isolation as domain and safety regression coverage.
### Phase B - Convert `TenantRequiredPermissions` into a native page-owned table and filter contract
Goal: remove pseudo-native filter controls while preserving the page's summary, guidance, copy payloads, and tenant-authoritative routing semantics.
Changes:
- Add `HasTable` and `InteractsWithTable` to `App\Filament\Pages\TenantRequiredPermissions`.
- Replace the manual public filter properties and `updated*()` handlers with native table filters and native table search, using a derived-records callback because permission rows are view-model based rather than Eloquent-backed.
- Keep the route tenant authoritative and allow query parameters only to seed initial filter state when the page first mounts.
- Keep the summary, copy, and guidance blocks, but derive their values from the same normalized filter state that drives the native table rows.
- Preserve the current behavior where copy payloads remain driven by the intended filter dimensions and do not silently widen tenant scope.
Tests:
- Extend the listed story tests before landing implementation changes.
- Modify `tests/Feature/Rbac/TenantRequiredPermissionsTrustedStateTest.php` to keep route-tenant authority and safe deeplink behavior after native filter adoption.
- Add `tests/Feature/Filament/TenantRequiredPermissionsPageTest.php` to cover native filter behavior, summary consistency, and no-results states.
- Reuse current unit tests for filtering, freshness, feature impacts, overall status, and copy payload derivation as unchanged domain-truth guards.
- Extend `tests/Feature/Guards/FilamentTableStandardsGuardTest.php` if the page becomes subject to shared page-table standards.
### Phase C - Convert `EvidenceOverview` into a native workspace table
Goal: remove the hand-built report table and make filtering, empty state, and row inspection native without changing workspace-safe scope behavior.
Changes:
- Add `HasTable` and `InteractsWithTable` to `App\Filament\Pages\Monitoring\EvidenceOverview`.
- Move row generation out of the Blade table contract and into a native table records callback, following the derived-row pattern already used by `InventoryCoverage`.
- Convert the current `tenantFilter` query handling into native filter state seeded from an entitled tenant prefilter only.
- Add native table search across tenant-facing row labels.
- Keep the existing row inspect destination to tenant evidence detail through a single native inspect model.
- Replace the Blade table markup with a page wrapper that renders the native table and keeps any lightweight surrounding layout only if still needed.
Tests:
- Extend the listed story tests before landing implementation changes.
- Modify `tests/Feature/Evidence/EvidenceOverviewPageTest.php` to assert native table output, native search behavior, workspace safety, entitled-tenant filtering, and current drilldowns.
- Modify `tests/Feature/Filament/EvidenceOverviewDerivedStateMemoizationTest.php` to keep DB-only derived-state guarantees after table conversion.
- Extend `tests/Feature/Guards/FilamentTableStandardsGuardTest.php` if the new page-owned table should now satisfy shared table standards.
### Phase D - Verification, guard alignment, and explicit scope stop
Goal: confirm the cleanup remains bounded to the three core surfaces and that the repo's existing guard layer reflects newly native table surfaces where appropriate.
Changes:
- Extend guard coverage only where native table adoption now makes a page eligible for existing table standards.
- Run focused Sail verification for the modified feature, RBAC, and guard tests.
- Record the release close-out in `specs/196-hard-filament-nativity-cleanup/quickstart.md`, including cleaned surfaces, deferred themes, optional extra hits, and touched follow-up specs.
- Document any optional additional same-class hit only if it was truly included; otherwise record that no extra candidate was confirmed.
- Stop immediately if implementation reaches shared micro-UI family, monitoring-state, or shell-context architecture.
Tests:
- Focused feature and Livewire test pack for the three surfaces.
- Existing RBAC and derived-state regression tests retained.
- Pint run after touched files are complete.
## Risk Assessment
### Risk 1 - Scope creep into shared monitoring or detail-micro-UI architecture
Mitigation:
- Keep `EvidenceOverview` limited to native table conversion, not broader monitoring-shell cleanup.
- Keep inventory dependencies embedded on the existing detail page and do not generalize a new micro-UI framework.
- Reject any additional surface that opens shared-family or shell questions.
### Risk 2 - Deeplink or initial-state regressions on required permissions and evidence overview
Mitigation:
- Treat query values strictly as initial seed state.
- Keep route tenant and entitled tenant scope authoritative.
- Preserve and extend current trusted-state tests.
### Risk 3 - Derived-data performance or DB-only regressions after native table adoption
Mitigation:
- Reuse the repo's existing derived-records page pattern from `InventoryCoverage`.
- Preserve current eager-loading and memoization behavior.
- Keep the current no-Graph and DB-only tests in the verification pack.
### Risk 4 - Over-correcting custom read-only rendering into an unnecessary generic surface
Mitigation:
- Keep only the controls and state contract native.
- Allow custom read-only cell or row presentation to remain where it carries real domain value.
- Avoid relation-manager or standalone-page moves for the dependency section.
## Implementation Order Recommendation
1. Establish the shared test and guard scaffolding first so story work starts from the same blocking regression baseline captured in the task plan.
2. Replace inventory dependencies second, with the focused story tests landing before the implementation changes.
3. Convert `TenantRequiredPermissions` third, again extending the story tests before code changes.
4. Convert `EvidenceOverview` fourth, with its focused page and derived-state tests updated before the refactor lands.
5. Run the final focused verification pack, formatting, and release close-out last, and only then consider whether any optional same-class extra hit truly qualifies.

View File

@ -0,0 +1,165 @@
# Quickstart: Hard Filament Nativity Cleanup
## Goal
Implement Spec 196 by replacing three pseudo-native UI contracts with native Filament or Livewire interaction models while preserving current scope, summaries, empty states, and drilldown behavior.
## Implementation Sequence
### 1. Prepare shared test and guard scaffolding
Touch:
- `apps/platform/tests/Feature/Filament/InventoryItemDependencyEdgesTableTest.php`
- `apps/platform/tests/Feature/Filament/TenantRequiredPermissionsPageTest.php`
- `apps/platform/tests/Feature/Guards/FilamentTableStandardsGuardTest.php`
- `apps/platform/tests/Feature/Rbac/TenantRequiredPermissionsTrustedStateTest.php`
- `apps/platform/tests/Feature/Evidence/EvidenceOverviewPageTest.php`
Do:
- create the new focused surface-test entry points before story implementation starts
- add the shared guard expectations for new native page-table and faux-control regressions
- add the shared mount-only query-seeding regression coverage that later story work depends on
### 2. Replace the inventory dependency GET form with an embedded `TableComponent`
Touch:
- `apps/platform/app/Filament/Resources/InventoryItemResource.php`
- `apps/platform/app/Livewire/InventoryItemDependencyEdgesTable.php`
- `apps/platform/resources/views/filament/components/dependency-edges.blade.php`
Do:
- extend the focused dependency tests before landing implementation changes
- embed a native Filament `TableComponent` inside the existing inventory detail section
- move direction and relationship state into the component
- fetch dependency rows through current dependency services
- keep missing-target rendering and target-link behavior intact
Do not:
- create a new standalone route for dependencies
- convert the surface into a RelationManager
- keep `request()` as the primary interaction-state source
### 3. Convert `TenantRequiredPermissions` to a native page-owned filter and table contract
Touch:
- `apps/platform/app/Filament/Pages/TenantRequiredPermissions.php`
- `apps/platform/resources/views/filament/pages/tenant-required-permissions.blade.php`
- `apps/platform/app/Services/Intune/TenantRequiredPermissionsViewModelBuilder.php` only if a small adapter is needed
Do:
- extend the focused required-permissions tests before landing implementation changes
- add `HasTable` and `InteractsWithTable`
- replace pseudo-native filter controls with native filters and native search
- derive the summary, guidance, and copy payload blocks from the same normalized filter state that drives the table rows
- keep the route tenant authoritative and allow query values only as initial seed state
Do not:
- let query values redefine tenant scope
- split the page into a new resource or standalone workflow
- introduce a wrapper abstraction that merely hides the old filter bar
### 4. Convert `EvidenceOverview` to a native page-owned table
Touch:
- `apps/platform/app/Filament/Pages/Monitoring/EvidenceOverview.php`
- `apps/platform/resources/views/filament/pages/monitoring/evidence-overview.blade.php`
Do:
- extend the focused evidence overview tests before landing implementation changes
- add `HasTable` and `InteractsWithTable`
- move current row construction into a native table records callback
- convert the current tenant query prefilter into a native filter seeded from entitled query input only
- add native search across tenant-facing row labels
- keep row inspect behavior pointed at the existing tenant evidence drilldown
- keep empty-state behavior explicit and native
Do not:
- introduce a new read model or persistence layer
- widen the workspace-context route into a tenant-context route
- make remote calls during render
### 5. Run the final focused verification pack and formatting
Touch:
- `apps/platform/tests/Feature/InventoryItemDependenciesTest.php`
- `apps/platform/tests/Feature/Rbac/TenantRequiredPermissionsTrustedStateTest.php`
- `apps/platform/tests/Feature/Evidence/EvidenceOverviewPageTest.php`
- `apps/platform/tests/Feature/Filament/EvidenceOverviewDerivedStateMemoizationTest.php`
- `apps/platform/tests/Feature/Filament/InventoryItemDependencyEdgesTableTest.php`
- `apps/platform/tests/Feature/Filament/TenantRequiredPermissionsPageTest.php`
- `apps/platform/tests/Feature/Guards/FilamentTableStandardsGuardTest.php` if newly applicable
Do:
- preserve current scope and authorization assertions
- replace GET-form assumptions with native Livewire or table-state assertions
- keep DB-only and no-Graph render guarantees
- keep unit tests for permission filtering and copy payload logic as domain-truth guards
- run the full focused Sail pack and `pint` only after the three story slices are complete
### 6. Stop on scope boundaries
If implementation touches any of the following, stop and defer instead of half-solving them here:
- shared detail micro-UI contract work
- monitoring page-state architecture
- global context shell behavior
- verification report viewer families
- diff, settings, restore preview, or enterprise-detail layout families
### 7. Record the release close-out in this quickstart
When implementation is complete, update this file with a short close-out note that records:
- which surfaces were actually cleaned
- whether any optional same-class extra hit was included or explicitly rejected
- which related themes stayed out of scope and were deferred
- which follow-up specs or artifacts were touched
## 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/InventoryItemDependenciesTest.php
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Filament/InventoryItemDependencyEdgesTableTest.php
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Rbac/TenantRequiredPermissionsTrustedStateTest.php
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Filament/TenantRequiredPermissionsPageTest.php
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Evidence/EvidenceOverviewPageTest.php
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Filament/EvidenceOverviewDerivedStateMemoizationTest.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/Unit/TenantRequiredPermissionsFilteringTest.php
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Unit/TenantRequiredPermissionsCopyPayloadTest.php
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Unit/TenantRequiredPermissionsOverallStatusTest.php
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Unit/TenantRequiredPermissionsFeatureImpactTest.php
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Unit/TenantRequiredPermissionsFreshnessTest.php
cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent
```
## Manual Smoke Checklist
1. Open an inventory item detail page and confirm dependency direction and relationship changes happen without a foreign apply-and-reload workflow.
2. Open tenant required permissions and confirm the filter surface feels native, while summary counts, guidance, and copy flows remain correct.
3. Open evidence overview and confirm the table behaves like a native Filament report with clear empty state and row inspect behavior.
4. Confirm no cleaned surface leaks scope through query manipulation.
5. Confirm no implementation expanded into monitoring-state, shell, or shared micro-UI redesign work.
## 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` deployment handling remains sufficient and unchanged.

View File

@ -0,0 +1,90 @@
# Research: Hard Filament Nativity Cleanup
## Decision: Reuse the repo's existing native page-table pattern for `EvidenceOverview` and `TenantRequiredPermissions`
### Rationale
The codebase already has two strong native examples for page-owned tables outside normal resource index pages: `ReviewRegister` and `InventoryCoverage`. Both use `InteractsWithTable`, `HasTable`, native Filament filters, Filament-managed filter state, native empty states, and one consistent inspect model. That makes them the narrowest repo-consistent replacement for the two current page-level bypasses.
`EvidenceOverview` is currently a hand-built Blade report table, and `TenantRequiredPermissions` is currently a custom page with pseudo-native filter controls. Both are better modeled as page-owned native tables than as bespoke Blade contracts.
### Alternatives considered
- Keep the current Blade table and filter bars, but restyle them more convincingly: rejected because that preserves the separate contract instead of removing it.
- Move either surface into a Resource or RelationManager: rejected because both already have correct route and page ownership; only their internal interaction model is wrong.
## Decision: Keep both page-level surfaces on derived data instead of adding new projections or schema
### Rationale
`EvidenceOverview` rows are already derived from `EvidenceSnapshot`, `TenantReview`, and `ArtifactTruthPresenter`, while `TenantRequiredPermissions` rows and summaries are already derived through `TenantRequiredPermissionsViewModelBuilder`. The current product truth is sufficient. The problem is not missing data infrastructure, but the non-native way the data is exposed.
Using derived table records keeps the implementation proportional and avoids importing persistence or a second source of truth for UI state.
### Alternatives considered
- Add dedicated read models or materialized projections for overview rows: rejected because the spec is cleanup, not reporting-architecture expansion.
- Convert the permission or evidence pages into query-first Eloquent resources: rejected because the current derived summaries and guidance would still need a second layer and would not simplify the domain.
## Decision: Replace inventory dependency GET controls with an embedded Livewire `TableComponent`
### Rationale
The dependency surface is not a standalone page and not a true Eloquent relationship that should become a RelationManager. It is a detail-context sub-surface inside inventory item view. The narrowest native replacement is therefore an embedded Livewire `TableComponent` that owns direction and relationship state, renders native filters, and stays inside the current inventory detail section.
The repo already uses Filament `TableComponent` in `BackupSetPolicyPickerTable`, which proves the pattern is acceptable and reusable here.
### Alternatives considered
- Convert the dependency section into a RelationManager: rejected because dependency edges are query-driven, not a direct relationship manager surface.
- Move dependencies to a new standalone page: rejected because it would break the current inspect-one-record workflow and widen scope.
- Keep a custom Blade fragment with `wire:model` on raw inputs: rejected because that still leaves a pseudo-native control surface instead of a real native table contract.
## Decision: Query parameters may seed initial state, but they do not remain the authoritative interaction contract
### Rationale
Both `TenantRequiredPermissions` and `EvidenceOverview` have valid deeplink or workflow-continuity reasons to accept initial query values. The spec explicitly allows that. What needs to change is ongoing ownership of page-body state. After first mount, filter state must live in native page or component state rather than continuing to be reconstructed from `request()` on every interaction.
This preserves existing deeplink behavior without letting query values become a shadow state system.
### Alternatives considered
- Remove all query seeding entirely: rejected because the current product does rely on deeplink and continuity behavior.
- Keep query parameters as the main contract forever: rejected because that is the bypass pattern the spec exists to remove.
## Decision: Preserve custom read-only presentation where it carries domain value, but make control state native
### Rationale
The spec is not a repo-wide custom Blade purge. Some read-only rendering still carries useful domain formatting, especially for dependency target badges, missing-target hints, permission guidance blocks, and evidence explanation text. The actual harm sits in fake controls, manual GET submission, and hand-built primary table contracts.
The narrowest implementation therefore replaces the primary control and table contracts while allowing domain-specific read-only cells or layout blocks to remain when they do not create a second state system.
### Alternatives considered
- Force every touched surface into generic Filament markup only: rejected because it risks over-correction and would expand scope into broader micro-UI standardization.
- Leave custom presentation and custom control markup mixed together: rejected because it would keep the core nativity problem alive.
## Decision: No additional same-class low-risk hit is confirmed during planning
### Rationale
The planning audit found the three target surfaces clearly. It did not identify a fourth candidate that is both obviously the same problem class and clearly small enough to include without opening shared-family or shell questions. That means the safe default is to keep the implementation scope locked to the three named surfaces and only admit an extra hit if implementation discovers a truly trivial match.
### Alternatives considered
- Expand planning scope now to include visually similar custom Blade surfaces elsewhere in monitoring or verification: rejected because those families carry broader architecture and product-semantics questions already marked out of scope.
## Decision: Extend existing focused tests and guardrails rather than introducing a new browser-centric verification layer
### Rationale
The repo already has meaningful coverage for all three areas: dependency rendering and tenant isolation, required-permissions trusted-state behavior and view-model derivation, and evidence overview authorization and DB-only rendering. The cleanup should lean on that existing coverage, then add only the missing surface-level native-table or component assertions.
This keeps the feature aligned with `TEST-TRUTH-001` and avoids creating a heavier verification framework than the change requires.
### Alternatives considered
- Add a new browser suite for all three surfaces as the primary proof: rejected because most required outcomes are already testable with feature and Livewire tests.
- Rely only on manual smoke checks: rejected because the repo rules require automated coverage for changed behavior.

View File

@ -0,0 +1,250 @@
# Feature Specification: Hard Filament Nativity Cleanup
**Feature Branch**: `[196-hard-filament-nativity-cleanup]`
**Created**: 2026-04-13
**Status**: Proposed
**Input**: User description: "Spec 196 - Hard Filament Nativity Cleanup"
## Spec Candidate Check *(mandatory - SPEC-GATE-001)*
- **Problem**: Three active admin surfaces signal native Filament behavior but still run on separate UI contracts: a GET-form dependency filter inside inventory detail, a plain-HTML filter bar on required permissions, and a hand-built evidence report table.
- **Today's failure**: Operators hit inconsistent filter behavior, apply-and-reload interaction, request-driven body state, and bespoke empty-state or navigation semantics inside surfaces that otherwise live in Filament and Livewire.
- **User-visible improvement**: Dependency review, permission follow-up, and evidence review feel like the rest of the admin product, with fewer foreign workflows and less hidden state drift.
- **Smallest enterprise-capable version**: Clean only the three confirmed bypass surfaces and only the parts that create the non-native contract; keep larger shell, monitoring-state, verification-report, and shared micro-UI families out of scope.
- **Explicit non-goals**: No global context-shell redesign, no monitoring page-state architecture rewrite, no repo-wide custom Blade purge, no special visualization rework, no badge-only polish sweep, and no new CI guardrail, review-enforcement, or constitution framework in this spec.
- **Permanent complexity imported**: Focused surface refactors, targeted regression coverage, and one close-out note. No new models, tables, enums, abstractions, or cross-surface UI framework are introduced.
- **Why now**: These are already active operator surfaces with real maintenance and consistency cost, and they are the clearest low-dispute cleanup targets before later specs touch larger UI families.
- **Why not local**: The harm comes from the same problem class repeating across multiple live surfaces. One-off cosmetic edits would leave the same parallel contracts and drift pattern intact.
- **Approval class**: Cleanup
- **Red flags triggered**: One mild red flag: multiple surfaces are touched in one spec. This is justified because all included surfaces share the same unnecessary nativity bypass and remain bounded to three concrete entry points plus optional same-class low-risk extras.
- **Score**: Nutzen: 2 | Dringlichkeit: 2 | Scope: 2 | Komplexitat: 1 | Produktnahe: 2 | Wiederverwendung: 2 | **Gesamt: 11/12**
- **Decision**: approve
## Spec Scope Fields *(mandatory)*
- **Scope**: tenant + workspace canonical-view cleanup
- **Primary Routes**:
- `/admin/t/{tenant}/inventory/inventory-items/{record}`
- `/admin/tenants/{tenant:external_id}/required-permissions`
- `/admin/evidence/overview`
- **Data Ownership**: Inventory dependencies continue to read tenant-owned inventory items and dependency edges in tenant context. Tenant required permissions continues to read tenant-owned permission verification truth and provider guidance for a single tenant. Evidence overview continues to read tenant-owned evidence snapshots inside a workspace-context route. This spec adds no new persistence and does not move ownership boundaries.
- **RBAC**: Inventory dependencies stays under tenant-context inventory detail and keeps existing tenant membership plus tenant entitlement requirements. Tenant required permissions keeps workspace and tenant entitlement, preserves route-tenant authority, and remains deny-as-not-found for non-members. Evidence overview remains workspace-context, still requires workspace membership, and must only reveal entitled tenant rows and drilldowns.
For canonical-view specs, the spec MUST define:
- **Default filter behavior when tenant-context is active**: Evidence overview may honor an entitled tenant prefilter for deeplink or workflow continuity, but it remains a workspace-context page and must not silently redefine scope from unrelated tenant-like query values.
- **Explicit entitlement checks preventing cross-tenant leakage**: Evidence overview rows, filters, and row drilldowns must resolve only within the current workspace and the viewer's entitled tenant set. Unauthorized tenant ids must not reveal rows, row counts, or drilldown targets.
## 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 |
|---|---|---|---|---|---|---|---|
| Inventory item dependencies section | Secondary Context Surface | While inspecting one inventory item, decide whether a related object explains impact or follow-up | Current inventory item context, dependency direction scope, relationship family, matching edges, missing-target markers | Linked target pages, raw references, last-known-name hints | Not primary because the operator's main decision remains about the current inventory item detail | Follows inspect-one-record workflow instead of creating a side workflow | Removes apply-and-reload detours inside detail view |
| Tenant required permissions | Primary Decision Surface | Decide whether tenant consent or verification follow-up is required and what action to take next | Overall state, freshness, missing application vs delegated counts, active filters, matching permission rows | Copy payloads, consent guidance, provider-connection destination | Primary because the page itself answers what permission action is next for this tenant | Follows tenant permission follow-up workflow instead of request-parameter reconstruction | Keeps filter state and guidance in one page-owned contract |
| Evidence overview | Primary Decision Surface | Decide which tenant's evidence needs refresh or review next | Tenant, artifact truth, freshness, burden, next step, inspect affordance | Tenant evidence detail, deeper snapshot explanation, row-specific follow-up context | Primary because it is the workspace evidence review list where operators choose the next follow-up target | Follows workspace evidence-review workflow instead of bespoke report markup | Native table behavior reduces bespoke scanning, empty-state, and drilldown rules |
## 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 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Inventory item dependencies section | Detail / Inspect | Detail-first resource sub-surface | Open a dependency target or change dependency scope | Current inventory item detail with linked dependency targets | forbidden | Inline non-destructive section controls only | none | `/admin/t/{tenant}/inventory/inventory-items/{record}` | Same route plus linked target destinations | Active tenant, current inventory item, dependency direction, relationship scope | Inventory item dependencies / dependency edge | Current record context, relationship family, missing-target state | Embedded detail micro-surface remains custom for domain-specific read-only edge rendering, but not for primary controls |
| Tenant required permissions | List / Guidance / Diagnostic | List-only read-first workflow page | Grant consent, rerun verification, or narrow the current filter state | Inline page itself | forbidden | Safe guidance and copy actions remain secondary in page sections or header | none | `/admin/tenants/{tenant:external_id}/required-permissions` | Same page | Current workspace, current tenant, freshness, active filters | Required permissions / permission row | Overall readiness, freshness, missing counts, active filter state | Permission rows remain an inline review matrix rather than a separate inspect route |
| Evidence overview | List / Table / Report | Read-only registry report | Open tenant evidence for the row that needs attention | Full-row inspect into tenant evidence detail | required | Header clear-filter action only; any safe secondary action stays clearly secondary | none | `/admin/evidence/overview` | Tenant evidence snapshot view for the selected row | Current workspace, entitled-tenant filter, artifact truth, freshness | Evidence overview / evidence snapshot | Artifact truth, freshness, burden, next step | none |
## 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 |
|---|---|---|---|---|---|---|---|---|---|---|
| Inventory item dependencies section | Tenant operator reviewing one inventory item | Decide whether related objects explain the current item and which target to inspect next | Detail micro-surface | Which dependencies matter for this item right now? | Current direction and relationship scope, grouped edges, missing-target markers, target badges | Raw references, last-known names, deeper target detail | relationship direction, relationship type, target availability | none; read-only inspect flow | Change direction scope, change relationship scope, open target | none |
| Tenant required permissions | Tenant operator or tenant manager | Decide whether consent, delegated follow-up, or verification rerun is needed | Read-first workflow page | What permission gap blocks this tenant right now and what should happen next? | Overall state, freshness, counts, active filters, matching permission rows | Copy payload detail, consent guidance, provider-connection management destination | overall readiness, freshness, permission status, permission type | read-only page with outbound follow-up links; no new mutation starts on this page | Adjust filters, open consent guidance, rerun verification, manage provider connection | none introduced by this spec |
| Evidence overview | Workspace operator | Decide which tenant evidence snapshot needs review or refresh next | Workspace report table | Which tenant needs evidence follow-up right now? | Tenant row, artifact truth, freshness, burden, next step, inspect affordance | Deeper snapshot explanation inside tenant evidence detail | artifact truth, freshness, evidence burden | none; read-only drilldown | Change filters, open tenant evidence row | none |
## Proportionality Review *(mandatory when structural complexity is introduced)*
- **New source of truth?**: no
- **New persisted entity/table/artifact?**: no
- **New abstraction?**: no
- **New enum/state/reason family?**: no
- **New cross-domain UI framework/taxonomy?**: no
- **Current operator problem**: Active admin surfaces inside existing Filament and Livewire context still bypass native primitives, forcing operators and maintainers to juggle extra contracts for simple filters and tables.
- **Existing structure is insufficient because**: The current harm comes from the mismatch itself. These surfaces already live in Filament and Livewire, so keeping plain HTML control contracts, request-driven state, or hand-built report tables preserves avoidable drift rather than solving a domain gap.
- **Narrowest correct implementation**: Convert only the three clear bypasses and only the parts that create the non-native contract. Keep legitimate custom read-only presentation and larger shell, monitoring-state, and shared-family questions out of scope.
- **Ownership cost**: Bounded surface refactors, focused tests, and one close-out note. No new domain model, state family, or UI framework is introduced.
- **Alternative intentionally rejected**: A repo-wide Filament purity sweep, a global shell or state redesign, or wrapper abstractions that merely hide the same non-native contract.
- **Release truth**: current-release cleanup that removes existing drift before later specs tackle larger UI families
## User Scenarios & Testing *(mandatory)*
### User Story 1 - Review Dependencies Without A Foreign Workflow (Priority: P1)
While inspecting an inventory item, an operator can change dependency scope and understand the resulting edges without submitting a separate GET form or feeling like the detail page has switched into a different mini app.
**Why this priority**: The inventory detail page already owns the current record context. A foreign interaction model inside that detail page directly harms comprehension and confidence.
**Independent Test**: Can be tested by opening an inventory item detail page, changing dependency direction and relationship scope, and verifying that the same matching edges, missing-target markers, and empty states appear without a manual apply-and-reload contract.
**Acceptance Scenarios**:
1. **Given** an inventory item with inbound and outbound edges, **When** the operator changes dependency direction, **Then** the visible edge set updates within the current detail surface without a separate GET apply workflow.
2. **Given** an inventory item with multiple relationship families, **When** the operator narrows relationship scope, **Then** only matching edges remain and the current record context stays intact.
3. **Given** an inventory item with no edges for the selected scope, **When** the operator applies that scope, **Then** the surface shows the same no-results meaning as today without losing tenant or record context.
---
### User Story 2 - Filter Required Permissions In One Native Page Contract (Priority: P1)
On the tenant required-permissions page, an operator can adjust status, type, feature, and search state through one native interaction contract while preserving the current tenant, guidance, copy flows, and verification follow-up paths.
**Why this priority**: This page is already a live operator decision surface. If its primary controls remain pseudo-native, the page keeps teaching a separate contract for a core admin workflow.
**Independent Test**: Can be tested by loading the required-permissions page with and without deeplink query values, adjusting filters live, and verifying that the route tenant stays authoritative while results, counts, and copy payloads remain correct.
**Acceptance Scenarios**:
1. **Given** a tenant required-permissions page with stored verification data, **When** the operator changes status, type, feature, or search state, **Then** the matching permission rows, counts, and related guidance update without a separate plain-HTML filter bar contract.
2. **Given** deeplink query values for status, type, or search, **When** the page first loads, **Then** the page may seed initial state from the deeplink while keeping the route tenant authoritative.
3. **Given** tenant-like query values that point at a different tenant, **When** the page loads for the current tenant route, **Then** the current route tenant remains the only authoritative tenant scope.
---
### User Story 3 - Review Evidence Through A Native Workspace Table (Priority: P2)
On the evidence overview, a workspace operator can scan, filter, and open the next tenant evidence item through a native table surface with consistent empty-state and row-inspect behavior.
**Why this priority**: The page is clearly a tabular workspace review surface. Keeping it as a hand-built report table preserves bespoke behavior where native table semantics are a better fit.
**Independent Test**: Can be tested by loading the workspace evidence overview with multiple entitled tenants, applying an entitled tenant prefilter, and verifying that rows, empty state, and drilldown behavior remain workspace-safe while the page behaves like a native table surface.
**Acceptance Scenarios**:
1. **Given** multiple entitled tenant evidence rows, **When** the operator opens the overview, **Then** the page renders them through one native table contract with the expected columns, inspect model, and empty-state rules.
2. **Given** an entitled tenant prefilter, **When** the operator applies or clears it, **Then** only the authorized rows remain in scope and row drilldown stays workspace-safe.
3. **Given** a user without workspace membership, **When** that user requests the evidence overview, **Then** the route remains deny-as-not-found.
### Edge Cases
- Dependency edges may resolve to missing targets; fallback labels, missing-target markers, and helpful hints must remain intact after the control contract changes.
- Tenant required permissions may open from a deeplink with initial filter state; the deeplink may seed state, but it must not redefine authoritative tenant scope or remain the page's ongoing state source.
- Evidence overview may receive an unauthorized tenant prefilter; the page must not leak that tenant's existence through rows, counts, or drilldown affordances.
- Evidence overview may have no rows in the current scope; the replacement table surface must preserve a clear empty state and a single safe recovery action.
- If an apparently similar surface expands into shared detail micro-UI, monitoring-state, context-shell, diff viewer, or verification-report architecture, that work must stop and be deferred instead of being half-cleaned here.
## Requirements *(mandatory)*
**Constitution alignment (required):** This feature changes three existing operator-facing admin surfaces only. It introduces no new Microsoft Graph endpoint family, no new write workflow, and no new queued or scheduled run. Existing audit, preview, confirmation, and run-observability rules remain authoritative for the destinations these pages may already link to.
**Constitution alignment (PROP-001 / ABSTR-001 / PERSIST-001 / STATE-001 / BLOAT-001):** No new persistence, abstraction, or state family is introduced. The bias is replacement before layering: remove pseudo-native contracts and use native existing primitives rather than adding wrapper infrastructure.
**Constitution alignment (OPS-UX):** Not applicable. This cleanup does not create or repurpose an `OperationRun`.
**Constitution alignment (RBAC-UX):** The feature spans tenant-context admin routes under `/admin/t/{tenant}/...`, a tenant-specific admin route under `/admin/tenants/{tenant:external_id}/required-permissions`, and the workspace-context canonical route `/admin/evidence/overview`. Non-members remain `404`. In-scope members keep current capability and entitlement rules. Tenant required permissions must keep the route tenant authoritative. Evidence overview must continue to suppress unauthorized tenant rows and remain deny-as-not-found when workspace membership is absent. No new destructive action is introduced.
**Constitution alignment (OPS-EX-AUTH-001):** Not applicable. Authentication handshake behavior is unchanged.
**Constitution alignment (BADGE-001):** Existing badge semantics remain centralized. The cleanup must not introduce page-local status languages or bespoke badge mappings for dependency state, permission state, or evidence state.
**Constitution alignment (UI-FIL-001):** Native Filament forms, filters, tables, actions, and existing shared UI primitives must replace pseudo-native primary controls and table contracts where they are an appropriate fit. Local markup may remain only for domain-specific read-only content cells and must not recreate fake controls or a second state contract.
**Constitution alignment (UI-NAMING-001):** Operator-facing vocabulary remains consistent across labels, empty states, actions, and follow-up copy: `Dependencies`, `Direction`, `Relationship`, `Required permissions`, `Status`, `Type`, `Search`, `Evidence overview`, `Artifact truth`, `Freshness`, and `Next step` stay stable and are not replaced by implementation-first terms.
**Constitution alignment (DECIDE-001):** Inventory dependencies remains a secondary context surface attached to inventory detail. Tenant required permissions and evidence overview remain primary decision surfaces. Each must keep the first decision visible without cross-page reconstruction and avoid making the default experience larger or noisier than it is today.
**Constitution alignment (UI-CONST-001 / UI-SURF-001 / ACTSURF-001 / UI-HARD-001 / UI-EX-001 / UI-REVIEW-001 / HDR-001):** The affected surfaces remain one embedded detail micro-surface, one read-first workflow page, and one read-only workspace report. Each keeps one primary inspect model, keeps safe secondary actions clearly secondary, and does not open a hidden shell or cross-page-state refactor in this spec.
**Constitution alignment (ACTSURF-001 - action hierarchy):** No destructive actions are added. Evidence overview keeps clear filters separate from inspect. Tenant required permissions keeps filter controls separate from copy and external-guidance actions. Inventory dependencies keeps scope controls separate from target inspection.
**Constitution alignment (OPSURF-001):** Default-visible content remains operator-first: dependency scope and edges on inventory detail, permission counts and matching rows on required permissions, and truth, freshness, burden, and next step on evidence overview. Diagnostics remain explicitly secondary.
**Constitution alignment (UI-SEM-001 / LAYER-001 / TEST-TRUTH-001):** Direct mapping from existing domain truth to UI remains sufficient. The cleanup must not introduce a presenter framework, wrapper layer, or second semantics system just to hide raw HTML controls or a custom table contract. Tests focus on user-visible behavior, scope safety, and contract removal.
**Constitution alignment (Filament Action Surfaces):** The Action Surface Contract remains satisfied. A UI Action Matrix is included below. Each affected surface keeps one primary inspect or open model, redundant `View` actions are absent, empty placeholder action groups are absent, and destructive action placement rules remain satisfied because no destructive actions are introduced. UI-FIL-001 is satisfied, with limited exceptions only for custom read-only content rendering inside inventory dependencies and the required-permissions matrix.
**Constitution alignment (UX-001 - Layout & Information Architecture):** Tenant required permissions and inventory detail remain section-based, view-first surfaces; their cleanup must remove naked pseudo-controls without forcing a broader page redesign. Evidence overview must provide native table search, filters, row inspection, and a clear empty state. No wider layout re-architecture is in scope.
### Functional Requirements
- **FR-196-001**: The inventory dependencies surface on inventory item detail MUST NOT use a GET form with raw HTML select and button elements as its primary interaction surface.
- **FR-196-002**: Inventory dependency direction and relationship scope MUST live in a native page-owned or component-owned state model within the current detail page and MUST update the result set without a separate apply-and-reload workflow.
- **FR-196-003**: The inventory dependency fragment MUST NOT derive its primary interaction state from `request()` or manual query parsing inside the Blade fragment.
- **FR-196-004**: Inventory dependency cleanup MUST preserve the current functional outcome: direction options, relationship narrowing, edge resolution, missing-target handling, empty-state meaning, and current-record context stay equivalent.
- **FR-196-005**: Inventory dependency cleanup MUST preserve tenant scoping, record scoping, linked-target safety, and existing authorization behavior.
- **FR-196-006**: The tenant required-permissions page MUST NOT use plain HTML controls styled as fake native inputs for its primary status, type, feature, or search controls.
- **FR-196-007**: Tenant required-permissions filter state MUST be expressed through one native page-owned form or filter contract that matches the surrounding admin experience.
- **FR-196-008**: Query parameters on tenant required permissions MAY seed deeplink or initial state, but they MUST NOT redefine the authoritative route tenant or remain the page's primary body-state contract after initial load.
- **FR-196-009**: Tenant required permissions MUST preserve current functional depth, including overview counts, freshness messaging, feature narrowing, copy payload support, guidance links, and permission-row filtering.
- **FR-196-010**: Tenant required-permissions cleanup MUST NOT introduce a replacement wrapper pattern that merely restyles raw controls or recreates a second mini contract outside native page state.
- **FR-196-011**: Evidence overview MUST replace the hand-built primary report table with a native table surface that expresses columns, filters, empty state, and row inspection using native table semantics.
- **FR-196-012**: Evidence overview MUST provide one consistent inspect or open model for authorized rows and MUST preserve the current workspace-safe drilldown into tenant evidence.
- **FR-196-013**: Evidence overview MUST remove manual page-body query and Blade wiring that exists only because the report table is hand-built, while preserving entitled tenant prefilter behavior.
- **FR-196-014**: Evidence overview MUST preserve workspace boundary enforcement, entitled-tenant filtering, and deny-as-not-found behavior for users outside the workspace boundary.
- **FR-196-015**: Any additional cleanup hit included under this spec MUST share the same unnecessary nativity bypass, remain low to medium complexity, add no new product semantics, and avoid shared-family, shell, monitoring-state, and special-visualization work.
- **FR-196-016**: Any discovered related surface that crosses into shared detail micro-UI, monitoring state, context shell, verification report, diff or settings viewer, restore preview or result layouts, or other declared non-goal families MUST be documented and deferred instead of partially refactored here.
- **FR-196-017**: This cleanup MUST NOT introduce a new wrapper microframework, presenter layer, or cross-page UI abstraction whose main purpose is to hide the same non-native contract.
- **FR-196-018**: Each cleaned surface MUST remain operatorically at least as clear as before, with no loss of empty-state meaning, next-step clarity, scope signals, or inspect navigation.
- **FR-196-019**: Release close-out MUST list which surfaces were actually cleaned, which optional same-class low-risk hits were included, which related themes remained out of scope, and which follow-up specs were touched.
## 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 |
|---|---|---|---|---|---|---|---|---|---|---|
| Inventory item dependencies section | `/admin/t/{tenant}/inventory/inventory-items/{record}` | none added by this spec | Linked dependency target inside the section; no separate row menu | Open dependency target | none | none | Existing inventory item view header actions remain unchanged | n/a | no new audit event | Embedded detail sub-surface. Action Surface Contract remains satisfied because one inspect model exists for linked targets and no destructive actions are added. Native controls replace the GET/apply contract. |
| Tenant required permissions | `/admin/tenants/{tenant:external_id}/required-permissions` | none required; one safe native reset action is allowed if needed | Inline review matrix; no per-row inspect destination | none | none | State-specific reset or re-run verification CTA as appropriate | n/a | n/a | no new audit event | Inline workflow exemption remains legitimate. Copy payload and guidance actions stay secondary and non-destructive. Native filter contract replaces pseudo controls. |
| Evidence overview | `/admin/evidence/overview` | `Clear filters` when a prefilter is active | Full-row inspect into tenant evidence detail | none | none | `Clear filters` | n/a | n/a | no new audit event | Action Surface Contract remains satisfied with one primary inspect model, no redundant `View` row action, and no destructive action. Native table semantics replace the bespoke report table. |
### Key Entities *(include if feature involves data)*
- **Dependency edge filter state**: The current direction and relationship scope bound to one inventory item detail context.
- **Required permissions filter state**: The current status, type, selected features, and search state for one tenant's required-permissions workflow page.
- **Evidence overview row projection**: The workspace-scoped summary row for one entitled tenant, including artifact truth, freshness, burden, next step, and inspect destination.
- **Cleanup admission candidate**: A discovered extra surface that may only be included when it matches the same low-risk nativity-bypass problem class.
## Success Criteria *(mandatory)*
### Measurable Outcomes
- **SC-196-001**: Focused release validation and regression coverage pass for all three core surfaces with preserved scope safety, empty-state meaning, and result logic.
- **SC-196-002**: On all three core surfaces, operators can change the primary in-scope controls or inspect targets without relying on a separate GET apply workflow or a request-driven page-body contract.
- **SC-196-003**: Evidence overview presents 100% of authorized rows through one native table inspect model, and zero hand-built primary report tables remain within the boundaries of this spec.
- **SC-196-004**: Release validation finds zero primary plain HTML control surfaces on the three core pages whose only purpose is to imitate native admin controls.
- **SC-196-005**: Deeplink and prefilter behaviors continue to work for the targeted routes without allowing unauthorized tenant scope changes or cross-tenant row leakage.
- **SC-196-006**: Final close-out documentation explicitly records completed surfaces, deferred related themes, and any optional extra hits that were admitted under the shared rule.
## Assumptions
- Current domain semantics for dependency direction, relationship type, permission status, freshness, artifact truth, and evidence drilldown remain authoritative; this spec changes interaction contracts, not domain meaning.
- Inventory dependencies may keep domain-specific read-only edge rendering as long as primary controls and state ownership become native.
- Tenant required permissions may keep inline diagnostic content and guidance blocks as long as the primary filter contract becomes native.
- Evidence overview can adopt native table semantics without reopening broader monitoring information architecture questions.
- Optional extra hits are not required for success and may be omitted entirely if no low-risk candidate qualifies.
## Non-Goals
- Global context bar or workspace or tenant shell redesign
- Monitoring operations tab or page-state contract redesign
- Audit log selected-record or inspect duality cleanup
- Finding exceptions queue dual-inspect cleanup
- Baseline compare matrix or other special visualization surfaces
- Verification report viewer family or onboarding verification report variants
- Normalized diff, normalized settings, or other large detail micro-UI families
- Restore preview, restore results, or enterprise-detail read-only layout rework
- Raw anchor-to-component link consistency sweeps
- Badge-only, banner-only, or style-only polish work
- New constitution rules, new CI guardrail frameworks, or broad review-enforcement programs
## Dependencies
- Existing inventory dependency resolution and rendered-target services remain the authoritative source for dependency result logic.
- Existing tenant required-permissions view-model building remains the authoritative source for counts, row filtering, copy payloads, and guidance content.
- Existing evidence snapshot truth and row drilldown destinations remain the authoritative domain truth for evidence overview rows.
- Existing workspace-selection, tenant entitlement, and route-boundary rules remain authoritative and must be preserved by the cleanup.
- Follow-up specs for shared detail micro-UI, monitoring page-state, global context shell, UI constitution extension, and enforcement guardrails remain separate work and are not absorbed here.
## Definition of Done
- Inventory dependencies, tenant required permissions, and evidence overview are cleaned within the scope defined above.
- None of the three core surfaces relies primarily on fake native controls or a request-driven page-body contract.
- Evidence overview is no longer a hand-built primary report table.
- Tests covering the targeted functional and authorization behavior pass.
- Manual smoke checks confirm that dependency review, permission follow-up, and evidence review still feel clear and correct.
- No out-of-scope shell, monitoring-state, shared-family, or special-visualization topic is half-solved under this spec.
- Close-out documentation records completed work, deliberate deferrals, and any admitted same-class extra hits.

View File

@ -0,0 +1,166 @@
---
description: "Task list for Spec 196 hard Filament nativity cleanup implementation"
---
# Tasks: Hard Filament Nativity Cleanup
**Input**: Design documents from `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/196-hard-filament-nativity-cleanup/`
**Prerequisites**: `plan.md`, `spec.md`, `research.md`, `data-model.md`, `quickstart.md`, `contracts/filament-nativity-cleanup.logical.openapi.yaml`
**Tests**: Runtime behavior changes on existing Filament v5 / Livewire v4 operator surfaces require Pest feature, Livewire, RBAC, unit, and guard coverage. This task list adds or extends only the focused tests needed for the three in-scope surfaces.
**Operations**: This cleanup does not introduce new queued work or `OperationRun` flows. Existing linked follow-up paths remain unchanged.
**RBAC**: Tenant-context, route-tenant, workspace-membership, and entitled-tenant boundaries remain authoritative. Non-members stay `404`, and no new destructive action is added.
**UI Naming**: Keep existing operator terms stable: `Dependencies`, `Direction`, `Relationship`, `Required permissions`, `Status`, `Type`, `Search`, `Evidence overview`, `Artifact truth`, `Freshness`, and `Next step`.
**Filament UI Action Surfaces**: The feature replaces pseudo-native controls and a hand-built report table with native Filament or Livewire contracts without changing the current inspect destinations or adding new actions.
**Proportionality / Anti-Bloat**: Stay inside the three named surfaces plus one embedded `TableComponent`. Do not add new persistence, enums, presenters, or shared UI frameworks.
## Phase 1: Setup (Shared Review Inputs)
**Purpose**: Confirm the exact implementation entry points, native reference patterns, and focused regression baselines before editing the three in-scope surfaces.
- [ ] T001 Audit the current nativity-bypass entry points and native reference implementations in `apps/platform/app/Filament/Resources/InventoryItemResource.php`, `apps/platform/resources/views/filament/components/dependency-edges.blade.php`, `apps/platform/app/Filament/Pages/TenantRequiredPermissions.php`, `apps/platform/resources/views/filament/pages/tenant-required-permissions.blade.php`, `apps/platform/app/Filament/Pages/Monitoring/EvidenceOverview.php`, `apps/platform/resources/views/filament/pages/monitoring/evidence-overview.blade.php`, `apps/platform/app/Filament/Pages/Reviews/ReviewRegister.php`, `apps/platform/app/Filament/Pages/InventoryCoverage.php`, and `apps/platform/app/Livewire/BackupSetPolicyPickerTable.php`
- [ ] T002 [P] Audit the focused regression baselines in `apps/platform/tests/Feature/InventoryItemDependenciesTest.php`, `apps/platform/tests/Feature/Rbac/TenantRequiredPermissionsTrustedStateTest.php`, `apps/platform/tests/Feature/Evidence/EvidenceOverviewPageTest.php`, `apps/platform/tests/Feature/Filament/EvidenceOverviewDerivedStateMemoizationTest.php`, `apps/platform/tests/Unit/TenantRequiredPermissionsFilteringTest.php`, `apps/platform/tests/Unit/TenantRequiredPermissionsOverallStatusTest.php`, `apps/platform/tests/Unit/TenantRequiredPermissionsFeatureImpactTest.php`, `apps/platform/tests/Unit/TenantRequiredPermissionsFreshnessTest.php`, `apps/platform/tests/Unit/TenantRequiredPermissionsCopyPayloadTest.php`, and `apps/platform/tests/Feature/Guards/FilamentTableStandardsGuardTest.php`
---
## Phase 2: Foundational (Blocking Prerequisites)
**Purpose**: Create the shared Spec 196 test and guard scaffolding that all three surface refactors depend on.
**CRITICAL**: No user story work should begin until this phase is complete.
- [ ] T003 [P] Create the new Spec 196 surface-test entry points in `apps/platform/tests/Feature/Filament/InventoryItemDependencyEdgesTableTest.php` and `apps/platform/tests/Feature/Filament/TenantRequiredPermissionsPageTest.php`
- [ ] T004 [P] Review and, if newly applicable, extend shared native-table guard coverage for Spec 196 page-owned tables and faux-control regressions in `apps/platform/tests/Feature/Guards/FilamentTableStandardsGuardTest.php`
- [ ] T005 [P] Add shared regression coverage for mount-only query seeding versus authoritative scope in `apps/platform/tests/Feature/Rbac/TenantRequiredPermissionsTrustedStateTest.php` and `apps/platform/tests/Feature/Evidence/EvidenceOverviewPageTest.php`
**Checkpoint**: The shared Spec 196 test harness is in place, and later surface work can prove native state ownership without reopening scope or guard assumptions.
---
## Phase 3: User Story 1 - Review Dependencies Without A Foreign Workflow (Priority: P1) MVP
**Goal**: Keep inventory dependencies embedded on inventory item detail while replacing the GET apply contract with native component-owned state.
**Independent Test**: Open an inventory item detail page, change dependency direction and relationship scope, and verify that the same matching edges, missing-target markers, and empty states appear without a manual GET apply workflow.
### Tests for User Story 1
> **NOTE**: Write these tests first and confirm they fail before implementation.
- [ ] T006 [P] [US1] Extend `apps/platform/tests/Feature/InventoryItemDependenciesTest.php` with native component-state expectations for direction changes, relationship narrowing, empty states, and preserved target safety
- [ ] T007 [P] [US1] Add Livewire table-component coverage in `apps/platform/tests/Feature/Filament/InventoryItemDependencyEdgesTableTest.php` for mount state, filter updates, missing-target rendering, and tenant isolation
### Implementation for User Story 1
- [ ] T008 [US1] Create `apps/platform/app/Livewire/InventoryItemDependencyEdgesTable.php` as an embedded Filament `TableComponent` that owns direction and relationship state and queries rows through the current dependency services
- [ ] T009 [US1] Update `apps/platform/app/Filament/Resources/InventoryItemResource.php` and `apps/platform/resources/views/filament/components/dependency-edges.blade.php` to mount the embedded table component and remove the GET-form / `request()`-driven control contract while preserving target links, badges, and missing-target hints
**Checkpoint**: User Story 1 is complete when inventory detail keeps the same dependency meaning and target safety without switching operators into a foreign apply-and-reload workflow.
---
## Phase 4: User Story 2 - Filter Required Permissions In One Native Page Contract (Priority: P1)
**Goal**: Make tenant required permissions use one native page-owned filter and table contract while preserving route-tenant authority, summaries, guidance, and copy payloads.
**Independent Test**: Load the required-permissions page with and without deeplink query values, adjust filters live, and verify that the route tenant stays authoritative while rows, counts, guidance, and copy payloads remain correct.
### Tests for User Story 2
> **NOTE**: Write these tests first and confirm they fail before implementation.
- [ ] T010 [P] [US2] Extend `apps/platform/tests/Feature/Rbac/TenantRequiredPermissionsTrustedStateTest.php` for route-tenant authority, query-seeded status/type/search/features state, and ignored foreign-tenant query values
- [ ] T011 [P] [US2] Add native page-table coverage in `apps/platform/tests/Feature/Filament/TenantRequiredPermissionsPageTest.php` for filter updates, search, summary consistency, guidance visibility, copy payload continuity, and no-results states
- [ ] T012 [P] [US2] Keep filter-normalization, overall-status, feature-impact, freshness, and copy-payload invariants aligned in `apps/platform/tests/Unit/TenantRequiredPermissionsFilteringTest.php`, `apps/platform/tests/Unit/TenantRequiredPermissionsOverallStatusTest.php`, `apps/platform/tests/Unit/TenantRequiredPermissionsFeatureImpactTest.php`, `apps/platform/tests/Unit/TenantRequiredPermissionsFreshnessTest.php`, and `apps/platform/tests/Unit/TenantRequiredPermissionsCopyPayloadTest.php`
### Implementation for User Story 2
- [ ] T013 [US2] Convert `apps/platform/app/Filament/Pages/TenantRequiredPermissions.php` to `HasTable` / `InteractsWithTable` with native filters, native search, and mount-only query seeding
- [ ] T014 [US2] Align `apps/platform/resources/views/filament/pages/tenant-required-permissions.blade.php` and, if needed, `apps/platform/app/Services/Intune/TenantRequiredPermissionsViewModelBuilder.php` so summary counts, freshness, feature impacts, guidance, and copy payloads are derived from the same normalized native table state
**Checkpoint**: User Story 2 is complete when required permissions behaves like one native Filament page without losing tenant authority, summary clarity, or follow-up guidance.
---
## Phase 5: User Story 3 - Review Evidence Through A Native Workspace Table (Priority: P2)
**Goal**: Replace the hand-built evidence report table with a native workspace table that preserves entitled-tenant filtering, clear empty states, and one inspect model.
**Independent Test**: Load the workspace evidence overview with multiple entitled tenants, apply and clear an entitled tenant prefilter, and verify that rows, empty state, and drilldown behavior remain workspace-safe while the page behaves like a native table surface.
### Tests for User Story 3
> **NOTE**: Write these tests first and confirm they fail before implementation.
- [ ] T015 [P] [US3] Extend `apps/platform/tests/Feature/Evidence/EvidenceOverviewPageTest.php` for native table rendering, native search behavior, entitled-tenant seed and clear behavior, workspace-safe row drilldown, empty states, and deny-as-not-found enforcement
- [ ] T016 [P] [US3] Extend `apps/platform/tests/Feature/Filament/EvidenceOverviewDerivedStateMemoizationTest.php` and, if newly applicable, `apps/platform/tests/Feature/Guards/FilamentTableStandardsGuardTest.php` for DB-only derived-row rendering and the new page-owned native table contract
### Implementation for User Story 3
- [ ] T017 [US3] Convert `apps/platform/app/Filament/Pages/Monitoring/EvidenceOverview.php` to `HasTable` / `InteractsWithTable` with derived row callbacks, native filter and search state, entitled-tenant query seeding, and one inspect model
- [ ] T018 [US3] Replace the hand-built report table in `apps/platform/resources/views/filament/pages/monitoring/evidence-overview.blade.php` with a native table wrapper that preserves the clear-filter affordance and current drilldown copy
**Checkpoint**: User Story 3 is complete when evidence overview reads like one native workspace review table without leaking unauthorized tenant scope or losing the current drilldown path.
---
## Phase 6: Polish & Cross-Cutting Verification
**Purpose**: Run the focused verification pack, format the touched files, and record the final bounded scope outcome for Spec 196.
- [ ] T019 Run the focused Spec 196 Sail verification pack from `specs/196-hard-filament-nativity-cleanup/quickstart.md` against `apps/platform/tests/Feature/InventoryItemDependenciesTest.php`, `apps/platform/tests/Feature/Filament/InventoryItemDependencyEdgesTableTest.php`, `apps/platform/tests/Feature/Rbac/TenantRequiredPermissionsTrustedStateTest.php`, `apps/platform/tests/Feature/Filament/TenantRequiredPermissionsPageTest.php`, `apps/platform/tests/Feature/Evidence/EvidenceOverviewPageTest.php`, `apps/platform/tests/Feature/Filament/EvidenceOverviewDerivedStateMemoizationTest.php`, `apps/platform/tests/Feature/Guards/FilamentTableStandardsGuardTest.php`, `apps/platform/tests/Unit/TenantRequiredPermissionsFilteringTest.php`, `apps/platform/tests/Unit/TenantRequiredPermissionsOverallStatusTest.php`, `apps/platform/tests/Unit/TenantRequiredPermissionsFeatureImpactTest.php`, `apps/platform/tests/Unit/TenantRequiredPermissionsFreshnessTest.php`, and `apps/platform/tests/Unit/TenantRequiredPermissionsCopyPayloadTest.php`
- [ ] T020 Run `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent` and resolve formatting issues in the changed files under `apps/platform/app/`, `apps/platform/resources/views/filament/`, and `apps/platform/tests/`
- [ ] T021 Execute the manual smoke checklist in `specs/196-hard-filament-nativity-cleanup/quickstart.md` across the three cleaned surfaces and capture any sign-off notes needed for release close-out
- [ ] T022 Record the Spec 196 release close-out in `specs/196-hard-filament-nativity-cleanup/quickstart.md` with the final cleaned surfaces, any optional same-class extra hit decision, deferred themes, and touched follow-up specs
- [ ] T023 Verify the final close-out note in `specs/196-hard-filament-nativity-cleanup/quickstart.md` and the contract-modeled consumers, invariants, and non-goals in `specs/196-hard-filament-nativity-cleanup/contracts/filament-nativity-cleanup.logical.openapi.yaml` remain aligned with the implemented scope
---
## Dependencies
- Setup tasks T001-T002 precede all implementation work.
- Foundational tasks T003-T005 block all user stories.
- User Story 1 depends on Phase 2 and is the recommended MVP cut.
- User Story 2 depends on Phase 2 and can proceed after User Story 1 or in parallel once the shared guard and seed-state scaffolding are stable.
- User Story 3 depends on Phase 2 and should land after the shared guard scaffolding is stable so the new page-owned table contract is enforced consistently.
- Polish tasks T019-T023 depend on all selected user stories being complete.
## Parallel Execution Examples
- After T001, run T002 in parallel with any remaining setup review.
- In Phase 2, T003, T004, and T005 can run in parallel.
- In User Story 1, T006 and T007 can run in parallel.
- In User Story 2, T010, T011, and T012 can run in parallel.
- In User Story 3, T015 and T016 can run in parallel.
## Parallel Example: User Story 1
```bash
# Parallel test pass for US1
T006 Extend inventory dependency regression coverage
T007 Add Livewire table-component coverage
```
## Parallel Example: User Story 2
```bash
# Parallel test pass for US2
T010 Extend trusted-state authority coverage
T011 Add native required-permissions page-table coverage
T012 Keep required-permissions unit invariants aligned
```
## Parallel Example: User Story 3
```bash
# Parallel test pass for US3
T015 Extend evidence overview page coverage
T016 Extend memoization and guard coverage
```
## Implementation Strategy
- Start with Phase 1 and Phase 2 so the native-table guard and new surface-test entry points are ready before any refactor lands.
- Deliver User Story 1 first as the MVP because it removes the most obvious foreign workflow inside an existing detail page with the least scope spill.
- Deliver User Story 2 next to normalize the second P1 surface and prove route-tenant authority still wins over deeplink state.
- Finish with User Story 3 once the shared table guard is stable, then run the focused Sail pack and Pint formatting from Phase 6.