*/ public static function firstSlice(): array { return [ 'Policy' => [ 'table' => 'policies', 'model' => Policy::class, 'resource' => PolicyResource::class, 'tenant_relationship' => 'tenant', 'search_posture' => 'disabled', 'action_surface' => 'declared', 'action_surface_reason' => 'PolicyResource declares its action surface contract directly.', 'notes' => 'Policy search remains disabled until list/detail parity is fully migrated.', ], 'PolicyVersion' => [ 'table' => 'policy_versions', 'model' => PolicyVersion::class, 'resource' => PolicyVersionResource::class, 'tenant_relationship' => 'tenant', 'search_posture' => 'disabled', 'action_surface' => 'declared', 'action_surface_reason' => 'PolicyVersionResource declares its action surface contract directly.', 'notes' => 'Policy version search remains disabled until parity is guaranteed.', ], 'BackupSchedule' => [ 'table' => 'backup_schedules', 'model' => BackupSchedule::class, 'resource' => BackupScheduleResource::class, 'tenant_relationship' => 'tenant', 'search_posture' => 'not_applicable', 'action_surface' => 'declared', 'action_surface_reason' => 'BackupScheduleResource declares its action surface contract directly.', 'notes' => 'Backup schedules are not part of global search.', ], 'BackupSet' => [ 'table' => 'backup_sets', 'model' => BackupSet::class, 'resource' => BackupSetResource::class, 'tenant_relationship' => 'tenant', 'search_posture' => 'not_applicable', 'action_surface' => 'declared', 'action_surface_reason' => 'BackupSetResource declares its action surface contract directly.', 'notes' => 'Backup sets are not part of global search.', ], 'RestoreRun' => [ 'table' => 'restore_runs', 'model' => RestoreRun::class, 'resource' => RestoreRunResource::class, 'tenant_relationship' => 'tenant', 'search_posture' => 'not_applicable', 'action_surface' => 'baseline_exemption', 'action_surface_reason' => 'Restore run resource retrofit is deferred to the restore track and remains explicitly exempt in the action-surface baseline.', 'notes' => 'Restore runs are not part of global search.', ], 'Finding' => [ 'table' => 'findings', 'model' => Finding::class, 'resource' => FindingResource::class, 'tenant_relationship' => 'tenant', 'search_posture' => 'not_applicable', 'action_surface' => 'declared', 'action_surface_reason' => 'FindingResource declares its action surface contract directly.', 'notes' => 'Findings are not part of global search in the first slice.', ], 'FindingException' => [ 'table' => 'finding_exceptions', 'model' => FindingException::class, 'resource' => FindingExceptionResource::class, 'tenant_relationship' => 'tenant', 'search_posture' => 'disabled', 'action_surface' => 'declared', 'action_surface_reason' => 'FindingExceptionResource declares its action surface contract directly.', 'notes' => 'Finding exceptions stay off global search in the first rollout.', ], 'EvidenceSnapshot' => [ 'table' => 'evidence_snapshots', 'model' => EvidenceSnapshot::class, 'resource' => EvidenceSnapshotResource::class, 'tenant_relationship' => 'tenant', 'search_posture' => 'disabled', 'action_surface' => 'declared', 'action_surface_reason' => 'EvidenceSnapshotResource declares its action surface contract directly.', 'notes' => 'Evidence snapshots stay off global search until broader evidence discovery is introduced.', ], 'InventoryItem' => [ 'table' => 'inventory_items', 'model' => InventoryItem::class, 'resource' => InventoryItemResource::class, 'tenant_relationship' => 'tenant', 'search_posture' => 'not_applicable', 'action_surface' => 'declared', 'action_surface_reason' => 'InventoryItemResource declares its action surface contract directly.', 'notes' => 'Inventory items stay off global search.', ], 'EntraGroup' => [ 'table' => 'entra_groups', 'model' => EntraGroup::class, 'resource' => EntraGroupResource::class, 'tenant_relationship' => 'tenant', 'search_posture' => 'scoped', 'action_surface' => 'declared', 'action_surface_reason' => 'EntraGroupResource declares its action surface contract directly.', 'notes' => 'Directory groups already support tenant-safe global search.', ], ]; } /** * @return array */ public static function residualRolloutInventory(): array { return [ 'BackupItem' => [ 'table' => 'backup_items', 'likely_surface' => 'BackupSetResource::BackupItemsRelationManager', 'why_not_in_first_slice' => 'Covered through the backup-set relation manager rather than a standalone primary resource.', ], 'InventoryLink' => [ 'table' => 'inventory_links', 'likely_surface' => 'InventoryItemResource related-links affordances', 'why_not_in_first_slice' => 'Inventory links are subordinate navigation metadata and inherit tenant scope through inventory items.', ], 'EntraRoleDefinition' => [ 'table' => 'entra_role_definitions', 'likely_surface' => 'Entra admin-role reporting and findings reference flows', 'why_not_in_first_slice' => 'Read paths remain indirect via reporting and findings surfaces, so direct tenant-owned resource parity is deferred.', ], 'TenantPermission' => [ 'table' => 'tenant_permissions', 'likely_surface' => 'Permissions and onboarding diagnostics surfaces', 'why_not_in_first_slice' => 'Permission posture is enforced through dedicated diagnostics and onboarding flows, not a first-slice primary resource.', ], 'FindingExceptionDecision' => [ 'table' => 'finding_exception_decisions', 'likely_surface' => 'FindingExceptionResource decision history entries', 'why_not_in_first_slice' => 'Decision history is subordinate to the finding exception aggregate instead of a standalone primary resource.', ], 'FindingExceptionEvidenceReference' => [ 'table' => 'finding_exception_evidence_references', 'likely_surface' => 'FindingExceptionResource evidence sections', 'why_not_in_first_slice' => 'Evidence references are subordinate support records rendered inside finding exception detail.', ], ]; } /** * @return array */ public static function names(): array { return array_keys(self::firstSlice()); } /** * @return array{table: string, model: class-string, resource: class-string, tenant_relationship: string, search_posture: 'scoped'|'disabled'|'not_applicable', action_surface: 'declared'|'baseline_exemption', action_surface_reason: string, notes: string}|null */ public static function forModel(string $modelClass): ?array { foreach (self::firstSlice() as $family) { if ($family['model'] === $modelClass) { return $family; } } return null; } public static function searchPostureForModel(string $modelClass): ?string { return self::forModel($modelClass)['search_posture'] ?? null; } public static function supportsScopedGlobalSearch(string $modelClass): bool { return self::searchPostureForModel($modelClass) === 'scoped'; } /** * @return array}> */ public static function scopeExceptions(): array { return [ 'ProviderConnectionResource' => [ 'exception_kind' => 'workspace_admin_canonical_viewer', 'why_excepted' => 'Workspace-admin tenant-default surface referencing tenant-owned data without being part of the mandatory first-slice canon.', 'still_required_checks' => [ 'workspace membership', 'remembered tenant entitlement', 'capability gating on the destination action', ], ], 'OperationRunResource' => [ 'exception_kind' => 'workspace_owned_reference_surface', 'why_excepted' => 'Workspace-owned canonical monitoring surface that may deep-link into tenant-owned records only after entitlement checks.', 'still_required_checks' => [ 'workspace membership', 'tenant entitlement on deep links', 'record-owner congruence before rendering tenant-owned destinations', ], ], 'AlertDeliveryResource' => [ 'exception_kind' => 'deferred_family', 'why_excepted' => 'Mixed workspace-owned and tenant-bound semantics keep this surface outside the mandatory tenant-owned family set for the first slice.', 'still_required_checks' => [ 'workspace membership', 'tenant capability checks for tenant-bound mutations', ], ], ]; } /** * @return array */ public static function explicitScopeExceptions(): array { return array_map( static fn (array $exception): string => $exception['why_excepted'], self::scopeExceptions(), ); } /** * @return array */ public static function actionSurfaceBaselineExemptions(): array { $exemptions = []; foreach (self::firstSlice() as $family) { if ($family['action_surface'] !== 'baseline_exemption') { continue; } $exemptions[$family['resource']] = $family['action_surface_reason']; } return $exemptions; } }