diff --git a/apps/platform/app/Services/TenantConfiguration/ClaimGuard.php b/apps/platform/app/Services/TenantConfiguration/ClaimGuard.php index 9a15ba3f..a8b56728 100644 --- a/apps/platform/app/Services/TenantConfiguration/ClaimGuard.php +++ b/apps/platform/app/Services/TenantConfiguration/ClaimGuard.php @@ -12,6 +12,22 @@ final class ClaimGuard { + public function evaluateStatement(string $claim, bool $internalOperatorOnly = false): ClaimState + { + $tokens = $this->claimTokens($claim); + $registryScoped = $this->isRegistryScopedStatement($tokens); + + if ($this->hasUnsafeBroadCoverageClaim($tokens, $registryScoped)) { + return ClaimState::ClaimBlocked; + } + + if ($internalOperatorOnly && $registryScoped) { + return ClaimState::InternalOnly; + } + + return $registryScoped ? ClaimState::ClaimBlocked : ClaimState::ClaimLimited; + } + public function evaluate( ?string $scopeKey, CoverageLevel|string $requestedLevel, @@ -107,4 +123,142 @@ private function identityState(IdentityState|string|null $identityState): ?Ident return IdentityState::from($identityState); } + + /** + * @return list + */ + private function claimTokens(string $claim): array + { + $normalized = strtolower($claim); + $normalized = str_replace('%', ' percent ', $normalized); + $normalized = (string) preg_replace('/[^a-z0-9]+/', ' ', $normalized); + $normalized = (string) preg_replace('/\s+/', ' ', trim($normalized)); + + if ($normalized === '') { + return []; + } + + return explode(' ', $normalized); + } + + /** + * @param list $tokens + */ + private function isRegistryScopedStatement(array $tokens): bool + { + return $this->hasToken($tokens, 'registry') + && $this->hasToken($tokens, 'coverage') + && $this->hasToken($tokens, 'seeded') + && $this->hasToken($tokens, 'resource') + && $this->hasToken($tokens, 'type') + && $this->hasToken($tokens, 'entries'); + } + + /** + * @param list $tokens + */ + private function hasUnsafeBroadCoverageClaim(array $tokens, bool $registryScoped): bool + { + $hasCoverageSurface = $this->hasAnyToken($tokens, [ + 'coverage', + 'resource', + 'resources', + 'support', + 'supported', + 'tenant', + ]); + $hasWorkloadReference = $this->hasMicrosoft365Reference($tokens) + || $this->hasAnyToken($tokens, [ + 'entra', + 'exchange', + 'teams', + 'defender', + 'purview', + 'tcm', + ]) + || ($this->hasToken($tokens, 'security') && $this->hasToken($tokens, 'compliance')); + + if ($this->hasHundredPercent($tokens) && ! $registryScoped) { + return true; + } + + if ($this->hasCertificationTerm($tokens) && ($hasWorkloadReference || $hasCoverageSurface || $registryScoped)) { + return true; + } + + if ($this->hasRestoreReadyTerm($tokens) && ($hasWorkloadReference || $hasCoverageSurface || $registryScoped)) { + return true; + } + + if ($this->hasAnyToken($tokens, ['full', 'complete', 'all']) + && ($hasCoverageSurface || $this->hasToken($tokens, 'tenant')) + && ($hasWorkloadReference || $this->hasToken($tokens, 'tenant'))) { + return true; + } + + return false; + } + + /** + * @param list $tokens + */ + private function hasMicrosoft365Reference(array $tokens): bool + { + return $this->hasToken($tokens, 'm365') + || ($this->hasToken($tokens, 'microsoft') && $this->hasToken($tokens, '365')); + } + + /** + * @param list $tokens + */ + private function hasHundredPercent(array $tokens): bool + { + return $this->hasToken($tokens, '100') + && $this->hasToken($tokens, 'percent'); + } + + /** + * @param list $tokens + */ + private function hasCertificationTerm(array $tokens): bool + { + return $this->hasAnyToken($tokens, [ + 'certified', + 'certification', + 'certify', + 'certifies', + ]); + } + + /** + * @param list $tokens + */ + private function hasRestoreReadyTerm(array $tokens): bool + { + return $this->hasToken($tokens, 'restorable') + || ($this->hasToken($tokens, 'restore') && $this->hasAnyToken($tokens, ['ready', 'readiness'])) + || ($this->hasToken($tokens, 'restore') && $this->hasToken($tokens, 'coverage')); + } + + /** + * @param list $tokens + */ + private function hasAnyToken(array $tokens, array $expectedTokens): bool + { + foreach ($expectedTokens as $token) { + if ($this->hasToken($tokens, $token)) { + return true; + } + } + + return false; + } + + /** + * @param list $tokens + */ + private function hasToken(array $tokens, string $expectedToken): bool + { + return in_array($expectedToken, $tokens, true); + } } diff --git a/apps/platform/app/Services/TenantConfiguration/ResourceTypeRegistry.php b/apps/platform/app/Services/TenantConfiguration/ResourceTypeRegistry.php index ebde07ad..f11b14e9 100644 --- a/apps/platform/app/Services/TenantConfiguration/ResourceTypeRegistry.php +++ b/apps/platform/app/Services/TenantConfiguration/ResourceTypeRegistry.php @@ -116,6 +116,7 @@ public static function defaultDefinitions(): array 'is_active' => true, 'metadata' => ['kernel' => 'coverage_v2', 'provider_owned_source' => true], ], + ...self::m365RepresentativeDefinitions(), ]; } @@ -126,7 +127,10 @@ public static function defaultCanonicalTypes(): array { return array_values(array_map( static fn (array $definition): string => (string) $definition['canonical_type'], - self::defaultDefinitions(), + array_filter( + self::defaultDefinitions(), + static fn (array $definition): bool => ($definition['workload'] ?? null) === Workload::Intune->value, + ), )); } @@ -197,4 +201,88 @@ private function rowsForUpsert(array $definitions): array return $definition; }, $definitions); } + + /** + * @return list> + */ + private static function m365RepresentativeDefinitions(): array + { + return [ + ...self::m365ResourceDefinitions(Workload::Entra, [ + ['conditionalAccessPolicy', 'Conditional Access policy', RestoreTier::NotRestorable, 'high', ['conditionalAccessPolicies']], + ['securityDefaults', 'Security defaults', RestoreTier::NotRestorable, 'high', ['securityDefaultsPolicy']], + ['application', 'Application registration', RestoreTier::PreviewOnly, 'medium', ['applications']], + ['servicePrincipal', 'Service principal', RestoreTier::PreviewOnly, 'medium', ['servicePrincipals']], + ['roleDefinition', 'Role definition', RestoreTier::NotRestorable, 'high', ['directoryRoleDefinition']], + ['administrativeUnit', 'Administrative unit', RestoreTier::PreviewOnly, 'medium', ['administrativeUnits']], + ]), + ...self::m365ResourceDefinitions(Workload::Exchange, [ + ['transportRule', 'Transport rule', RestoreTier::NotRestorable, 'high', ['mailFlowRule']], + ['acceptedDomain', 'Accepted domain', RestoreTier::PreviewOnly, 'medium', ['acceptedDomains']], + ['sharedMailbox', 'Shared mailbox', RestoreTier::PreviewOnly, 'medium', ['sharedMailboxes']], + ['remoteDomain', 'Remote domain', RestoreTier::PreviewOnly, 'medium', ['remoteDomains']], + ['mailboxPlan', 'Mailbox plan', RestoreTier::PreviewOnly, 'medium', ['mailboxPlans']], + ['organizationConfig', 'Organization configuration', RestoreTier::NotRestorable, 'high', ['organizationConfiguration']], + ]), + ...self::m365ResourceDefinitions(Workload::Teams, [ + ['appPermissionPolicy', 'App permission policy', RestoreTier::PreviewOnly, 'medium', ['teamsAppPermissionPolicy']], + ['appSetupPolicy', 'App setup policy', RestoreTier::PreviewOnly, 'medium', ['teamsAppSetupPolicy']], + ['meetingPolicy', 'Meeting policy', RestoreTier::PreviewOnly, 'medium', ['teamsMeetingPolicy']], + ['messagingPolicy', 'Messaging policy', RestoreTier::PreviewOnly, 'medium', ['teamsMessagingPolicy']], + ['teamsUpdateManagementPolicy', 'Teams update management policy', RestoreTier::PreviewOnly, 'medium', ['updateManagementPolicy']], + ['voiceRoute', 'Voice route', RestoreTier::PreviewOnly, 'medium', ['onlineVoiceRoute']], + ]), + ...self::m365ResourceDefinitions(Workload::SecurityCompliance, [ + ['labelPolicy', 'Label policy', RestoreTier::NotRestorable, 'high', ['sensitivityLabelPolicy']], + ['retentionCompliancePolicy', 'Retention compliance policy', RestoreTier::NotRestorable, 'high', ['retentionPolicy']], + ['dlpCompliancePolicy', 'DLP compliance policy', RestoreTier::NotRestorable, 'high', ['dataLossPreventionPolicy']], + ['autoSensitivityLabelPolicy', 'Auto sensitivity label policy', RestoreTier::NotRestorable, 'high', ['autoLabelingPolicy']], + ['protectionAlert', 'Protection alert', RestoreTier::NotRestorable, 'high', ['alertPolicy']], + ['complianceTag', 'Compliance tag', RestoreTier::NotRestorable, 'high', ['retentionLabel']], + ]), + ]; + } + + /** + * @param list}> $entries + * @return list> + */ + private static function m365ResourceDefinitions(Workload $workload, array $entries): array + { + return array_map( + static fn (array $entry): array => [ + 'canonical_type' => $entry[0], + 'display_name' => $entry[1], + 'description' => sprintf('Registry-only Microsoft 365 %s planning entry.', str($workload->value)->replace('_', ' ')->headline()), + 'source_class' => SourceClass::Tcm->value, + 'workload' => $workload->value, + 'resource_class' => ResourceClass::Configuration->value, + 'support_state' => SupportState::OutOfScope->value, + 'default_coverage_level' => CoverageLevel::Detected->value, + 'default_evidence_state' => EvidenceState::NotCaptured->value, + 'default_identity_state' => IdentityState::Derived->value, + 'default_claim_state' => ClaimState::InternalOnly->value, + 'restore_tier' => $entry[2]->value, + 'allows_beta_claims' => false, + 'allows_graph_fallback_claims' => false, + 'allows_certified_claims' => false, + 'is_active' => true, + 'metadata' => [ + 'kernel' => 'coverage_v2', + 'provider_owned_source' => true, + 'registry_only' => true, + 'documentation_status' => 'documented_resource_catalog', + 'catalog_source' => 'm365_tcm_registry_seed', + 'catalog_last_reviewed_at' => '2026-06-26', + 'source_aliases' => $entry[4], + 'risk_tier' => $entry[3], + 'default_restore_posture' => $entry[2]->value, + 'is_full_catalog' => false, + 'catalog_import_batch' => 'spec_419_seeded_representative_manifest', + 'customer_claims_allowed' => false, + ], + ], + $entries, + ); + } } diff --git a/apps/platform/app/Services/TenantConfiguration/SupportedScopeResolver.php b/apps/platform/app/Services/TenantConfiguration/SupportedScopeResolver.php index 80fcc447..95dc4c0d 100644 --- a/apps/platform/app/Services/TenantConfiguration/SupportedScopeResolver.php +++ b/apps/platform/app/Services/TenantConfiguration/SupportedScopeResolver.php @@ -56,6 +56,7 @@ public static function defaultDefinitions(): array 'is_active' => true, 'metadata' => ['kernel' => 'coverage_v2', 'claim_surface' => 'future_activation'], ], + ...self::m365PlanningScopes(), ]; } @@ -252,4 +253,119 @@ private function normalizeResourceType(array|TenantConfigurationResourceType $re 'source_class' => (string) $resourceType['source_class'], ]; } + + /** + * @return list> + */ + private static function m365PlanningScopes(): array + { + $entra = [ + 'conditionalAccessPolicy', + 'securityDefaults', + 'application', + 'servicePrincipal', + 'roleDefinition', + 'administrativeUnit', + ]; + $exchange = [ + 'transportRule', + 'acceptedDomain', + 'sharedMailbox', + 'remoteDomain', + 'mailboxPlan', + 'organizationConfig', + ]; + $teams = [ + 'appPermissionPolicy', + 'appSetupPolicy', + 'meetingPolicy', + 'messagingPolicy', + 'teamsUpdateManagementPolicy', + 'voiceRoute', + ]; + $securityCompliance = [ + 'labelPolicy', + 'retentionCompliancePolicy', + 'dlpCompliancePolicy', + 'autoSensitivityLabelPolicy', + 'protectionAlert', + 'complianceTag', + ]; + $allRepresentative = [ + ...$entra, + ...$exchange, + ...$teams, + ...$securityCompliance, + ]; + + return [ + self::m365Scope('m365_tcm_registry_detected', 'M365 TCM registry detected', $allRepresentative, [ + 'documentation_status' => 'combined_catalog', + 'workload_documentation_status' => [ + 'entra' => 'documented_resource_catalog', + 'exchange' => 'documented_resource_catalog', + 'teams' => 'documented_resource_catalog', + 'security_compliance' => 'combined_catalog', + 'defender' => 'documented_overview_only', + 'purview' => 'combined_catalog', + 'tenantpilot' => 'internal', + 'unknown' => 'unknown', + ], + ]), + self::m365Scope('entra_tcm_registry_detected', 'Entra TCM registry detected', $entra, [ + 'workload' => 'entra', + 'documentation_status' => 'documented_resource_catalog', + ]), + self::m365Scope('exchange_tcm_registry_detected', 'Exchange TCM registry detected', $exchange, [ + 'workload' => 'exchange', + 'documentation_status' => 'documented_resource_catalog', + ]), + self::m365Scope('teams_tcm_registry_detected', 'Teams TCM registry detected', $teams, [ + 'workload' => 'teams', + 'documentation_status' => 'documented_resource_catalog', + ]), + self::m365Scope('security_compliance_tcm_registry_detected', 'Security and Compliance TCM registry detected', $securityCompliance, [ + 'workload' => 'security_compliance', + 'documentation_status' => 'combined_catalog', + ]), + self::m365Scope('m365_tcm_generic_future', 'M365 TCM generic future', [], [ + 'documentation_status' => 'graph_only', + 'future_only' => true, + 'generic_capture_active' => false, + ]), + self::m365Scope('m365_tcm_certified_none', 'M365 TCM certified none', [], [ + 'documentation_status' => 'internal', + 'certified_scope_available' => false, + ]), + ]; + } + + /** + * @param list $canonicalTypes + * @param array $metadata + * @return array + */ + private static function m365Scope(string $scopeKey, string $displayName, array $canonicalTypes, array $metadata): array + { + return [ + 'scope_key' => $scopeKey, + 'display_name' => $displayName, + 'description' => 'Inactive registry-only Microsoft 365 planning denominator.', + 'minimum_coverage_level' => CoverageLevel::Detected->value, + 'included_resource_types' => $canonicalTypes, + 'allow_beta' => false, + 'allow_graph_fallback' => false, + 'customer_claims_allowed' => false, + 'is_active' => false, + 'metadata' => [ + 'kernel' => 'coverage_v2', + 'registry_only' => true, + 'planning_status' => 'detected_only', + 'customer_claims_allowed' => false, + 'is_full_catalog' => false, + 'catalog_import_batch' => 'spec_419_seeded_representative_manifest', + ...$metadata, + ], + ]; + } } diff --git a/apps/platform/app/Support/TenantConfiguration/Workload.php b/apps/platform/app/Support/TenantConfiguration/Workload.php index 025cbe4c..6310ad28 100644 --- a/apps/platform/app/Support/TenantConfiguration/Workload.php +++ b/apps/platform/app/Support/TenantConfiguration/Workload.php @@ -7,6 +7,14 @@ enum Workload: string { case Intune = 'intune'; + case Entra = 'entra'; + case Exchange = 'exchange'; + case Teams = 'teams'; + case SecurityCompliance = 'security_compliance'; + case Defender = 'defender'; + case Purview = 'purview'; + case Tenantpilot = 'tenantpilot'; + case Unknown = 'unknown'; /** * @return list diff --git a/apps/platform/database/migrations/2026_06_26_000419_expand_tenant_configuration_workloads.php b/apps/platform/database/migrations/2026_06_26_000419_expand_tenant_configuration_workloads.php new file mode 100644 index 00000000..51f347ac --- /dev/null +++ b/apps/platform/database/migrations/2026_06_26_000419_expand_tenant_configuration_workloads.php @@ -0,0 +1,393 @@ +replaceWorkloadConstraint(self::WORKLOADS); + + DB::table('tenant_configuration_resource_types')->upsert( + $this->rowsForUpsert($this->resourceTypeDefinitions()), + ['canonical_type', 'source_class'], + [ + 'display_name', + 'description', + 'workload', + 'resource_class', + 'support_state', + 'default_coverage_level', + 'default_evidence_state', + 'default_identity_state', + 'default_claim_state', + 'restore_tier', + 'allows_beta_claims', + 'allows_graph_fallback_claims', + 'allows_certified_claims', + 'is_active', + 'metadata', + 'updated_at', + ], + ); + + DB::table('tenant_configuration_supported_scopes')->upsert( + $this->rowsForUpsert($this->supportedScopeDefinitions()), + ['scope_key'], + [ + 'display_name', + 'description', + 'minimum_coverage_level', + 'included_resource_types', + 'allow_beta', + 'allow_graph_fallback', + 'customer_claims_allowed', + 'is_active', + 'metadata', + 'updated_at', + ], + ); + } + + public function down(): void + { + DB::table('tenant_configuration_supported_scopes') + ->whereIn('scope_key', self::SPEC_419_SCOPE_KEYS) + ->where('metadata->catalog_import_batch', self::SPEC_419_IMPORT_BATCH) + ->delete(); + + DB::table('tenant_configuration_resource_types') + ->where('source_class', 'tcm') + ->where('workload', '!=', 'intune') + ->whereIn('canonical_type', self::SPEC_419_CANONICAL_TYPES) + ->where('metadata->catalog_import_batch', self::SPEC_419_IMPORT_BATCH) + ->delete(); + + $this->restoreWorkloadConstraintAfterSpec419Rollback(); + } + + /** + * @param list $values + */ + private function replaceWorkloadConstraint(array $values): void + { + if (DB::getDriverName() !== 'pgsql') { + return; + } + + DB::statement('ALTER TABLE tenant_configuration_resource_types DROP CONSTRAINT IF EXISTS tenant_config_resource_types_workload_check'); + DB::statement($this->checkIn('tenant_configuration_resource_types', 'workload', $values, 'tenant_config_resource_types_workload_check')); + } + + private function restoreWorkloadConstraintAfterSpec419Rollback(): void + { + if (DB::getDriverName() !== 'pgsql') { + return; + } + + $remainingWorkloads = DB::table('tenant_configuration_resource_types') + ->select('workload') + ->distinct() + ->pluck('workload') + ->map(static fn (mixed $workload): string => (string) $workload) + ->filter(static fn (string $workload): bool => $workload !== '') + ->values() + ->all(); + + $this->replaceWorkloadConstraint(array_values(array_unique([ + 'intune', + ...$remainingWorkloads, + ]))); + } + + /** + * @return list> + */ + private function resourceTypeDefinitions(): array + { + return [ + ...$this->m365ResourceDefinitions('entra', [ + ['conditionalAccessPolicy', 'Conditional Access policy', 'not_restorable', 'high', ['conditionalAccessPolicies']], + ['securityDefaults', 'Security defaults', 'not_restorable', 'high', ['securityDefaultsPolicy']], + ['application', 'Application registration', 'preview_only', 'medium', ['applications']], + ['servicePrincipal', 'Service principal', 'preview_only', 'medium', ['servicePrincipals']], + ['roleDefinition', 'Role definition', 'not_restorable', 'high', ['directoryRoleDefinition']], + ['administrativeUnit', 'Administrative unit', 'preview_only', 'medium', ['administrativeUnits']], + ]), + ...$this->m365ResourceDefinitions('exchange', [ + ['transportRule', 'Transport rule', 'not_restorable', 'high', ['mailFlowRule']], + ['acceptedDomain', 'Accepted domain', 'preview_only', 'medium', ['acceptedDomains']], + ['sharedMailbox', 'Shared mailbox', 'preview_only', 'medium', ['sharedMailboxes']], + ['remoteDomain', 'Remote domain', 'preview_only', 'medium', ['remoteDomains']], + ['mailboxPlan', 'Mailbox plan', 'preview_only', 'medium', ['mailboxPlans']], + ['organizationConfig', 'Organization configuration', 'not_restorable', 'high', ['organizationConfiguration']], + ]), + ...$this->m365ResourceDefinitions('teams', [ + ['appPermissionPolicy', 'App permission policy', 'preview_only', 'medium', ['teamsAppPermissionPolicy']], + ['appSetupPolicy', 'App setup policy', 'preview_only', 'medium', ['teamsAppSetupPolicy']], + ['meetingPolicy', 'Meeting policy', 'preview_only', 'medium', ['teamsMeetingPolicy']], + ['messagingPolicy', 'Messaging policy', 'preview_only', 'medium', ['teamsMessagingPolicy']], + ['teamsUpdateManagementPolicy', 'Teams update management policy', 'preview_only', 'medium', ['updateManagementPolicy']], + ['voiceRoute', 'Voice route', 'preview_only', 'medium', ['onlineVoiceRoute']], + ]), + ...$this->m365ResourceDefinitions('security_compliance', [ + ['labelPolicy', 'Label policy', 'not_restorable', 'high', ['sensitivityLabelPolicy']], + ['retentionCompliancePolicy', 'Retention compliance policy', 'not_restorable', 'high', ['retentionPolicy']], + ['dlpCompliancePolicy', 'DLP compliance policy', 'not_restorable', 'high', ['dataLossPreventionPolicy']], + ['autoSensitivityLabelPolicy', 'Auto sensitivity label policy', 'not_restorable', 'high', ['autoLabelingPolicy']], + ['protectionAlert', 'Protection alert', 'not_restorable', 'high', ['alertPolicy']], + ['complianceTag', 'Compliance tag', 'not_restorable', 'high', ['retentionLabel']], + ]), + ]; + } + + /** + * @param list}> $entries + * @return list> + */ + private function m365ResourceDefinitions(string $workload, array $entries): array + { + return array_map( + static fn (array $entry): array => [ + 'canonical_type' => $entry[0], + 'display_name' => $entry[1], + 'description' => 'Registry-only Microsoft 365 planning entry.', + 'source_class' => 'tcm', + 'workload' => $workload, + 'resource_class' => 'configuration', + 'support_state' => 'out_of_scope', + 'default_coverage_level' => 'detected', + 'default_evidence_state' => 'not_captured', + 'default_identity_state' => 'derived', + 'default_claim_state' => 'internal_only', + 'restore_tier' => $entry[2], + 'allows_beta_claims' => false, + 'allows_graph_fallback_claims' => false, + 'allows_certified_claims' => false, + 'is_active' => true, + 'metadata' => [ + 'kernel' => 'coverage_v2', + 'provider_owned_source' => true, + 'registry_only' => true, + 'documentation_status' => 'documented_resource_catalog', + 'catalog_source' => 'm365_tcm_registry_seed', + 'catalog_last_reviewed_at' => '2026-06-26', + 'source_aliases' => $entry[4], + 'risk_tier' => $entry[3], + 'default_restore_posture' => $entry[2], + 'is_full_catalog' => false, + 'catalog_import_batch' => self::SPEC_419_IMPORT_BATCH, + 'customer_claims_allowed' => false, + ], + ], + $entries, + ); + } + + /** + * @return list> + */ + private function supportedScopeDefinitions(): array + { + $entra = [ + 'conditionalAccessPolicy', + 'securityDefaults', + 'application', + 'servicePrincipal', + 'roleDefinition', + 'administrativeUnit', + ]; + $exchange = [ + 'transportRule', + 'acceptedDomain', + 'sharedMailbox', + 'remoteDomain', + 'mailboxPlan', + 'organizationConfig', + ]; + $teams = [ + 'appPermissionPolicy', + 'appSetupPolicy', + 'meetingPolicy', + 'messagingPolicy', + 'teamsUpdateManagementPolicy', + 'voiceRoute', + ]; + $securityCompliance = [ + 'labelPolicy', + 'retentionCompliancePolicy', + 'dlpCompliancePolicy', + 'autoSensitivityLabelPolicy', + 'protectionAlert', + 'complianceTag', + ]; + $allRepresentative = [ + ...$entra, + ...$exchange, + ...$teams, + ...$securityCompliance, + ]; + + return [ + $this->m365Scope('m365_tcm_registry_detected', 'M365 TCM registry detected', $allRepresentative, [ + 'documentation_status' => 'combined_catalog', + 'workload_documentation_status' => [ + 'entra' => 'documented_resource_catalog', + 'exchange' => 'documented_resource_catalog', + 'teams' => 'documented_resource_catalog', + 'security_compliance' => 'combined_catalog', + 'defender' => 'documented_overview_only', + 'purview' => 'combined_catalog', + 'tenantpilot' => 'internal', + 'unknown' => 'unknown', + ], + ]), + $this->m365Scope('entra_tcm_registry_detected', 'Entra TCM registry detected', $entra, [ + 'workload' => 'entra', + 'documentation_status' => 'documented_resource_catalog', + ]), + $this->m365Scope('exchange_tcm_registry_detected', 'Exchange TCM registry detected', $exchange, [ + 'workload' => 'exchange', + 'documentation_status' => 'documented_resource_catalog', + ]), + $this->m365Scope('teams_tcm_registry_detected', 'Teams TCM registry detected', $teams, [ + 'workload' => 'teams', + 'documentation_status' => 'documented_resource_catalog', + ]), + $this->m365Scope('security_compliance_tcm_registry_detected', 'Security and Compliance TCM registry detected', $securityCompliance, [ + 'workload' => 'security_compliance', + 'documentation_status' => 'combined_catalog', + ]), + $this->m365Scope('m365_tcm_generic_future', 'M365 TCM generic future', [], [ + 'documentation_status' => 'graph_only', + 'future_only' => true, + 'generic_capture_active' => false, + ]), + $this->m365Scope('m365_tcm_certified_none', 'M365 TCM certified none', [], [ + 'documentation_status' => 'internal', + 'certified_scope_available' => false, + ]), + ]; + } + + /** + * @param list $canonicalTypes + * @param array $metadata + * @return array + */ + private function m365Scope(string $scopeKey, string $displayName, array $canonicalTypes, array $metadata): array + { + return [ + 'scope_key' => $scopeKey, + 'display_name' => $displayName, + 'description' => 'Inactive registry-only Microsoft 365 planning denominator.', + 'minimum_coverage_level' => 'detected', + 'included_resource_types' => $canonicalTypes, + 'allow_beta' => false, + 'allow_graph_fallback' => false, + 'customer_claims_allowed' => false, + 'is_active' => false, + 'metadata' => [ + 'kernel' => 'coverage_v2', + 'registry_only' => true, + 'planning_status' => 'detected_only', + 'customer_claims_allowed' => false, + 'is_full_catalog' => false, + 'catalog_import_batch' => self::SPEC_419_IMPORT_BATCH, + ...$metadata, + ], + ]; + } + + /** + * @param list> $definitions + * @return list> + */ + private function rowsForUpsert(array $definitions): array + { + $now = now(); + + return array_map(static function (array $definition) use ($now): array { + if (isset($definition['included_resource_types'])) { + $definition['included_resource_types'] = json_encode($definition['included_resource_types'], JSON_THROW_ON_ERROR); + } + + $definition['metadata'] = json_encode($definition['metadata'] ?? null, JSON_THROW_ON_ERROR); + $definition['created_at'] = $now; + $definition['updated_at'] = $now; + + return $definition; + }, $definitions); + } + + /** + * @param list $values + */ + private function checkIn(string $table, string $column, array $values, string $constraint): string + { + return sprintf( + 'ALTER TABLE %s ADD CONSTRAINT %s CHECK (%s IN (%s))', + $table, + $constraint, + $column, + implode(', ', array_map( + static fn (string $value): string => DB::getPdo()->quote($value), + $values, + )), + ); + } +}; diff --git a/apps/platform/tests/Browser/Spec419M365RegistryOperatorSurfaceSmokeTest.php b/apps/platform/tests/Browser/Spec419M365RegistryOperatorSurfaceSmokeTest.php new file mode 100644 index 00000000..d71c730d --- /dev/null +++ b/apps/platform/tests/Browser/Spec419M365RegistryOperatorSurfaceSmokeTest.php @@ -0,0 +1,95 @@ +browser()->timeout(60_000); + +it('Spec419 smokes active M365 registry rows on the Coverage v2 operator surface', function (): void { + [$user, $environment] = spec419M365RegistryBrowserFixture(); + spec419AuthenticateM365RegistryBrowser($this, $user, $environment); + + visit(CoverageV2Readiness::getUrl(tenant: $environment, panel: 'admin')) + ->resize(1440, 1100) + ->waitForText('Coverage v2 Readiness') + ->assertSee('Spec419 Browser Environment') + ->assertSee('Resource type registry') + ->assertSee('Resource instances') + ->assertSee('Conditional Access policy') + ->assertSee('Application registration') + ->assertSee('Security defaults') + ->assertSee('Accepted domain') + ->assertSee('TCM') + ->assertSee('Out of scope') + ->assertSee('Detected') + ->assertSee('Not included') + ->assertSee('Internal only') + ->assertDontSee('100% Microsoft 365 coverage') + ->assertDontSee('100% M365 coverage') + ->assertDontSee('Full M365 coverage') + ->assertDontSee('Certified M365 coverage') + ->assertDontSee('Restore-ready M365 coverage') + ->assertDontSee('M365 TCM registry detected') + ->assertDontSee('m365_tcm_registry_detected') + ->assertScript('typeof window.Livewire !== "undefined"', true) + ->assertScript('(() => document.querySelectorAll("table tbody tr").length >= 8)()', true) + ->assertScript("(() => Array.from(document.querySelectorAll('table button, table a')).filter((element) => /^(Capture|Start capture|Restore|Restore ready|Certify|Certified|Publish|Export|Download|Report)$/i.test(element.textContent.trim())).length)()", 0) + ->assertScript("(() => ! /M365 full coverage|Microsoft 365 full coverage|M365 certified coverage|Microsoft 365 certified coverage|M365 restore-ready coverage|Microsoft 365 restore-ready coverage/i.test(document.body.textContent))()", true) + ->assertScript("(() => ! /m365_tcm_registry_detected|entra_tcm_registry_detected|exchange_tcm_registry_detected|teams_tcm_registry_detected|security_compliance_tcm_registry_detected|m365_tcm_generic_future|m365_tcm_certified_none/i.test(document.body.textContent))()", true) + ->assertScript("(() => performance.getEntriesByType('resource').filter((entry) => /graph\\.microsoft\\.com|\\/tcm\\b|provider-remote/i.test(entry.name)).length)()", 0) + ->assertNoJavaScriptErrors() + ->assertNoConsoleLogs() + ->screenshot(true, 'spec419-m365-registry-operator-surface'); +}); + +/** + * @return array{0: User, 1: ManagedEnvironment} + */ +function spec419M365RegistryBrowserFixture(): array +{ + $environment = ManagedEnvironment::factory()->active()->create([ + 'name' => 'Spec419 Browser Environment', + 'external_id' => 'spec419-browser-environment', + ]); + + [$user, $environment] = createUserWithTenant( + tenant: $environment, + role: 'owner', + workspaceRole: 'owner', + clearCapabilityCaches: true, + ); + + (new ResourceTypeRegistry)->syncDefaults(); + (new SupportedScopeResolver)->syncDefaults(); + + return [$user, $environment->refresh()]; +} + +function spec419AuthenticateM365RegistryBrowser( + mixed $test, + User $user, + ManagedEnvironment $environment, +): void { + $workspaceId = (int) $environment->workspace_id; + + $test->actingAs($user)->withSession([ + WorkspaceContext::SESSION_KEY => $workspaceId, + WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => [ + (string) $workspaceId => (int) $environment->getKey(), + ], + ]); + + session()->put(WorkspaceContext::SESSION_KEY, $workspaceId); + session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [ + (string) $workspaceId => (int) $environment->getKey(), + ]); +} diff --git a/apps/platform/tests/Feature/TenantConfiguration/Spec419M365RegistryExpansionTest.php b/apps/platform/tests/Feature/TenantConfiguration/Spec419M365RegistryExpansionTest.php new file mode 100644 index 00000000..215ac101 --- /dev/null +++ b/apps/platform/tests/Feature/TenantConfiguration/Spec419M365RegistryExpansionTest.php @@ -0,0 +1,258 @@ +value => 8, + Workload::Entra->value => 6, + Workload::Exchange->value => 6, + Workload::Teams->value => 6, + Workload::SecurityCompliance->value => 6, + ]; + $counts = TenantConfigurationResourceType::query() + ->selectRaw('workload, count(*) as aggregate') + ->groupBy('workload') + ->pluck('aggregate', 'workload') + ->map(fn (int|string $count): int => (int) $count); + + foreach ($expectedByWorkload as $workload => $expectedCount) { + expect($counts[$workload] ?? null)->toBe($expectedCount); + } + + expect(TenantConfigurationResourceType::query()->count())->toBe(32) + ->and(TenantConfigurationResourceType::query()->whereIn('workload', [ + Workload::Defender->value, + Workload::Purview->value, + Workload::Tenantpilot->value, + Workload::Unknown->value, + ])->exists())->toBeFalse() + ->and(TenantConfigurationResource::query()->count())->toBe(0) + ->and(TenantConfigurationResourceEvidence::query()->count())->toBe(0); + + $m365Rows = TenantConfigurationResourceType::query() + ->where('workload', '!=', Workload::Intune->value) + ->orderBy('canonical_type') + ->get(); + + foreach ($m365Rows as $row) { + expect($row->support_state)->toBe(SupportState::OutOfScope) + ->and($row->default_coverage_level)->toBe(CoverageLevel::Detected) + ->and($row->default_evidence_state)->toBe(EvidenceState::NotCaptured) + ->and($row->default_identity_state)->toBe(IdentityState::Derived) + ->and($row->default_claim_state)->toBe(ClaimState::InternalOnly) + ->and($row->allows_beta_claims)->toBeFalse() + ->and($row->allows_graph_fallback_claims)->toBeFalse() + ->and($row->allows_certified_claims)->toBeFalse() + ->and($row->metadata['registry_only'])->toBeTrue() + ->and($row->metadata['is_full_catalog'])->toBeFalse() + ->and($row->metadata['customer_claims_allowed'])->toBeFalse(); + + expect([ + RestoreTier::NotRestorable, + RestoreTier::PreviewOnly, + ])->toContain($row->restore_tier); + } +}); + +it('Spec419 persists inactive M365 planning scopes and leaves active Intune scopes unchanged', function (): void { + $requiredScopeKeys = [ + 'm365_tcm_registry_detected', + 'entra_tcm_registry_detected', + 'exchange_tcm_registry_detected', + 'teams_tcm_registry_detected', + 'security_compliance_tcm_registry_detected', + 'm365_tcm_generic_future', + 'm365_tcm_certified_none', + ]; + + $scopes = TenantConfigurationSupportedScope::query() + ->whereIn('scope_key', $requiredScopeKeys) + ->get() + ->keyBy('scope_key'); + + expect($scopes->keys()->sort()->values()->all())->toBe(collect($requiredScopeKeys)->sort()->values()->all()); + + foreach ($scopes as $scope) { + expect($scope->minimum_coverage_level)->toBe(CoverageLevel::Detected) + ->and($scope->customer_claims_allowed)->toBeFalse() + ->and($scope->is_active)->toBeFalse() + ->and($scope->metadata['registry_only'])->toBeTrue() + ->and($scope->metadata['is_full_catalog'])->toBeFalse(); + } + + expect(TenantConfigurationSupportedScope::query()->active()->orderBy('scope_key')->pluck('scope_key')->all()) + ->toBe([ + 'intune_tcm_core', + 'intune_tcm_core_with_graph_fallback', + ]) + ->and(TenantConfigurationSupportedScope::query() + ->whereIn('scope_key', [ + 'm365_full_coverage', + 'm365_certified', + 'all_microsoft_365_supported', + 'full_tenant_coverage', + 'full_m365_restore_ready', + ]) + ->exists())->toBeFalse(); + + $aggregate = $scopes['m365_tcm_registry_detected']; + + expect($aggregate->included_resource_types)->toHaveCount(24) + ->and($aggregate->included_resource_types)->toContain('conditionalAccessPolicy') + ->toContain('transportRule') + ->toContain('meetingPolicy') + ->toContain('dlpCompliancePolicy'); +}); + +it('Spec419 keeps registry sync idempotent and non-capturing', function (): void { + (new ResourceTypeRegistry)->syncDefaults(); + (new ResourceTypeRegistry)->syncDefaults(); + (new SupportedScopeResolver)->syncDefaults(); + (new SupportedScopeResolver)->syncDefaults(); + + expect(TenantConfigurationResourceType::query()->count())->toBe(32) + ->and(TenantConfigurationSupportedScope::query()->count())->toBe(9) + ->and(TenantConfigurationResource::query()->count())->toBe(0) + ->and(TenantConfigurationResourceEvidence::query()->count())->toBe(0); +}); + +it('Spec419 rollback deletes only exact Spec419 rows and preserves later non-Intune promotions', function (): void { + $futureResourceType = TenantConfigurationResourceType::query() + ->where('canonical_type', 'conditionalAccessPolicy') + ->where('source_class', 'tcm') + ->firstOrFail(); + $futureResourceType->update([ + 'metadata' => [ + ...$futureResourceType->metadata, + 'catalog_import_batch' => 'future_entra_promotion', + 'registry_only' => false, + 'future_spec' => 'preserve_non_intune_after_spec_419', + ], + ]); + + $futureScope = TenantConfigurationSupportedScope::query() + ->where('scope_key', 'm365_tcm_registry_detected') + ->firstOrFail(); + $futureScope->update([ + 'metadata' => [ + ...$futureScope->metadata, + 'catalog_import_batch' => 'future_m365_scope_promotion', + 'future_spec' => 'preserve_non_intune_after_spec_419', + ], + ]); + + $migration = require database_path('migrations/2026_06_26_000419_expand_tenant_configuration_workloads.php'); + $migration->down(); + + expect(TenantConfigurationResourceType::query() + ->where('canonical_type', 'conditionalAccessPolicy') + ->where('source_class', 'tcm') + ->exists())->toBeTrue() + ->and(TenantConfigurationResourceType::query() + ->where('canonical_type', 'securityDefaults') + ->where('source_class', 'tcm') + ->exists())->toBeFalse() + ->and(TenantConfigurationSupportedScope::query() + ->where('scope_key', 'm365_tcm_registry_detected') + ->exists())->toBeTrue() + ->and(TenantConfigurationSupportedScope::query() + ->where('scope_key', 'entra_tcm_registry_detected') + ->exists())->toBeFalse(); + + if (DB::getDriverName() === 'pgsql') { + $definition = DB::scalar(<<<'SQL' + SELECT pg_get_constraintdef(oid) + FROM pg_constraint + WHERE conrelid = 'tenant_configuration_resource_types'::regclass + AND conname = 'tenant_config_resource_types_workload_check' + SQL); + + expect((string) $definition)->toContain('intune') + ->toContain('entra'); + } +}); + +it('Spec419 does not add tenant ownership columns to registry definition tables', function (): void { + foreach (['tenant_configuration_resource_types', 'tenant_configuration_supported_scopes'] as $table) { + expect(Schema::getColumnListing($table)) + ->not->toContain('tenant_id') + ->not->toContain('provider_tenant_id') + ->not->toContain('entra_tenant_id') + ->not->toContain('workspace_id') + ->not->toContain('managed_environment_id') + ->not->toContain('provider_connection_id'); + } +}); + +it('Spec419 stays out of capture clients, v1 adapters, and workload mini-platforms', function (): void { + $files = [ + app_path('Services/TenantConfiguration/ResourceTypeRegistry.php'), + app_path('Services/TenantConfiguration/SupportedScopeResolver.php'), + app_path('Services/TenantConfiguration/ClaimGuard.php'), + app_path('Support/TenantConfiguration/Workload.php'), + database_path('migrations/2026_06_26_000419_expand_tenant_configuration_workloads.php'), + ]; + + $content = collect($files) + ->map(fn (string $file): string => file_get_contents($file) ?: '') + ->implode("\n"); + + expect($content) + ->not->toContain('GraphClientInterface') + ->not->toContain('ProviderGateway') + ->not->toContain('StartTenantConfigurationCapture') + ->not->toContain('CaptureTenantConfigurationEvidenceJob') + ->not->toContain('TenantConfigurationResource::query()->create') + ->not->toContain('TenantConfigurationResourceEvidence::query()->create') + ->not->toContain('Http::') + ->not->toContain('tenant_id') + ->not->toContain('ProviderV1') + ->not->toContain('LegacyAdapter') + ->not->toContain('dual_write') + ->not->toContain('namespace App\\Services\\TenantConfiguration\\Entra') + ->not->toContain('namespace App\\Services\\TenantConfiguration\\Exchange') + ->not->toContain('namespace App\\Services\\TenantConfiguration\\Teams') + ->not->toContain('namespace App\\Services\\TenantConfiguration\\Defender') + ->not->toContain('namespace App\\Services\\TenantConfiguration\\Purview') + ->not->toContain('namespace App\\Services\\TenantConfiguration\\SecurityCompliance') + ->not->toContain('create_entra_') + ->not->toContain('create_exchange_') + ->not->toContain('create_teams_') + ->not->toContain('create_defender_') + ->not->toContain('create_purview_') + ->not->toContain('create_security_compliance_'); +}); + +it('Spec419 updates the PostgreSQL workload check constraint', function (): void { + if (DB::getDriverName() !== 'pgsql') { + $this->markTestSkipped('PostgreSQL-specific workload check proof runs in the pgsql lane.'); + } + + $definition = DB::scalar(<<<'SQL' + SELECT pg_get_constraintdef(oid) + FROM pg_constraint + WHERE conrelid = 'tenant_configuration_resource_types'::regclass + AND conname = 'tenant_config_resource_types_workload_check' + SQL); + + foreach (Workload::values() as $workload) { + expect((string) $definition)->toContain($workload); + } +}); diff --git a/apps/platform/tests/Feature/TenantConfiguration/TenantConfigurationResourceTypeRegistryTest.php b/apps/platform/tests/Feature/TenantConfiguration/TenantConfigurationResourceTypeRegistryTest.php index 58ffcba9..7d5630e3 100644 --- a/apps/platform/tests/Feature/TenantConfiguration/TenantConfigurationResourceTypeRegistryTest.php +++ b/apps/platform/tests/Feature/TenantConfiguration/TenantConfigurationResourceTypeRegistryTest.php @@ -9,8 +9,8 @@ it('Spec414 persists the required initial Coverage v2 registry entries', function () { $registry = new ResourceTypeRegistry; - expect(TenantConfigurationResourceType::query()->count())->toBe(8) - ->and($registry->active())->toHaveCount(8) + expect(TenantConfigurationResourceType::query()->count())->toBe(32) + ->and($registry->active())->toHaveCount(32) ->and($registry->findActive('notificationMessageTemplate')?->source_class)->toBe(SourceClass::GraphV1Fallback) ->and($registry->findActive('roleScopeTag')?->source_class)->toBe(SourceClass::GraphBetaExperimental); }); @@ -21,7 +21,7 @@ $registry->syncDefaults(); $registry->syncDefaults(); - expect(TenantConfigurationResourceType::query()->count())->toBe(8) + expect(TenantConfigurationResourceType::query()->count())->toBe(32) ->and(TenantConfigurationResourceType::query() ->where('canonical_type', 'roleScopeTag') ->where('source_class', SourceClass::GraphBetaExperimental->value) diff --git a/apps/platform/tests/Feature/TenantConfiguration/TenantConfigurationSupportedScopeTest.php b/apps/platform/tests/Feature/TenantConfiguration/TenantConfigurationSupportedScopeTest.php index 9267f27a..7b35f1f2 100644 --- a/apps/platform/tests/Feature/TenantConfiguration/TenantConfigurationSupportedScopeTest.php +++ b/apps/platform/tests/Feature/TenantConfiguration/TenantConfigurationSupportedScopeTest.php @@ -60,7 +60,7 @@ $resolver->syncDefaults(); $resolver->syncDefaults(); - expect(TenantConfigurationSupportedScope::query()->count())->toBe(2) + expect(TenantConfigurationSupportedScope::query()->count())->toBe(9) ->and(TenantConfigurationSupportedScope::query() ->where('scope_key', 'intune_tcm_core') ->count())->toBe(1); diff --git a/apps/platform/tests/Unit/Support/TenantConfiguration/CoverageKernelValueTest.php b/apps/platform/tests/Unit/Support/TenantConfiguration/CoverageKernelValueTest.php index 1810c1ae..bda32e6b 100644 --- a/apps/platform/tests/Unit/Support/TenantConfiguration/CoverageKernelValueTest.php +++ b/apps/platform/tests/Unit/Support/TenantConfiguration/CoverageKernelValueTest.php @@ -18,7 +18,17 @@ 'graph_v1_fallback', 'graph_beta_experimental', ]) - ->and(Workload::values())->toBe(['intune']) + ->and(Workload::values())->toBe([ + 'intune', + 'entra', + 'exchange', + 'teams', + 'security_compliance', + 'defender', + 'purview', + 'tenantpilot', + 'unknown', + ]) ->and(ResourceClass::values())->toBe(['configuration']) ->and(SupportState::values())->toBe([ 'supported', diff --git a/apps/platform/tests/Unit/Support/TenantConfiguration/ResourceTypeRegistryTest.php b/apps/platform/tests/Unit/Support/TenantConfiguration/ResourceTypeRegistryTest.php index a6bb5b69..4b1c24c3 100644 --- a/apps/platform/tests/Unit/Support/TenantConfiguration/ResourceTypeRegistryTest.php +++ b/apps/platform/tests/Unit/Support/TenantConfiguration/ResourceTypeRegistryTest.php @@ -6,8 +6,9 @@ it('Spec414 defines the initial Coverage v2 resource type registry', function () { $definitions = collect(ResourceTypeRegistry::defaultDefinitions()); + $initialDefinitions = $definitions->take(8); - expect($definitions->pluck('canonical_type')->all())->toBe([ + expect($initialDefinitions->pluck('canonical_type')->all())->toBe([ 'deviceAndAppManagementAssignmentFilter', 'deviceEnrollmentLimitRestriction', 'deviceEnrollmentPlatformRestriction', @@ -18,7 +19,7 @@ 'roleScopeTag', ]); - expect($definitions->where('source_class', 'tcm'))->toHaveCount(6) + expect($definitions->where('source_class', 'tcm')->where('workload', 'intune'))->toHaveCount(6) ->and($definitions->firstWhere('canonical_type', 'notificationMessageTemplate')['source_class'])->toBe('graph_v1_fallback') ->and($definitions->firstWhere('canonical_type', 'roleScopeTag')['source_class'])->toBe('graph_beta_experimental'); }); diff --git a/apps/platform/tests/Unit/Support/TenantConfiguration/Spec419M365ClaimGuardTest.php b/apps/platform/tests/Unit/Support/TenantConfiguration/Spec419M365ClaimGuardTest.php new file mode 100644 index 00000000..9a12e364 --- /dev/null +++ b/apps/platform/tests/Unit/Support/TenantConfiguration/Spec419M365ClaimGuardTest.php @@ -0,0 +1,40 @@ +evaluateStatement($claim))->toBe(ClaimState::ClaimBlocked); +})->with([ + '100 percent Microsoft 365' => '100% Microsoft 365 coverage', + '100 percent M365' => '100% M365 coverage', + 'full M365' => 'Full M365 coverage', + 'full Microsoft 365' => 'Full Microsoft 365 coverage', + 'certified M365' => 'Certified M365 coverage', + 'M365 certified' => 'M365 certified coverage', + 'certified Microsoft 365' => 'Certified Microsoft 365 coverage', + 'Microsoft 365 certified' => 'Microsoft 365 certified coverage', + 'certified coverage across M365' => 'Certified coverage across M365', + 'restore ready M365' => 'Restore-ready M365 coverage', + 'M365 restore ready' => 'M365 restore-ready coverage', + 'restore ready Microsoft 365' => 'Restore ready Microsoft 365 coverage', + 'complete tenant' => 'Complete tenant coverage', + 'full tenant' => 'Full tenant coverage', + 'all Microsoft 365 resources' => 'All Microsoft 365 resources supported', + 'M365 all resources' => 'M365 all resources supported', + 'all TCM resources' => 'All TCM resources certified', + 'certified seeded registry' => 'Certified registry coverage for seeded Entra resource type entries', + 'restore ready seeded registry' => 'Restore-ready registry coverage for seeded Entra resource type entries', +]); + +it('Spec419 permits only internal registry-scoped denominator wording', function (): void { + $guard = new ClaimGuard; + $statement = '100% registry coverage for seeded Entra resource type entries'; + + expect($guard->evaluateStatement($statement))->toBe(ClaimState::ClaimBlocked) + ->and($guard->evaluateStatement($statement, internalOperatorOnly: true))->toBe(ClaimState::InternalOnly) + ->and($guard->evaluateStatement('Detected registry coverage for seeded Exchange resource type entries', internalOperatorOnly: true))->toBe(ClaimState::InternalOnly) + ->and($guard->evaluateStatement('Detected registry coverage for seeded Exchange resource type entries'))->toBe(ClaimState::ClaimBlocked); +}); diff --git a/apps/platform/tests/Unit/Support/TenantConfiguration/Spec419M365WorkloadRegistryTest.php b/apps/platform/tests/Unit/Support/TenantConfiguration/Spec419M365WorkloadRegistryTest.php new file mode 100644 index 00000000..40253ba0 --- /dev/null +++ b/apps/platform/tests/Unit/Support/TenantConfiguration/Spec419M365WorkloadRegistryTest.php @@ -0,0 +1,171 @@ +toBe([ + 'intune', + 'entra', + 'exchange', + 'teams', + 'security_compliance', + 'defender', + 'purview', + 'tenantpilot', + 'unknown', + ]); + + expect(ResourceTypeRegistry::defaultCanonicalTypes())->toBe([ + 'deviceAndAppManagementAssignmentFilter', + 'deviceEnrollmentLimitRestriction', + 'deviceEnrollmentPlatformRestriction', + 'deviceEnrollmentStatusPageWindows10', + 'appProtectionPolicyAndroid', + 'appProtectionPolicyiOS', + 'notificationMessageTemplate', + 'roleScopeTag', + ]); +}); + +it('Spec419 seeds representative M365 resource types as registry-only detected entries', function (): void { + $definitions = collect(ResourceTypeRegistry::defaultDefinitions()); + $expectedByWorkload = [ + Workload::Entra->value => [ + 'conditionalAccessPolicy', + 'securityDefaults', + 'application', + 'servicePrincipal', + 'roleDefinition', + 'administrativeUnit', + ], + Workload::Exchange->value => [ + 'transportRule', + 'acceptedDomain', + 'sharedMailbox', + 'remoteDomain', + 'mailboxPlan', + 'organizationConfig', + ], + Workload::Teams->value => [ + 'appPermissionPolicy', + 'appSetupPolicy', + 'meetingPolicy', + 'messagingPolicy', + 'teamsUpdateManagementPolicy', + 'voiceRoute', + ], + Workload::SecurityCompliance->value => [ + 'labelPolicy', + 'retentionCompliancePolicy', + 'dlpCompliancePolicy', + 'autoSensitivityLabelPolicy', + 'protectionAlert', + 'complianceTag', + ], + ]; + + expect($definitions)->toHaveCount(32) + ->and($definitions->reject(fn (array $definition): bool => $definition['workload'] === Workload::Intune->value))->toHaveCount(24); + + foreach ($expectedByWorkload as $workload => $canonicalTypes) { + $workloadDefinitions = $definitions->where('workload', $workload)->values(); + + expect($workloadDefinitions->pluck('canonical_type')->all())->toBe($canonicalTypes); + + foreach ($workloadDefinitions as $definition) { + expect($definition['source_class'])->toBe(SourceClass::Tcm->value) + ->and($definition['support_state'])->toBe(SupportState::OutOfScope->value) + ->and($definition['default_coverage_level'])->toBe(CoverageLevel::Detected->value) + ->and($definition['default_evidence_state'])->toBe(EvidenceState::NotCaptured->value) + ->and($definition['default_identity_state'])->toBe(IdentityState::Derived->value) + ->and($definition['default_claim_state'])->toBe(ClaimState::InternalOnly->value) + ->and($definition['allows_beta_claims'])->toBeFalse() + ->and($definition['allows_graph_fallback_claims'])->toBeFalse() + ->and($definition['allows_certified_claims'])->toBeFalse() + ->and($definition['metadata']['registry_only'])->toBeTrue() + ->and($definition['metadata']['is_full_catalog'])->toBeFalse() + ->and($definition['metadata']['customer_claims_allowed'])->toBeFalse() + ->and($definition['metadata']['documentation_status'])->toBe('documented_resource_catalog'); + + expect([ + RestoreTier::NotRestorable->value, + RestoreTier::PreviewOnly->value, + ])->toContain($definition['restore_tier']); + } + } + + expect($definitions->where('workload', Workload::Defender->value))->toHaveCount(0) + ->and($definitions->where('workload', Workload::Purview->value))->toHaveCount(0) + ->and($definitions->firstWhere('canonical_type', 'dlpCompliancePolicy')['metadata']['source_aliases']) + ->toContain('dataLossPreventionPolicy'); +}); + +it('Spec419 classifies high-risk restore tiers conservatively', function (): void { + $definitions = collect(ResourceTypeRegistry::defaultDefinitions())->keyBy('canonical_type'); + + foreach (['conditionalAccessPolicy', 'securityDefaults', 'roleDefinition', 'transportRule', 'organizationConfig', 'labelPolicy', 'retentionCompliancePolicy', 'dlpCompliancePolicy', 'autoSensitivityLabelPolicy', 'protectionAlert', 'complianceTag'] as $canonicalType) { + expect($definitions[$canonicalType]['restore_tier'])->toBe(RestoreTier::NotRestorable->value); + } + + foreach (['appPermissionPolicy', 'appSetupPolicy', 'meetingPolicy', 'messagingPolicy', 'teamsUpdateManagementPolicy', 'voiceRoute'] as $canonicalType) { + expect($definitions[$canonicalType]['restore_tier'])->toBe(RestoreTier::PreviewOnly->value); + } + + expect($definitions->where('workload', '!=', Workload::Intune->value)->pluck('restore_tier')->unique()->values()->all()) + ->not->toContain(RestoreTier::Restorable->value); +}); + +it('Spec419 documents planning-scope provenance without advertising certification', function (): void { + $scopes = collect(SupportedScopeResolver::defaultDefinitions())->keyBy('scope_key'); + + expect($scopes)->toHaveKeys([ + 'm365_tcm_registry_detected', + 'entra_tcm_registry_detected', + 'exchange_tcm_registry_detected', + 'teams_tcm_registry_detected', + 'security_compliance_tcm_registry_detected', + 'm365_tcm_generic_future', + 'm365_tcm_certified_none', + ]); + + expect($scopes['m365_tcm_registry_detected']['metadata']['workload_documentation_status'])->toBe([ + 'entra' => 'documented_resource_catalog', + 'exchange' => 'documented_resource_catalog', + 'teams' => 'documented_resource_catalog', + 'security_compliance' => 'combined_catalog', + 'defender' => 'documented_overview_only', + 'purview' => 'combined_catalog', + 'tenantpilot' => 'internal', + 'unknown' => 'unknown', + ]) + ->and($scopes['m365_tcm_generic_future']['metadata']['documentation_status'])->toBe('graph_only') + ->and($scopes['m365_tcm_generic_future']['metadata']['generic_capture_active'])->toBeFalse() + ->and($scopes['m365_tcm_certified_none']['metadata']['certified_scope_available'])->toBeFalse(); + + foreach ($scopes->only([ + 'm365_tcm_registry_detected', + 'entra_tcm_registry_detected', + 'exchange_tcm_registry_detected', + 'teams_tcm_registry_detected', + 'security_compliance_tcm_registry_detected', + 'm365_tcm_generic_future', + 'm365_tcm_certified_none', + ]) as $scope) { + expect($scope['is_active'])->toBeFalse() + ->and($scope['customer_claims_allowed'])->toBeFalse() + ->and($scope['minimum_coverage_level'])->toBe(CoverageLevel::Detected->value) + ->and($scope['metadata']['registry_only'])->toBeTrue() + ->and($scope['metadata']['is_full_catalog'])->toBeFalse(); + } +}); diff --git a/specs/419-m365-tcm-workload-registry-expansion/checklists/requirements.md b/specs/419-m365-tcm-workload-registry-expansion/checklists/requirements.md new file mode 100644 index 00000000..a13bb0b2 --- /dev/null +++ b/specs/419-m365-tcm-workload-registry-expansion/checklists/requirements.md @@ -0,0 +1,136 @@ +# Requirements Checklist: Spec 419 - M365 TCM Workload Registry Expansion + +## Preparation Checklist + +- [x] Candidate is user-provided, not auto-selected from the empty active candidate queue. +- [x] Spec 414 is completed/validated dependency context only. +- [x] Spec 415 is completed/validated dependency context only. +- [x] Spec 417 is completed/validated dependency context only. +- [x] Spec 418 is completed/validated dependency context only. +- [x] No existing `specs/419-*` package was found before creation. +- [x] Existing Coverage v2 registry, supported scopes, enums, `ResourceTypeRegistry`, and `ClaimGuard` were verified as repo truth. +- [x] Draft-to-repo deviations are documented. +- [x] No application implementation was performed during preparation. + +## Scope Checklist + +- [x] Scope is registry expansion only. +- [x] No capture implementation is in scope. +- [x] No compare/render/restore/certification is in scope. +- [x] No customer-facing claims are in scope. +- [x] No new primary navigation or UI route is in scope. +- [x] No domain-specific mini-platform is in scope. +- [x] No runtime Microsoft docs fetch is in scope. + +## Product Surface Checklist + +- [x] UI Surface Impact records existing Spec 418 operator-surface data impact without runtime UI code scope. +- [x] Product Surface Impact covers data-driven existing-surface impact. +- [x] Browser proof is required if active rows/scopes render, or N/A only with proof that no rendered output changed. +- [x] Human Product Sanity is required if active rows/scopes render, or N/A only with proof that no rendered output changed. +- [x] Product Surface exceptions are `none`. +- [x] Stop-and-amend rule exists for any runtime UI file, route, navigation, action, report, download, or rendered label change beyond data-driven existing registry display. + +## Workload Requirements Specified + +- [x] Entra workload registration is required. +- [x] Exchange workload registration is required. +- [x] Teams workload registration is required. +- [x] Security and Compliance workload registration is required. +- [x] Defender safe overview/combined representation is required. +- [x] Purview safe overview/combined representation is required. +- [x] Defender/Purview representation uses aggregate supported-scope metadata, not fake certified resource types. +- [x] `tenantpilot` and `unknown` workload posture is covered. + +## Resource Type Requirements Specified + +- [x] Entra representative entries are listed. +- [x] Exchange representative entries are listed. +- [x] Teams representative entries are listed. +- [x] Security and Compliance representative entries are listed. +- [x] Defender/Purview uncertainty is explicit. +- [x] Full vs seeded/partial catalog decision is explicit. +- [x] Partial list must not be presented as full. + +## Source / Support State Requirements Specified + +- [x] TCM entries use `source_class = tcm`. +- [x] Current repo source classes remain authoritative unless amended with proportionality proof. +- [x] New non-Intune entries default to detected/registry-only. +- [x] No new entry defaults to content-backed. +- [x] No new entry defaults to comparable. +- [x] No new entry defaults to renderable. +- [x] No new entry defaults to certified. +- [x] No new entry defaults to restore-ready. +- [x] Existing repo restore tiers are mapped safely: `not_restorable` or `preview_only`, never `restorable`. + +## Supported Scope Requirements Specified + +- [x] Registry-only M365 detected scope is required. +- [x] Per-workload registry detected scopes are required. +- [x] Future generic scope is clearly future-only. +- [x] Certified M365 scope is explicitly none. +- [x] Broad full/certified M365 scope names are forbidden. + +## Claim Guard Requirements Specified + +- [x] Broad M365 coverage claims must be blocked. +- [x] Certified M365 claims must be blocked. +- [x] Restore-ready M365 claims must be blocked. +- [x] Registry-only claims are internal/operator and denominator-scoped. +- [x] Percent claims require explicit denominator and registry-only wording. + +## No Runtime Capture Requirements Specified + +- [x] No Graph/TCM calls may be added. +- [x] No runtime Microsoft docs fetch may be added. +- [x] No capture job/action may be added. +- [x] No concrete resources/evidence may be created by registry expansion. +- [x] No OperationRun-producing workflow is planned. + +## No Legacy / Ownership Requirements Specified + +- [x] No `tenant_id`. +- [x] No old gap taxonomy. +- [x] No v1-to-v2 adapter. +- [x] No fallback reader. +- [x] No dual writes. +- [x] Provider-native tenant/directory/account IDs remain metadata only. + +## Test Requirements Specified + +- [x] Unit tests cover workloads, manifest/defaults, claims, restore tiers, documentation status, and partial-vs-full catalog behavior. +- [x] Feature/static guards cover registry/scopes/no-overclaim/no-capture/no-mini-platform/no-tenant-id. +- [x] No real Graph/TCM/provider calls are allowed. +- [x] Test lane impact is documented. +- [x] Browser proof is required if active rows/scopes render on the existing Spec 418 operator surface. + +## Future Implementation Gate + +- [x] M365 workload registry expansion exists. +- [x] New workload entries are registry-only/detected by default. +- [x] Representative resource types exist. +- [x] Full vs partial catalog status is explicit. +- [x] Claim Guard blocks broad M365/certified/restore claims. +- [x] No runtime capture is added. +- [x] No customer-facing claim is activated. +- [x] No `tenant_id` is introduced. +- [x] No mini-platform tables/classes are introduced. +- [x] Focused tests pass. +- [x] Product Surface data-impact decision is confirmed, including browser/Human Product Sanity proof or exact N/A proof. + +## Spec Readiness Gate + +- [x] `spec.md` exists. +- [x] `plan.md` exists. +- [x] `tasks.md` exists. +- [x] Requirements are bounded and testable. +- [x] Plan identifies likely affected repo surfaces. +- [x] Tasks are ordered, small, verifiable, and include validation. +- [x] Product Surface, RBAC/no-UI, workspace/provider isolation, OperationRun/no-run, evidence/result truth, provider boundary, no-legacy, and test governance are addressed. +- [x] No open question blocks safe implementation. + +## Gate Results + +- [x] Candidate Selection Gate: PASS. +- [x] Spec Readiness Gate: PASS for preparation; implementation must still follow `tasks.md`. diff --git a/specs/419-m365-tcm-workload-registry-expansion/implementation-report.md b/specs/419-m365-tcm-workload-registry-expansion/implementation-report.md new file mode 100644 index 00000000..b01db4e6 --- /dev/null +++ b/specs/419-m365-tcm-workload-registry-expansion/implementation-report.md @@ -0,0 +1,136 @@ +# Implementation Report: Spec 419 - M365 TCM Workload Registry Expansion + +## Preflight + +- Date: 2026-06-26 +- Branch: `419-m365-tcm-workload-registry-expansion` +- HEAD before implementation: `4aaec352 feat: add coverage v2 operator surface (#485)` +- Dirty state before implementation: active `specs/419-m365-tcm-workload-registry-expansion/` artifacts were untracked; no unrelated tracked code changes were present. +- Dirty state after implementation: expected Spec 419 code/test/spec artifacts only. +- Activated skills/gates: + - `spec-kit-implementation-loop`: active implementation workflow. + - `.agent workflows/spec-readiness-gate`: PASS; spec, plan, tasks, and checklist existed with no blocking open questions. + - `.agent repo-contracts/product-surface-gate`: PASS with existing-surface data impact. + - `.agent temporary-migrations/tcm-cutover-guard`: PASS; no capture, restore, customer claim, or UI activation. + - `pest-testing`: used for Pest 4 unit/feature/browser tests. + - `browsertest`: used for focused existing-surface browser proof. +- Dependency context: Specs 414, 415, 417, and 418 were read-only context and were not modified. +- Stop conditions: none hit. + +## Files Changed + +- `apps/platform/app/Support/TenantConfiguration/Workload.php` +- `apps/platform/app/Services/TenantConfiguration/ResourceTypeRegistry.php` +- `apps/platform/app/Services/TenantConfiguration/SupportedScopeResolver.php` +- `apps/platform/app/Services/TenantConfiguration/ClaimGuard.php` +- `apps/platform/database/migrations/2026_06_26_000419_expand_tenant_configuration_workloads.php` +- `apps/platform/tests/Unit/Support/TenantConfiguration/CoverageKernelValueTest.php` +- `apps/platform/tests/Unit/Support/TenantConfiguration/ResourceTypeRegistryTest.php` +- `apps/platform/tests/Unit/Support/TenantConfiguration/Spec419M365WorkloadRegistryTest.php` +- `apps/platform/tests/Unit/Support/TenantConfiguration/Spec419M365ClaimGuardTest.php` +- `apps/platform/tests/Feature/TenantConfiguration/TenantConfigurationResourceTypeRegistryTest.php` +- `apps/platform/tests/Feature/TenantConfiguration/TenantConfigurationSupportedScopeTest.php` +- `apps/platform/tests/Feature/TenantConfiguration/Spec419M365RegistryExpansionTest.php` +- `apps/platform/tests/Browser/Spec419M365RegistryOperatorSurfaceSmokeTest.php` +- `specs/419-m365-tcm-workload-registry-expansion/tasks.md` +- `specs/419-m365-tcm-workload-registry-expansion/implementation-report.md` + +## Implementation Summary + +- Expanded `Workload` to `intune`, `entra`, `exchange`, `teams`, `security_compliance`, `defender`, `purview`, `tenantpilot`, and `unknown`. +- Added a reversible migration to update the PostgreSQL workload check constraint, seed 24 representative M365 registry rows, and seed 7 inactive planning scopes. +- Added 24 active registry-only resource type definitions: + - Entra: `conditionalAccessPolicy`, `securityDefaults`, `application`, `servicePrincipal`, `roleDefinition`, `administrativeUnit` + - Exchange: `transportRule`, `acceptedDomain`, `sharedMailbox`, `remoteDomain`, `mailboxPlan`, `organizationConfig` + - Teams: `appPermissionPolicy`, `appSetupPolicy`, `meetingPolicy`, `messagingPolicy`, `teamsUpdateManagementPolicy`, `voiceRoute` + - Security and Compliance: `labelPolicy`, `retentionCompliancePolicy`, `dlpCompliancePolicy`, `autoSensitivityLabelPolicy`, `protectionAlert`, `complianceTag` +- Represented Defender and Purview only in aggregate planning metadata, not as fake certified resource types. +- Kept `ResourceTypeRegistry::defaultCanonicalTypes()` Intune-only so default capture paths do not silently expand to M365. +- Added Claim Guard statement checks that block broad M365, certified, restore-ready, complete-tenant, all-resource, and unscoped percent claims. Follow-up hardening made this token-based rather than dependent on one wording/order, so variants such as `M365 certified coverage`, `Microsoft 365 certified coverage`, and `M365 restore-ready coverage` are blocked. + +## Registry Defaults + +- New non-Intune rows use `source_class = tcm`. +- New non-Intune rows use `support_state = out_of_scope`. +- New non-Intune rows use `default_coverage_level = detected`. +- New non-Intune rows use `default_evidence_state = not_captured`. +- New non-Intune rows use `default_identity_state = derived`. +- New non-Intune rows use `default_claim_state = internal_only`. +- New non-Intune rows set `allows_beta_claims`, `allows_graph_fallback_claims`, and `allows_certified_claims` to false. +- Restore posture is conservative: high-risk entries are `not_restorable`; remaining representative entries are `preview_only`; none are `restorable`. +- Catalog metadata uses existing JSONB `metadata`: `documentation_status`, `catalog_source`, `catalog_last_reviewed_at`, `source_aliases`, `risk_tier`, `default_restore_posture`, `is_full_catalog`, `catalog_import_batch`, and `customer_claims_allowed`. +- Full-vs-partial decision: all new M365 catalog rows/scopes are partial representative planning truth with `is_full_catalog = false`. + +## Supported Scopes + +Added inactive planning scopes: + +- `m365_tcm_registry_detected` +- `entra_tcm_registry_detected` +- `exchange_tcm_registry_detected` +- `teams_tcm_registry_detected` +- `security_compliance_tcm_registry_detected` +- `m365_tcm_generic_future` +- `m365_tcm_certified_none` + +All new M365 scopes use `minimum_coverage_level = detected`, `customer_claims_allowed = false`, `is_active = false`, and registry-only metadata. Existing active Intune scopes remain the only active supported scopes, preserving the Coverage v2 operator surface default scope. + +## Guard Proof + +- No runtime capture path added: no Graph client, provider gateway, HTTP remote call, capture job, scheduler sync, or runtime Microsoft documentation fetch was introduced. +- No concrete resource data added by registry sync: `TenantConfigurationResource` and `TenantConfigurationResourceEvidence` remain untouched by defaults/sync. +- No `tenant_id` ownership truth added to registry definition tables or Spec 419 changed files. +- No workload mini-platform added: no Entra/Exchange/Teams/SecurityCompliance/Defender/Purview tables, models, engines, or namespaces. +- No v1 compatibility path added: no v1 adapter, fallback reader, dual write, or old gap taxonomy. +- No broad M365 claim is allowed. Internal registry-scoped denominator wording returns `internal_only` only when explicitly requested for operator/internal context. +- Rollback guard proof: rollback now deletes only rows tagged with the exact Spec 419 `catalog_import_batch`; if a later migration promotes a Non-Intune row/scope by changing that metadata, rollback preserves it and keeps the PostgreSQL workload check compatible with remaining workloads. + +## Product Surface Close-Out + +- No runtime UI route, Filament page/provider, navigation entry, Blade view, Livewire component, action, report, download, customer output, asset, or rendered label was edited. +- Product Surface Impact: existing Spec 418 Coverage v2 operator surface has data-driven impact because active registry rows now render. +- UI Surface Impact: existing Coverage v2 resource type registry table can show new M365 rows; no new controls or routes. +- Page archetype: internal/operator registry/readiness surface. +- Surface budget: existing table/filter/inspect affordances only. +- Technical Annex/deep-link demotion: catalog/source details remain internal registry metadata, not customer proof. +- Canonical status vocabulary: existing Coverage v2 enum vocabulary only; no legacy gap vocabulary. +- Product Surface exceptions: none. +- Focused browser proof: PASS via `tests/Browser/Spec419M365RegistryOperatorSurfaceSmokeTest.php`. +- Human Product Sanity: PASS. Browser proof showed registry rows with conservative labels (`Out of scope`, `Detected`, `Not included`, `Internal only`), no broad M365 coverage/certification/restore-ready labels, no table capture/restore/certify/publish/export/download/report actions, inactive M365 planning scope keys not rendered, Livewire present, no console or JavaScript errors, and no remote Graph/TCM/provider resource loads. +- Visible complexity outcome: broader registry data on the existing surface; no new page family. + +## Filament / Livewire / Assets + +- Livewire v4.0+ compliance: unchanged; no Livewire code was edited. +- Provider registration location: unchanged; Laravel panel providers remain registered through `apps/platform/bootstrap/providers.php`. +- Global search: no Filament Resource changes; no globally searchable resource added or changed. +- Destructive/high-impact actions: none added. +- Asset strategy: no new global or on-demand assets; no `filament:assets` deployment requirement introduced. +- Testing plan: unit, feature/static, pgsql constraint proof, and focused browser smoke were run. + +## Deployment Impact + +- Migration impact: yes. `2026_06_26_000419_expand_tenant_configuration_workloads.php` updates the PostgreSQL workload check constraint and upserts registry/scope rows. +- Rollback impact: deletes only exact Spec 419 planning scopes and M365 registry rows identified by the Spec 419 import-batch metadata. It restores the Intune-only workload check when no later Non-Intune rows remain; if later Non-Intune rows remain, it preserves those rows and constrains to the remaining workload values instead of damaging future entries. +- Environment variables: none. +- Queues/workers/scheduler: none. +- Storage/volumes: none. +- Assets: none. +- Staging expectation: run migrations and the focused registry/pgsql/browser validation before production promotion. + +## Validation + +- `cd apps/platform && ./vendor/bin/sail php -l app/Services/TenantConfiguration/ResourceTypeRegistry.php`: PASS +- `cd apps/platform && ./vendor/bin/sail php -l app/Services/TenantConfiguration/SupportedScopeResolver.php`: PASS +- `cd apps/platform && ./vendor/bin/sail php -l app/Services/TenantConfiguration/ClaimGuard.php`: PASS +- `cd apps/platform && ./vendor/bin/sail php -l database/migrations/2026_06_26_000419_expand_tenant_configuration_workloads.php`: PASS +- `cd apps/platform && ./vendor/bin/sail artisan test tests/Unit/Support/TenantConfiguration tests/Feature/TenantConfiguration`: PASS, 102 passed, 9 skipped +- `cd apps/platform && ./vendor/bin/sail php vendor/bin/pest -c phpunit.pgsql.xml tests/Feature/TenantConfiguration/Spec419M365RegistryExpansionTest.php`: PASS, 7 passed +- `cd apps/platform && ./vendor/bin/sail artisan test tests/Browser/Spec419M365RegistryOperatorSurfaceSmokeTest.php`: PASS, 1 passed, 28 assertions +- `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent`: PASS +- `git diff --check`: PASS + +## Deferred Work + +- Actual M365 capture, compare, render, restore, certification, customer reports, dashboards, and customer-facing M365 claims remain deferred to future specs. +- Defender and Purview remain workload-status metadata only until a later spec defines representative resource types with reviewed source truth. diff --git a/specs/419-m365-tcm-workload-registry-expansion/plan.md b/specs/419-m365-tcm-workload-registry-expansion/plan.md new file mode 100644 index 00000000..74872b42 --- /dev/null +++ b/specs/419-m365-tcm-workload-registry-expansion/plan.md @@ -0,0 +1,234 @@ +# Implementation Plan: Spec 419 - M365 TCM Workload Registry Expansion + +**Branch**: `419-m365-tcm-workload-registry-expansion` | **Date**: 2026-06-26 | **Spec**: `specs/419-m365-tcm-workload-registry-expansion/spec.md` +**Input**: Feature specification from `/specs/419-m365-tcm-workload-registry-expansion/spec.md` + +## Summary + +Expand the existing Coverage v2 registry so TenantPilot can classify Microsoft 365 TCM workload families as registry-only/detected planning truth without activating capture, compare, render, restore, certification, customer output, or UI changes. The implementation should reuse `TenantConfigurationResourceType`, `TenantConfigurationSupportedScope`, `ResourceTypeRegistry`, Coverage v2 enum/check constraints, metadata JSONB, and `ClaimGuard`. New non-Intune entries default conservatively and broad M365 claims remain blocked. + +## Technical Context + +**Language/Version**: PHP 8.4.x, Laravel 12.x +**Primary Dependencies**: existing Coverage v2 TenantConfiguration models/services/enums, Pest 4, PostgreSQL via Sail +**Storage**: Existing `tenant_configuration_resource_types` and `tenant_configuration_supported_scopes`; JSONB `metadata` preferred for catalog/documentation metadata. No new core table by default. +**Testing**: Pest 4 / PHPUnit 12 via Sail +**Validation Lanes**: fast-feedback, confidence, PostgreSQL lane if enum/check constraints or JSONB/query constraints change, focused browser if active rows/scopes render on the existing Spec 418 surface +**Target Platform**: Laravel Sail locally, Dokploy/container deployment for staging/production +**Project Type**: Laravel monolith under `apps/platform` +**Performance Goals**: deterministic local registry sync/seed, no remote calls, no runtime docs fetch, bounded test fixtures +**Constraints**: registry-only, no runtime UI route/action changes, existing-surface data-impact proof if rendered, no Graph/TCM/provider calls, no concrete evidence rows, no `tenant_id`, no mini-platform tables/classes, no broad M365 customer claims +**Scale/Scope**: representative or full static M365 TCM catalog entries for Entra, Exchange, Teams, Security and Compliance, plus safe workload-level Defender/Purview status + +## UI / Surface Guardrail Plan + +- **Guardrail scope**: no runtime UI code change, no new route, and no new action; existing operator-facing Coverage v2 surface may change data-driven rows/scopes. +- **Affected routes/pages/actions/states/navigation/panel/provider surfaces**: existing Spec 418 Coverage v2 readiness/resource-type registry surface only, via active registry data. No route, navigation, action, panel provider, Blade, or Livewire file change is planned. +- **No-impact class, if applicable**: not applicable if new rows/scopes render. This is a backend registry/config/persistence seed with possible existing-surface data impact. +- **Native vs custom classification summary**: N/A. +- **Shared-family relevance**: no UI shared-family change. +- **State layers in scope**: registry/persistence state plus existing Coverage v2 operator table/filter/scope data; no shell/page/detail/URL state changes. +- **Audience modes in scope**: N/A. +- **Decision/diagnostic/raw hierarchy plan**: existing rendered hierarchy only. Registry metadata must avoid customer-proof wording, raw provider payloads, and proof semantics. +- **Raw/support gating plan**: no raw payload or provider response storage in manifest metadata. +- **One-primary-action / duplicate-truth control**: no actions. +- **Handling modes by drift class or surface**: hard stop if runtime UI file edits, route, navigation, action, report, customer output, or rendered labels are needed. Data-driven rows/scopes on the existing Spec 418 surface require focused proof. +- **Repository-signal treatment**: no UI audit registry update unless implementation amends scope. +- **Special surface test profiles**: N/A. +- **Required tests or manual smoke**: unit and feature/static guards plus focused existing-surface browser proof if new active rows/scopes render. +- **Exception path and spread control**: none. +- **Active feature PR close-out entry**: Guardrail / Exception / Smoke Coverage with either focused existing-surface proof or explicit proof that no rendered output changed. +- **UI/Productization coverage decision**: Existing operator surface data impact only; no UI code change. +- **Coverage artifacts to update**: active Spec 419 artifacts and implementation report only. Do not rewrite Spec 418. +- **No-impact rationale**: no runtime UI files are planned, but active registry rows/scopes may render on the existing generic Spec 418 surface. +- **Navigation / Filament provider-panel handling**: no panel/provider registration change. +- **Screenshot or page-report need**: focused browser proof is required if rendered rows/scopes change. + +## Product Surface Contract Plan + +- **Product Surface Contract reference**: `docs/product/standards/product-surface-contract.md`. +- **No-legacy posture**: canonical Coverage v2 registry expansion; no compatibility exception. +- **Page archetype and surface budget plan**: existing internal/operator Coverage v2 readiness and registry inspection surface. Use the existing table/filter/modal budget only. +- **Technical Annex and deep-link demotion plan**: Registry metadata must not create customer-facing proof, raw provider output, or raw technical output. Source/catalog notes remain internal metadata. +- **Canonical status vocabulary plan**: Use existing internal Coverage v2 enums plus explicit registry-only/detected wording. Do not introduce page-local M365 coverage truth. +- **Product Surface exceptions**: none. +- **Browser verification plan**: focused existing-surface browser proof if active rows/scopes render; otherwise document proof that rendered output did not change. +- **Human Product Sanity plan**: required if active rows/scopes render; otherwise N/A with proof. +- **Visible complexity outcome target**: slightly broader existing registry data, no new surface family. +- **Implementation report target**: `specs/419-m365-tcm-workload-registry-expansion/implementation-report.md`. + +## Filament / Livewire / Deployment Posture + +- **Livewire v4 compliance**: Livewire v4.x remains the required runtime. No Livewire code is planned. +- **Panel provider registration location**: Laravel 12 panel providers remain in `apps/platform/bootstrap/providers.php`; no panel/provider change is planned. +- **Global search posture**: no Filament Resource changes. If a Resource is unexpectedly added, stop and amend the spec. +- **Destructive/high-impact action posture**: none. No action may mutate tenant/provider state, start capture, restore, certify, publish, export, or override claims. +- **Asset strategy**: no assets. `filament:assets` not required unless scope is amended to register Filament assets. +- **Testing plan**: focused unit/feature/static guard tests for registry, manifest/defaults, supported scopes, Claim Guard, default-scope preservation, no runtime capture, no mini-platform, no `tenant_id`, plus focused existing-surface browser proof if active rows/scopes render. +- **Deployment impact**: migrations/check constraints likely if workload enum values are persisted with PostgreSQL checks. No env vars, queues, scheduler, storage, assets, or workers. Static registry/seed changes must be validated on staging before production. + +## Shared Pattern & System Fit + +- **Cross-cutting feature marker**: yes at domain-contract level; no UI interaction family. +- **Systems touched**: `TenantConfigurationResourceType`, `TenantConfigurationSupportedScope`, `ResourceTypeRegistry`, `ClaimGuard`, `Workload`, possible enum/check constraints, registry tests. +- **Shared abstractions reused**: existing Coverage v2 registry/scope/claim guard paths. +- **New abstraction introduced? why?**: none by default. A static manifest/config file is allowed only if it replaces scattered arrays and remains local reviewed data. +- **Why the existing abstraction was sufficient or insufficient**: Existing registry is sufficient for resource type and supported-scope truth, but its current workload enum/check values are Intune-only. +- **Bounded deviation / spread control**: no M365-specific engine, table, dashboard, capture service, provider framework, or UI presenter. + +## OperationRun UX Impact + +- **Touches OperationRun start/completion/link UX?**: no. +- **Central contract reused**: N/A. +- **Delegated UX behaviors**: N/A. +- **Surface-owned behavior kept local**: none. +- **Queued DB-notification policy**: N/A. +- **Terminal notification path**: N/A. +- **Exception path**: none. + +## Provider Boundary & Portability Fit + +- **Shared provider/platform boundary touched?**: yes. +- **Provider-owned seams**: Microsoft workload names, TCM catalog source URLs, source aliases, Microsoft documentation review metadata, Graph fallback/beta source classification. +- **Platform-core seams**: workload enum/check constraints, canonical resource type, source class, support state, coverage/evidence/identity/claim defaults, supported scopes, restore tier, Claim Guard. +- **Neutral platform terms / contracts preserved**: provider connection, managed environment, resource type, workload, source class, supported scope, coverage level, evidence state, claim state, restore tier. +- **Retained provider-specific semantics and why**: Microsoft M365 workload labels remain because this spec is explicitly an M365 TCM registry expansion. They stay source metadata and classification, not ownership truth. +- **Bounded extraction or follow-up path**: document-in-feature if a static manifest path is introduced; follow-up-spec only for capture/compare/render/restore/certification packs. + +## Constitution Check + +- Inventory/evidence truth: PASS. Registry entries are denominator/planning truth only and do not create evidence rows. +- Read/write separation: PASS. No tenant/provider write action or restore path. +- Graph contract path: PASS. No Graph calls; no hardcoded endpoints outside contracts. +- Deterministic capabilities: PASS. Defaults are deterministic and testable. +- RBAC-UX: PASS. No new UI/action. Future consumers must enforce existing RBAC. +- Workspace isolation: PASS. Registry definitions are platform/product metadata; concrete evidence remains workspace/managed-environment scoped. +- OperationRun: PASS. No queued/remote work. +- Evidence/currentness: PASS. `not_captured` remains explicit for new entries. +- Customer output: PASS. No Review Pack/report/customer surface changes. +- Provider boundary: PASS with Microsoft source metadata bounded to registry fields. +- Product Surface: PASS for data-driven existing-surface impact if active rows/scopes render; otherwise N/A only with proof that no rendered output changed. +- Test governance: PASS. Unit/feature/pgsql-if-needed lanes named; no browser/heavy family. +- Proportionality: PASS. Workload enum/registry expansion is justified by false-claim prevention and future pack consistency. +- No premature abstraction: PASS if implementation reuses existing registry. +- Persisted truth: PASS. Registry definitions are durable product metadata; no new core table by default. +- Behavioral state: PASS with existing conservative support/coverage/evidence/claim/restore values. New source/support/restore states require amendment. +- No legacy / pre-production lean: PASS. No compatibility paths, `tenant_id`, v1 adapters, fallback readers, or dual writes. + +## Test Governance Check + +- **Test purpose / classification by changed surface**: Unit for enum/default manifest/Claim Guard; Feature for persisted registry/supported scopes/no-overclaim/no-runtime/no-mini-platform/no-tenant-id; PostgreSQL if migrations/check constraints change. +- **Affected validation lanes**: fast-feedback, confidence, pgsql only if schema/check constraints are changed, focused browser if active rows/scopes render on the existing Spec 418 operator surface. +- **Why this lane mix is the narrowest sufficient proof**: The change is registry/config/persistence truth. Unit and feature/static guards prove registry/default/claim behavior; focused browser proof is required only for the existing rendered surface when new active rows/scopes become visible. +- **Narrowest proving command(s)**: + - `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent` + - `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Unit/Support/TenantConfiguration/Spec419M365WorkloadRegistryTest.php tests/Unit/Support/TenantConfiguration/Spec419M365ResourceTypeManifestTest.php tests/Unit/Support/TenantConfiguration/Spec419M365ClaimGuardTest.php tests/Unit/Support/TenantConfiguration/Spec419M365RestoreTierDefaultTest.php tests/Unit/Support/TenantConfiguration/Spec419M365DocumentationStatusTest.php` + - `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/TenantConfiguration/Spec419M365RegistryExpansionTest.php tests/Feature/TenantConfiguration/Spec419M365SupportedScopesTest.php tests/Feature/TenantConfiguration/Spec419M365NoOverclaimTest.php tests/Feature/TenantConfiguration/Spec419M365NoRuntimeCaptureTest.php tests/Feature/TenantConfiguration/Spec419M365NoMiniPlatformTest.php tests/Feature/TenantConfiguration/Spec419M365NoTenantIdTest.php` + - `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Browser/Spec419M365RegistryOperatorSurfaceSmokeTest.php` if active rows/scopes render on the existing operator surface, or the repo-equivalent focused browser smoke path + - `cd apps/platform && ./vendor/bin/sail php vendor/bin/pest -c phpunit.pgsql.xml tests/Feature/TenantConfiguration/Spec419M365RegistryExpansionTest.php tests/Feature/TenantConfiguration/Spec419M365SupportedScopesTest.php` if migrations/check constraints/indexes change + - `git diff --check` +- **Fixture / helper / factory / seed / context cost risks**: keep M365 registry factories/fixtures local or opt-in; do not make broad workspace/provider setup default. +- **Expensive defaults or shared helper growth introduced?**: none expected. +- **Heavy-family additions, promotions, or visibility changes**: focused existing-surface browser smoke only when rows/scopes render; no broad browser family by default. +- **Surface-class relief / special coverage rule**: browser proof may be N/A only when implementation proves no rendered output changed. +- **Closing validation and reviewer handoff**: implementation report records exact files, matrices, default-scope proof, no-overclaim proof, no-runtime proof, no-tenant-id proof, no-mini-platform proof, Product Surface/browser proof or N/A proof, commands/results, and deferred work. +- **Budget / baseline / trend follow-up**: none expected unless PostgreSQL lane or guard tests become broad. +- **Review-stop questions**: lane fit, partial/full catalog honesty, broad claim blocking, no runtime capture, no mini-platform, no `tenant_id`, no runtime UI code scope, and Product Surface data-impact proof. +- **Escalation path**: document-in-feature for contained metadata/enum expansion; follow-up-spec for capture, UI, full-catalog import tooling, or new registry tables. +- **Active feature PR close-out entry**: Guardrail / Exception / Smoke Coverage. +- **Why no dedicated follow-up spec is needed**: this is the bounded registry denominator expansion; downstream capability packs are listed separately. + +## Project Structure + +### Documentation (this feature) + +```text +specs/419-m365-tcm-workload-registry-expansion/ ++-- spec.md ++-- plan.md ++-- tasks.md ++-- checklists/ + +-- requirements.md +``` + +### Source Code (likely affected in later implementation) + +```text +apps/platform/app/ ++-- Services/TenantConfiguration/ +| +-- ResourceTypeRegistry.php +| +-- ClaimGuard.php ++-- Support/TenantConfiguration/ +| +-- Workload.php +| +-- SourceClass.php +| +-- SupportState.php +| +-- CoverageLevel.php +| +-- EvidenceState.php +| +-- ClaimState.php +| +-- RestoreTier.php ++-- Models/ + +-- TenantConfigurationResourceType.php + +-- TenantConfigurationSupportedScope.php + +apps/platform/database/migrations/ ++-- + +apps/platform/tests/ ++-- Unit/Support/TenantConfiguration/ ++-- Feature/TenantConfiguration/ +``` + +**Structure Decision**: Reuse the existing Coverage v2 registry and scope infrastructure. Prefer existing `metadata` JSONB and static registry definitions. Do not add workload-specific tables/classes or a parallel M365 registry service. + +## Implementation Phases + +### Phase 0 - Preflight + +Capture branch, HEAD, dirty state, activated skills, current Coverage v2 registry/service names, related completed-spec guardrail, and stop conditions. Confirm no unrelated dirty files before implementation. + +### Phase 1 - Inspect Existing Registry Truth + +Inspect current enum values/check constraints, `ResourceTypeRegistry::defaultDefinitions()`, existing supported scope definitions, default scope selection, `ClaimGuard`, migrations, factories, and Spec 418 surface behavior. Map draft terms to current repo states before editing. + +### Phase 2 - Tests First: Workload And Manifest Defaults + +Add focused tests for workload values, representative M365 entries, Defender/Purview supported-scope metadata, documentation status metadata, full-vs-seeded catalog markers, default scope preservation, and conservative default support/coverage/evidence/claim/restore states. + +### Phase 3 - Tests First: Claim And Guard Boundaries + +Add Claim Guard/no-overclaim tests and static/feature guards for no runtime docs fetch, no Graph/TCM calls, no concrete evidence rows, no `tenant_id`, and no mini-platform tables/classes. + +### Phase 4 - Registry And Scope Expansion + +Expand workload enum/check values, static resource type defaults or manifest/config, supported-scope planning entries, aggregate M365 workload metadata, default-scope safeguards, metadata conventions, and idempotent sync/upsert behavior. + +### Phase 5 - Claim Guard Expansion + +Block broad M365, certified, restore-ready, complete-tenant, all-resource, and unscoped percent claims. Allow only explicit internal registry-only denominator-scoped wording when safe. + +### Phase 6 - Persistence And PostgreSQL Validation + +If migrations/check constraints/columns change, keep them reversible and narrow. Run focused PostgreSQL validation for changed TenantConfiguration paths. + +### Phase 7 - Product Surface Data-Impact Verification + +Confirm no UI route, page, navigation, Filament provider, action, report, download, customer output, or browser-visible claim changed. If active registry rows/scopes render on the existing Spec 418 surface, run focused browser proof that the page remains internal/operator-only, no broad M365 coverage label appears, default scope behavior is intentional, and no capture/restore/certify/report/download action appears. If runtime UI code changes are required, stop and amend artifacts before implementation continues. + +### Phase 8 - Validation And Implementation Report + +Run Pint dirty, focused unit/feature tests, PostgreSQL lane if required, and `git diff --check`. Complete implementation report with workload/resource matrices and required no-overclaim/no-runtime/no-tenant-id/no-mini-platform proof. + +## Stop Conditions + +Stop and update `spec.md`, `plan.md`, and `tasks.md` before continuing if any of these appear: + +- Capture, compare, render, restore, apply, certify, publish, export, customer report, Review Pack, or dashboard behavior is needed. +- Graph/TCM/provider remote work or runtime Microsoft documentation fetch is needed. +- A new UI route/page/navigation/action/table/form/rendered label is needed. +- Existing Coverage v2 operator surface default scope changes without explicit Product Surface/browser proof. +- A full static catalog import is too large for bounded review and the implementation cannot mark the catalog partial. +- A new `SourceClass`, `SupportState`, or `RestoreTier` value is needed without proportionality proof and tests. +- `tenant_id` appears as Coverage v2 ownership truth. +- A workload-specific table, model, engine, provider framework, or mini-platform appears. +- Partial catalog wording implies full M365 coverage. +- A broad M365/certified/restore-ready/customer claim needs to be allowed. diff --git a/specs/419-m365-tcm-workload-registry-expansion/spec.md b/specs/419-m365-tcm-workload-registry-expansion/spec.md new file mode 100644 index 00000000..70097e6f --- /dev/null +++ b/specs/419-m365-tcm-workload-registry-expansion/spec.md @@ -0,0 +1,371 @@ +# Feature Specification: Spec 419 - M365 TCM Workload Registry Expansion + +**Feature Branch**: `419-m365-tcm-workload-registry-expansion` +**Created**: 2026-06-26 +**Status**: Draft +**Input**: User-provided "Spec 419 - M365 TCM Workload Registry Expansion" draft plus repo checks against Specs 414, 415, 417, 418, roadmap/candidate queue, constitution, Product Surface Contract, TenantPilot agent gates, and current TenantConfiguration runtime. + +## Candidate Selection + +- **Selected candidate**: Spec 419 - M365 TCM Workload Registry Expansion. +- **Source location**: User-provided prompt in this session. The active queue in `docs/product/spec-candidates.md` explicitly has no automatic next-best-prep target, so this is a direct manual promotion. +- **Why selected**: Specs 414, 415, 417, and 418 establish the Coverage v2 kernel, generic capture, identity engine, and operator surface. The next safe manually promoted slice is registry-only M365 workload recognition, so future Entra, Exchange, Teams, Security and Compliance, Defender, and Purview packs do not create parallel mini-platforms or premature customer claims. +- **Roadmap relationship**: Aligns with roadmap themes for Microsoft 365 governance expansion, provider-boundary discipline, workspace/managed-environment ownership, coverage claim safety, and no-legacy cutover. It is not auto-selected from the candidate queue. +- **Close alternatives deferred**: Management-report PDF runtime validation, artifact lifecycle retention, provider readiness productization, cross-domain indicator follow-through, system-panel browser fixture work, and first governed AI consumer remain manual-promotion backlog items. M365 capture, compare, render, restore, certification, Review Pack/report output, customer-facing M365 claims, and M365 dashboards are deferred to later specs. +- **Related completed-spec guardrail**: `specs/414-tcm-first-coverage-core-cutover/`, `specs/415-generic-content-backed-capture/`, `specs/417-canonical-identity-engine/`, and `specs/418-coverage-v2-operator-surface/` contain completed/validated signals and are read-only dependency context. Do not rewrite them, normalize their close-out history, or strip validation/task/browser/review history. +- **Prerequisite gate result**: PASS. Repo truth has `TenantConfigurationResourceType`, `TenantConfigurationSupportedScope`, Coverage v2 enums, `ResourceTypeRegistry`, `ClaimGuard`, capture/evidence services, canonical identity services, and the Spec 418 operator surface. Stop if implementation discovers the Coverage v2 registry or Claim Guard is missing. +- **Smallest viable implementation slice**: Expand the existing Coverage v2 registry and supported-scope planning metadata so M365 workload families and representative resource types are classified as registry-only/detected internal planning truth. Do not activate capture, compare, render, restore, certification, customer output, runtime docs fetches, or new UI routes. +- **Draft-to-repo deviations**: The user draft names `tenantpilot_internal`, `detected_only`, `compare_only`, and `manual_review_required`; current repo truth does not have those enum values. This spec maps the intent to existing conservative states unless implementation amends enum/check constraints with proportionality proof: `source_class` remains `tcm`, `graph_v1_fallback`, or `graph_beta_experimental`; registry-only support defaults to existing `out_of_scope` unless a new state is justified; "compare/manual review" restore posture maps to `preview_only` or `not_restorable`, never `restorable`. + +## Spec Candidate Check *(mandatory - SPEC-GATE-001)* + +- **Problem**: Coverage v2 is Intune-first. Without one shared M365 workload registry expansion, later Entra, Exchange, Teams, Security and Compliance, Defender, and Purview work can create parallel models, vocabularies, support states, claim rules, evidence semantics, and restore assumptions. +- **Today's failure**: Operators and implementers cannot distinguish "registry knows this M365 workload/type" from "TenantPilot captures, compares, renders, restores, certifies, or can claim customer coverage for it." That creates a high-risk path to false M365 coverage claims. +- **User-visible improvement**: Future operator surfaces and implementation reports can classify M365 workload/resource-type readiness honestly as registry-only/detected, with clear "not content-backed, not certified, not restore-ready" defaults. +- **Smallest enterprise-capable version**: Expand existing Coverage v2 enum/check constraints, registry definitions, metadata, supported scopes, and Claim Guard rules for representative M365 workloads. Use static reviewed manifest data only. No runtime remote calls, no customer output, and no new UI surface. +- **Explicit non-goals**: No content capture for new workloads, no TCM snapshot ingestion for all workloads, no Graph fallback capture, no live Microsoft documentation scraping, no compare/render/restore/apply, no certification, no customer-facing M365 reports, no Review Pack output, no broad M365 dashboard, no new provider framework, no workload-specific tables, no v1 compatibility, no `tenant_id`. +- **Permanent complexity imported**: Additional workload enum/check values, conservative registry entries or static manifest definitions, supported-scope planning rows, metadata conventions for documentation status/source aliases/risk/default restore posture, Claim Guard M365 deny rules, and focused tests. No new core tables are expected. +- **Why now**: Spec 418 made Coverage v2 inspectable. Before future M365 workload packs begin, the denominator and claim-safety model must be shared and conservative. +- **Why not local**: A local Entra or Exchange list would immediately create another vocabulary and claim path. The existing Coverage v2 registry is the correct shared contract and must absorb this expansion. +- **Approval class**: Core Enterprise. +- **Red flags triggered**: New taxonomy/enum values, registry expansion, and future-oriented workload coverage. Defense: this prevents concrete false coverage/restore/certification claims and uses the existing Coverage v2 registry rather than creating a new framework. +- **Score**: Nutzen: 2 | Dringlichkeit: 2 | Scope: 2 | Komplexitaet: 1 | Produktnaehe: 1 | Wiederverwendung: 2 | **Gesamt: 10/12** +- **Decision**: approve as a strict registry-only expansion with no capture, no customer claim activation, no UI route expansion, no runtime docs fetch, no `tenant_id`, and no workload-specific mini-platform. + +## Spec Scope Fields *(mandatory)* + +- **Scope**: platform registry metadata plus existing workspace/managed-environment boundaries for any future environment-owned evidence. This spec creates product/platform registry definitions, not concrete environment resource evidence. +- **Primary Routes**: No new route, page, action, navigation entry, customer output, download, or report is in scope. The existing Spec 418 Coverage v2 operator surface may render the expanded registry rows/scopes through its generic data path. +- **Data Ownership**: `tenant_configuration_resource_types` and `tenant_configuration_supported_scopes` remain platform/product registry metadata according to current Coverage v2 design. Concrete resource/evidence rows remain workspace + managed-environment + provider-connection scoped in existing capture specs. No `tenant_id` is allowed as internal ownership truth. +- **RBAC**: No new runtime action or UI route is introduced. Existing operator surface authorization from Spec 418 may display new rows if it derives from the registry. If implementation changes rendered UI files, actions, labels, or default operator decisions beyond the data-driven registry display described here, stop and amend Product Surface/RBAC sections before runtime edits. + +For canonical-view specs: + +- **Default filter behavior when tenant-context is active**: N/A - no canonical view or route change. Existing Coverage v2 operator surface behavior remains the only possible consumer. +- **Explicit entitlement checks preventing cross-tenant leakage**: Registry definitions are not tenant-owned. Any future evidence/resource rows must remain scoped by workspace, managed environment, and same-scope provider connection. + +## No Legacy / No Backward Compatibility Constraint *(mandatory)* + +TenantPilot is pre-production unless this spec explicitly records a compatibility exception. + +- **Compatibility posture**: canonical Coverage v2 registry expansion; no compatibility exception. +- **Legacy aliases, fallback readers, hidden routes, duplicate UI, old labels, or historical fixtures kept?**: no. +- **Why clean replacement is safe now**: This is registry metadata only. No production/customer data or external contract requires v1 coverage compatibility. The implementation must not create v1-to-v2 adapters, old gap taxonomy, dual writes, fallback readers, old snapshot promotion, old support-state aliases, or customer-facing dual truth. + +## UI Surface Impact *(mandatory - UI-COV-001)* + +Does this spec add, remove, rename, or materially change any reachable UI surface? + +- [ ] No UI surface impact +- [x] Existing page changed +- [ ] New page/route added +- [ ] Navigation changed +- [ ] Filament panel/provider surface changed +- [ ] New modal/drawer/wizard/action added +- [ ] New table/form/state added +- [ ] Customer-facing surface changed +- [ ] Dangerous action changed +- [x] Status/evidence/review presentation changed +- [ ] Workspace/environment context presentation changed + +No runtime UI file, route, navigation, action, table/form, or rendered label change is planned. The impact is data-driven: existing Spec 418 registry/readiness surfaces may show new rows, workload filters, scope options, and registry-only states because they already read active Coverage v2 registry data. If implementation requires runtime UI edits, stop and amend `spec.md`, `plan.md`, and `tasks.md` before editing runtime UI. + +## UI/Productization Coverage *(mandatory when UI Surface Impact is not "No UI surface impact"; otherwise write `N/A - no reachable UI surface impact` plus rationale)* + +Existing Coverage v2 operator surface data may change without UI code edits. Productization coverage is limited to the existing Spec 418 Coverage v2 readiness/resource-type registry surface. No new route, navigation entry, action, customer surface, report, download, or primary dashboard is allowed. Registry-only wording, no-customer-claim semantics, and no capture/restore/certify affordances must remain visible-safe on that existing surface. + +## Product Surface Impact *(mandatory for UI-affecting specs; otherwise write `N/A - no rendered product surface changed` plus rationale)* + +Reference: `docs/product/standards/product-surface-contract.md`. + +- **Product Surface Contract applies?**: yes, for data-driven changes on the existing Spec 418 Coverage v2 operator surface. No UI code or route expansion is planned. +- **Page archetype**: existing internal/operator readiness and registry inspection surface. +- **Primary user question**: Which Coverage v2 registry entries and supported scopes are known, and which are registry-only rather than content-backed/customer-claimable? +- **Primary action**: inspect existing registry rows only. No capture, restore, certify, publish, export, or customer-output action. +- **Surface budget result**: existing table/filter/modal budget only; no new primary navigation, dashboard, page, action family, or customer report. +- **Technical Annex / deep-link demotion**: Registry metadata must avoid customer-facing raw payload/proof semantics. Any source URLs, catalog review notes, aliases, and uncertainty remain internal registry metadata. +- **Canonical status vocabulary**: Use existing Coverage v2 enum/status vocabulary and explicit registry-only/detected wording. Do not introduce page-local M365 coverage status truth. +- **Visible complexity impact**: slightly broader existing registry table/filter data; no new surface family. +- **Product Surface exceptions**: none. + +## Browser Verification Plan *(mandatory)* + +- **Browser proof required?**: yes, if new active registry rows/scopes are visible through the existing Spec 418 Coverage v2 operator surface. If the implementation keeps new planning rows inactive and proves no rendered output changes, document that proof instead. +- **No-browser rationale**: N/A only when implementation proves no new rows/scopes render in the existing operator surface. +- **Focused path when required**: existing Spec 418 Coverage v2 readiness/operator route. +- **Primary interaction to execute**: load the existing page, inspect the resource type registry table/filter/scope selector, and verify registry-only M365 rows/scopes do not create broad claims or actions. +- **Console, Livewire, Filament, network, and 500-error checks**: required for the focused path when rendered data changes. +- **Full-suite failure triage**: If runtime UI files, labels, routes, actions, or navigation change, stop and amend this spec before continuing. + +## Human Product Sanity Check *(mandatory)* + +- **Required?**: yes, when new registry rows/scopes render on the existing operator surface. +- **No-human-sanity rationale**: N/A only when implementation proves no rendered output changes. +- **Reviewer questions**: Does the existing Coverage v2 surface still read as internal/operator registry truth, not M365 customer coverage? Are registry-only, not content-backed, not restore-ready, and not certified boundaries clear enough without adding a new page? +- **Planned result location**: `specs/419-m365-tcm-workload-registry-expansion/implementation-report.md`. + +## Product Surface Merge Gate Checklist *(mandatory)* + +- [x] No-legacy posture or approved exception recorded. +- [x] Product Surface Impact is completed for data-driven existing-surface impact. +- [x] Browser proof is required if new active rows/scopes render, or `N/A` must be justified by proof that no rendered output changed. +- [x] Human Product Sanity is required if new active rows/scopes render, or `N/A` must be justified by proof that no rendered output changed. +- [x] Product Surface exceptions are documented or `none`. +- [x] Implementation report will state Livewire v4 compliance, provider registration location, global search posture, destructive/high-impact action posture, asset strategy, tests/browser result, deployment impact, visible complexity outcome, and no completed-spec rewrite assertion. + +## Cross-Cutting / Shared Pattern Reuse *(mandatory when the feature touches notifications, status messaging, action links, header actions, dashboard signals/cards, alerts, navigation entry points, evidence/report viewers, or any other existing shared operator interaction family; otherwise write `N/A - no shared interaction family touched`)* + +- **Cross-cutting feature?**: existing Coverage v2 registry/readiness interaction family may receive new data only. Registry and Claim Guard contracts are shared domain contracts, not a new UI interaction contract. +- **Interaction class(es)**: N/A. +- **Systems touched**: Coverage v2 resource type registry, supported scopes, enum/check constraints, Claim Guard, static manifest/registry definitions, focused tests. +- **Existing pattern(s) to extend**: existing `ResourceTypeRegistry`, `TenantConfigurationResourceType`, `TenantConfigurationSupportedScope`, Coverage v2 enum classes, and `ClaimGuard`. +- **Shared contract / presenter / builder / renderer to reuse**: existing Coverage v2 registry and Claim Guard services. Do not create a new M365 registry service unless existing registry structure cannot support static manifest grouping after proportionality review. +- **Why the existing shared path is sufficient or insufficient**: Current registry already owns resource type/source/support/claim defaults. It is sufficient for M365 workload recognition when carefully expanded. +- **Allowed deviation and why**: none. +- **Consistency impact**: M365 workload packs must use the same Coverage v2 source/support/coverage/evidence/identity/claim vocabulary. +- **Review focus**: Verify no mini-platform registry, no old v1 vocabulary, no customer claims, and no new UI interaction family. + +## OperationRun UX Impact *(mandatory when the feature creates, queues, deduplicates, resumes, blocks, completes, or deep-links to an `OperationRun`; otherwise write `N/A - no OperationRun start or link semantics touched`)* + +- **Touches OperationRun start/completion/link UX?**: no. +- **Shared OperationRun UX contract/layer reused**: N/A. +- **Delegated start/completion UX behaviors**: N/A. +- **Local surface-owned behavior that remains**: none. +- **Queued DB-notification policy**: N/A. +- **Terminal notification path**: N/A. +- **Exception required?**: none. + +## Provider Boundary / Platform Core Check *(mandatory when the feature changes shared provider/platform seams, identity scope, governed-subject taxonomy, compare strategy selection, provider connection descriptors, or operator vocabulary that may leak provider-specific semantics into platform-core truth; otherwise write `N/A - no shared provider/platform boundary touched`)* + +- **Shared provider/platform boundary touched?**: yes. +- **Boundary classification**: mixed. Workload/source classification is platform-core registry truth; Microsoft TCM catalog names and source URLs are provider-owned metadata. +- **Seams affected**: workload enum/check constraints, resource type canonical names, source classes, metadata source aliases, supported-scope keys, Claim Guard forbidden/allowed wording. +- **Neutral platform terms preserved or introduced**: workload, resource type, source class, support state, coverage level, evidence state, identity state, claim state, supported scope, restore tier, documentation status metadata. +- **Provider-specific semantics retained and why**: Microsoft workload names and TCM catalog metadata are retained because this spec is explicitly M365 TCM registry expansion. They must remain registry/source metadata and must not become tenant ownership truth. +- **Why this does not deepen provider coupling accidentally**: No new provider framework, provider connection model, tenant ownership key, runtime Graph/TCM calls, or workload-specific tables are introduced. Provider-native IDs stay metadata only. +- **Follow-up path**: later workload packs for capture/compare/render/restore/certification must use this registry and amend specs before adding customer claims. + +## UI / Surface Guardrail Impact *(mandatory when operator-facing surfaces are changed; otherwise write `N/A`)* + +Existing Spec 418 Coverage v2 operator surfaces may show new active registry rows/scopes through existing data queries. No new route, page, navigation, action, table/form definition, Blade view, Livewire component, Filament provider, customer surface, report, or download is in scope. + +## Decision-First Surface Role *(mandatory when operator-facing surfaces are changed)* + +The existing surface role remains internal operator readiness and registry inspection. New data must answer "known in registry and registry-only" rather than implying capture, content-backed evidence, certification, restore readiness, or customer coverage. + +## Audience-Aware Disclosure *(mandatory when operator-facing surfaces are changed)* + +Internal/operator only. Registry metadata and workload status may be visible to authorized operators through the existing surface, but no customer-facing claim, report, Review Pack output, or public M365 coverage statement is activated. + +## UI/UX Surface Classification *(mandatory when operator-facing surfaces are changed)* + +Existing page, data-only impact. No new page archetype, action family, primary navigation, modal type, wizard, or dashboard is introduced. + +## Operator Surface Contract *(mandatory when operator-facing surfaces are changed)* + +The existing Spec 418 operator surface must remain read-only and claim-safe: no capture/start, restore/apply, certify, publish, export, report/download, or customer-output action; no broad M365 coverage label; no 100% M365 coverage wording; and no default scope change unless it is explicitly covered by Product Surface/browser proof. + +## Proportionality Review *(mandatory when structural complexity is introduced)* + +- **New source of truth?**: yes, expanded registry definitions become product/platform metadata for M365 workload recognition. +- **New persisted entity/table/artifact?**: no new core table expected. Static registry definitions and/or existing `tenant_configuration_resource_types` and `tenant_configuration_supported_scopes` rows are the artifact. Existing JSONB `metadata` is preferred for documentation status, source aliases, catalog source, review date, risk tier, default restore posture notes, and `is_full_catalog`. +- **New abstraction?**: no new abstraction by default. Use existing `ResourceTypeRegistry`/supported-scope paths. A static config/manifest may be introduced only if it replaces scattered arrays and remains reviewed local code, not runtime docs sync. +- **New enum/state/reason family?**: yes, workload enum/check values must expand beyond `intune`. `tenantpilot` is included only as an internal/platform workload bucket from the user draft so future TenantPilot-owned registry rows do not overload `unknown`; this spec does not require TenantPilot resource entries. Additional documentation status may be metadata first. Do not add new source/support/restore enum values unless implementation proves existing values cannot express the conservative defaults. +- **New cross-domain UI framework/taxonomy?**: no. +- **Current operator problem**: Future M365 packs need one denominator and claim-safety contract so "known in registry" is not misread as "captured/certified/restorable/customer-claimable." +- **Existing structure is insufficient because**: Current repo truth only supports `Workload::Intune` and initial Intune Coverage v2 resource types. It cannot classify M365 workload families without expanding allowed workload values and registry metadata. +- **Narrowest correct implementation**: Add workload values and representative registry entries/scopes using existing tables/services, with conservative default coverage/evidence/claim/restore states and tests proving no overclaiming. +- **Ownership cost**: Maintain M365 workload/resource type manifests, conservative defaults, Claim Guard phrases, and tests as later workload packs mature. +- **Alternative intentionally rejected**: Per-workload models, tables, engines, dashboards, capture jobs, or customer reports. Rejected because they fragment Coverage v2 and create false product maturity. +- **Release truth**: current-release registry/planning truth required before later M365 evidence and compare packs. + +### Compatibility posture + +This feature assumes a pre-production environment. Backward compatibility, legacy aliases, migration shims, historical fixtures, v1 adapters, fallback readers, and dual writes are out of scope. + +## Testing / Lane / Runtime Impact *(mandatory for runtime behavior changes)* + +- **Test purpose / classification**: Unit for enum/default manifest/Claim Guard behavior; Feature for persisted registry/supported-scope seed behavior, no-overclaim, no-runtime-capture, no-mini-platform, no-tenant-id guards. +- **Validation lane(s)**: fast-feedback and confidence. PostgreSQL lane required if check constraints, JSONB constraints, unique indexes, or migrations are changed. Focused browser lane required if active rows/scopes render on the existing Spec 418 operator surface. +- **Why this classification and these lanes are sufficient**: Registry expansion is service/config/persistence truth. Unit and feature tests prove classification, default-scope safety, and claim safety; focused browser proof is needed only for the existing rendered surface when active registry rows/scopes become visible. +- **New or expanded test families**: focused Spec 419 TenantConfiguration unit/feature tests, plus a focused existing-surface browser smoke when rendered rows/scopes change. No broad heavy-governance browser family by default. +- **Fixture / helper cost impact**: keep fixtures local or opt-in; do not broaden default TenantConfiguration factories or browser setup. +- **Heavy-family visibility / justification**: none. +- **Special surface test profile**: focused existing Coverage v2 operator-surface proof if active rows/scopes render. +- **Standard-native relief or required special coverage**: Browser proof may be N/A only when implementation proves no rendered output changed. +- **Reviewer handoff**: Reviewers must verify lane fit, exact conservative defaults, default-scope safety, Product Surface data-impact proof, no runtime remote calls, no `tenant_id`, no mini-platforms, and no customer claims. +- **Budget / baseline / trend impact**: none expected; document if focused registry tests materially widen confidence lane runtime. +- **Escalation needed**: document-in-feature for contained enum/metadata expansion; follow-up-spec if implementation requires full catalog import tooling, UI, capture, or new persistent registry tables. +- **Active feature PR close-out entry**: Guardrail / Exception / Smoke Coverage with focused existing-surface proof or explicit proof that no rendered output changed. +- **Planned validation commands**: + - `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent` + - `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Unit/Support/TenantConfiguration/Spec419M365WorkloadRegistryTest.php tests/Unit/Support/TenantConfiguration/Spec419M365ResourceTypeManifestTest.php tests/Unit/Support/TenantConfiguration/Spec419M365ClaimGuardTest.php tests/Unit/Support/TenantConfiguration/Spec419M365RestoreTierDefaultTest.php tests/Unit/Support/TenantConfiguration/Spec419M365DocumentationStatusTest.php` + - `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/TenantConfiguration/Spec419M365RegistryExpansionTest.php tests/Feature/TenantConfiguration/Spec419M365SupportedScopesTest.php tests/Feature/TenantConfiguration/Spec419M365NoOverclaimTest.php tests/Feature/TenantConfiguration/Spec419M365NoRuntimeCaptureTest.php tests/Feature/TenantConfiguration/Spec419M365NoMiniPlatformTest.php tests/Feature/TenantConfiguration/Spec419M365NoTenantIdTest.php` + - `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Browser/Spec419M365RegistryOperatorSurfaceSmokeTest.php` if active rows/scopes render on the existing operator surface, or the repo-equivalent focused browser smoke path + - `cd apps/platform && ./vendor/bin/sail php vendor/bin/pest -c phpunit.pgsql.xml tests/Feature/TenantConfiguration/Spec419M365RegistryExpansionTest.php tests/Feature/TenantConfiguration/Spec419M365SupportedScopesTest.php` if migrations/check constraints/indexes change + - `git diff --check` + +## User Scenarios & Testing *(mandatory)* + +### User Story 1 - Classify M365 Workloads In Coverage v2 (Priority: P1) + +As a platform reviewer, I need Coverage v2 to recognize M365 workload families under one shared registry so future domain packs do not invent their own models or vocabulary. + +**Independent Test**: Focused registry tests assert workloads `entra`, `exchange`, `teams`, `security_compliance`, `defender`, and `purview` are accepted/classified without creating workload-specific tables. + +**Acceptance Scenarios**: + +1. **Given** the registry defaults are synchronized, **When** workload values are inspected, **Then** Intune plus the new M365 workload values are available under the shared Coverage v2 registry. +2. **Given** Defender and Purview do not have separate mapped resource catalogs in this spec, **When** workload metadata is inspected, **Then** they are represented as overview-only or combined-catalog planning status without fake certified resource entries. + +### User Story 2 - Seed Representative Registry-Only Resource Types (Priority: P1) + +As a release reviewer, I need representative Entra, Exchange, Teams, and Security and Compliance resource types seeded with conservative defaults so later packs start from honest denominator metadata. + +**Independent Test**: Feature tests assert required representative canonical types exist with `source_class = tcm`, `default_coverage_level = detected`, `default_evidence_state = not_captured`, `default_claim_state = internal_only` or `claim_blocked`, and no default certified/restorable/customer claim state. + +**Acceptance Scenarios**: + +1. **Given** the registry contains Entra seed entries, **When** `conditionalAccessPolicy`, `securityDefaults`, `application`, `servicePrincipal`, `roleDefinition`, and `administrativeUnit` are inspected, **Then** they are registry-only/detected and not auto-restorable. +2. **Given** the registry contains Exchange seed entries, **When** `transportRule`, `acceptedDomain`, `sharedMailbox`, `remoteDomain`, `mailboxPlan`, and `organizationConfig` are inspected, **Then** mailflow/org defaults are conservative and no restore-ready claim is possible. +3. **Given** the registry contains Teams seed entries, **When** `appPermissionPolicy`, `appSetupPolicy`, `meetingPolicy`, `messagingPolicy`, `teamsUpdateManagementPolicy`, and `voiceRoute` are inspected, **Then** entries are registry-only/detected and manual-review/preview-only at most. +4. **Given** the registry contains Security and Compliance seed entries, **When** `labelPolicy`, `retentionCompliancePolicy`, `dlpCompliancePolicy` or a repo-canonical equivalent, `autoSensitivityLabelPolicy`, `protectionAlert`, and `complianceTag` are inspected, **Then** high-risk defaults block restore/certified claims. + +### User Story 3 - Block Broad M365 Claims (Priority: P1) + +As a product reviewer, I need Claim Guard to block broad M365, certified, restore-ready, and "100%" claims unless the statement is explicitly registry-only and denominator-scoped. + +**Independent Test**: Claim Guard tests assert broad M365 phrases are blocked and internal scoped registry-only phrases are allowed only with explicit seeded denominator wording. + +**Acceptance Scenarios**: + +1. **Given** a claim says "100% Microsoft 365 coverage", **When** Claim Guard evaluates it, **Then** the claim is blocked. +2. **Given** a claim says "Certified M365 coverage" or "Restore-ready M365 coverage", **When** Claim Guard evaluates it, **Then** the claim is blocked. +3. **Given** a claim says "100% registry coverage for seeded Entra resource type entries" and remains internal/operator-only, **When** Claim Guard evaluates it, **Then** the claim may be allowed or limited according to exact scope metadata. + +### User Story 4 - Prove Registry-Only Boundaries (Priority: P1) + +As an implementation reviewer, I need tests proving Spec 419 does not add runtime capture, customer output, UI activation, `tenant_id`, or workload-specific mini-platforms. + +**Independent Test**: Guard tests inspect changed files/schema/service registrations and assert no Graph/TCM runtime calls, no capture job/action, no concrete resource/evidence rows created by registry seed, no `tenant_id`, and no `entra_*`, `exchange_*`, `teams_*`, `purview_*`, `defender_*`, or `security_compliance_*` tables/classes. + +**Acceptance Scenarios**: + +1. **Given** the implementation diff is reviewed, **When** runtime code is inspected, **Then** no Graph/TCM/provider remote call path or runtime Microsoft docs fetch is added. +2. **Given** the implementation diff is reviewed, **When** schema/classes are inspected, **Then** no workload-specific mini-platform table/class namespace is introduced. +3. **Given** registry seeds run, **When** concrete Coverage v2 resource/evidence tables are inspected, **Then** no concrete environment-owned evidence rows are created by registry expansion. + +## Functional Requirements *(mandatory)* + +- **FR-419-001**: The implementation MUST expand or confirm Coverage v2 workload support for `intune`, `entra`, `exchange`, `teams`, `security_compliance`, `defender`, `purview`, `tenantpilot`, and `unknown`, using existing enum/check-constraint patterns. +- **FR-419-002**: The implementation MUST keep `intune` existing entries intact and MUST NOT duplicate existing Intune defaults. +- **FR-419-003**: The implementation MUST record documentation status for workload/resource catalog entries using existing JSONB `metadata` first unless a dedicated column is proven necessary. +- **FR-419-004**: Documentation status values MUST include `documented_resource_catalog`, `documented_overview_only`, `combined_catalog`, `graph_only`, `internal`, and `unknown` as metadata or a justified enum/constraint. +- **FR-419-005**: New M365 representative entries MUST default to registry-only/detected: `default_coverage_level = detected`, `default_evidence_state = not_captured`, and `default_claim_state = internal_only` or `claim_blocked`. +- **FR-419-006**: New M365 representative entries MUST NOT default to `content_backed`, `comparable`, `renderable`, `restorable`, `certified`, or `claim_allowed`. +- **FR-419-007**: New M365 representative entries MUST use `source_class = tcm` when they are TCM-documented. +- **FR-419-008**: Source class values MUST remain limited to current repo values unless implementation amends the enum/check constraints with proportionality proof. `tenantpilot_internal` is not required for this M365 TCM slice. +- **FR-419-009**: If a resource is not TCM-documented and is included only as future Graph fallback, it MUST default to `graph_v1_fallback`, `default_coverage_level = detected`, `default_evidence_state = not_captured`, `default_claim_state = claim_blocked`, and no customer claim. +- **FR-419-010**: Beta/experimental resources, if any are added, MUST use `graph_beta_experimental`, `support_state = experimental`, `default_coverage_level = detected`, `default_evidence_state = not_captured` or `schema_unknown`, `default_claim_state = claim_blocked`, and no beta/certified claims. +- **FR-419-011**: Entra representative entries MUST include `conditionalAccessPolicy`, `securityDefaults`, `application`, `servicePrincipal`, `roleDefinition`, and `administrativeUnit`. +- **FR-419-012**: Exchange representative entries MUST include `transportRule`, `acceptedDomain`, `sharedMailbox`, `remoteDomain`, `mailboxPlan`, and `organizationConfig`. +- **FR-419-013**: Teams representative entries MUST include `appPermissionPolicy`, `appSetupPolicy`, `meetingPolicy`, `messagingPolicy`, `teamsUpdateManagementPolicy`, and `voiceRoute`. +- **FR-419-014**: Security and Compliance representative entries MUST include `labelPolicy`, `retentionCompliancePolicy`, `dlpCompliancePolicy` or a repo-canonical equivalent with aliases, `autoSensitivityLabelPolicy`, `protectionAlert`, and `complianceTag`. +- **FR-419-015**: Defender MUST be represented as workload-level overview-only or combined-catalog metadata under `tenant_configuration_supported_scopes.metadata.workload_documentation_status.defender` on the aggregate M365 planning scope unless repo/current official mapping already ties Defender resource types to shared Security and Compliance entries. Do not invent fake Defender certified resource types. +- **FR-419-016**: Purview MUST be represented as workload-level overview-only or combined-catalog metadata under `tenant_configuration_supported_scopes.metadata.workload_documentation_status.purview` on the aggregate M365 planning scope unless repo/current official mapping already ties Purview resource types to shared Security and Compliance entries. Do not invent fake Purview certified resource types. +- **FR-419-017**: Partial/seeded catalogs MUST be marked explicitly with `is_full_catalog = false` or equivalent metadata and MUST NOT be displayed or claimed as full workload coverage. +- **FR-419-018**: Supported-scope planning entries MUST include `m365_tcm_registry_detected`, `entra_tcm_registry_detected`, `exchange_tcm_registry_detected`, `teams_tcm_registry_detected`, `security_compliance_tcm_registry_detected`, `m365_tcm_generic_future`, and `m365_tcm_certified_none`. +- **FR-419-019**: Supported-scope planning entries MUST NOT include forbidden broad scopes such as `m365_full_coverage`, `m365_certified`, `all_microsoft_365_supported`, `full_tenant_coverage`, or `full_m365_restore_ready`. +- **FR-419-020**: Claim Guard MUST block broad claims including "100% Microsoft 365 coverage", "Full M365 coverage", "Certified M365 coverage", "Restore-ready M365 coverage", "Complete tenant coverage", "All Microsoft 365 resources supported", and "All TCM resources certified". +- **FR-419-021**: Claim Guard MAY allow internal registry-only wording only when it is explicitly denominator-scoped, for example "100% registry coverage for seeded Entra resource type entries". +- **FR-419-022**: Restore posture for high-risk M365 resources MUST use `not_restorable` or `preview_only`; it MUST NOT use `restorable`. +- **FR-419-023**: The implementation MUST NOT add runtime HTTP fetches, Microsoft Learn scraping, Graph calls, TCM calls, scheduler sync, queue sync, capture jobs, start actions, or customer output for this registry expansion. +- **FR-419-024**: The implementation MUST NOT create workload-specific tables, domain engines, mini-platform namespaces, or standalone M365 dashboards. +- **FR-419-025**: The implementation MUST NOT introduce `tenant_id` as Coverage v2 internal ownership truth. +- **FR-419-026**: The implementation MUST NOT create concrete environment-owned `TenantConfigurationResource` or `TenantConfigurationResourceEvidence` rows as part of registry seed/sync. +- **FR-419-027**: New supported-scope planning rows MUST NOT accidentally change the existing Coverage v2 operator surface default scope selection. If active planning scopes would sort before current Intune scopes, implementation MUST keep them inactive, introduce an explicit default-ordering contract, or amend Product Surface/browser proof to cover the changed default. +- **FR-419-028**: Because the existing Spec 418 operator surface reads active Coverage v2 registry data, implementation MUST treat visible new rows/scopes as a data-driven Product Surface impact and provide focused proof that no broad M365 coverage label, customer claim, capture/start action, restore action, certification action, report/download, or new primary navigation appears. +- **FR-419-029**: If implementation changes any rendered UI file, route, navigation, Filament page/provider, action, report, download, or customer surface beyond the existing data-driven registry display, work MUST stop until Product Surface Impact, Browser Verification Plan, Human Product Sanity, and focused browser proof tasks are amended. + +## Non-Functional Requirements *(mandatory)* + +- **NFR-419-001**: Registry sync/seed behavior MUST be deterministic and idempotent. +- **NFR-419-002**: Static manifest/config data MUST contain no secrets, tenant-specific values, raw provider payloads, or customer data. +- **NFR-419-003**: Migrations/check constraints MUST be reversible where practical and must avoid long locks. +- **NFR-419-004**: Tests MUST fail if partial catalogs are treated as full workload coverage. +- **NFR-419-005**: Tests MUST fail if broad M365/certified/restore-ready claims become allowed. +- **NFR-419-006**: Tests MUST fail if runtime Graph/TCM/docs-fetch code is added in this spec. +- **NFR-419-007**: Tests MUST fail if `tenant_id` or workload-specific mini-platform tables/classes appear in Spec 419 changes. + +## Key Entities *(include if feature involves data)* + +- **TenantConfigurationResourceType**: Existing Coverage v2 registry definition for canonical resource type, source class, workload, support state, default coverage/evidence/identity/claim state, restore tier, and metadata. +- **TenantConfigurationSupportedScope**: Existing supported-scope definition for denominator planning, minimum coverage level, included resource types, beta/fallback allowance, customer claim allowance, and metadata. +- **ResourceTypeRegistry**: Existing service/static registry path that should be expanded rather than replaced. +- **ClaimGuard**: Existing service that blocks unsafe coverage claims and must receive M365 broad-claim guard behavior. +- **Workload metadata**: Existing enum/check values plus metadata representing documentation status and catalog completeness; no separate workload table by default. + +## Acceptance Criteria *(mandatory)* + +- **AC-419-001**: Coverage v2 can classify Entra, Exchange, Teams, Security and Compliance, Defender, and Purview workload families. +- **AC-419-002**: Required representative Entra, Exchange, Teams, and Security and Compliance resource types exist in registry definitions. +- **AC-419-003**: Defender and Purview are represented safely without fake certified resource entries. +- **AC-419-004**: Full versus seeded/partial catalog status is explicit. +- **AC-419-005**: Broad M365, certified, restore-ready, complete-tenant, and all-resource claims are blocked. +- **AC-419-006**: No M365-wide certified or full-coverage supported scope exists. +- **AC-419-007**: New non-Intune entries default to detected/not-captured/internal-only or claim-blocked, not content-backed/comparable/renderable/restorable/certified. +- **AC-419-008**: No Graph/TCM/provider runtime call, capture job, scheduler, docs fetch, or concrete resource/evidence creation is added. +- **AC-419-009**: No `tenant_id`, v1 gap taxonomy, v1-to-v2 adapter, dual write, fallback reader, or old snapshot promotion is added. +- **AC-419-010**: No workload-specific mini-platform table/class/engine is introduced. +- **AC-419-011**: UI impact is explicitly no-impact, or the spec is amended before any UI change. +- **AC-419-012**: Focused unit/feature tests and `git diff --check` pass, with PostgreSQL lane included if check constraints/migrations change. + +## Success Criteria *(mandatory)* + +- **SC-419-001**: Implementation report includes workload matrix with documentation status, entries added, full catalog decision, default support, and claim state. +- **SC-419-002**: Implementation report includes representative resource type matrix with source class, support state, coverage level, restore tier, and claim state. +- **SC-419-003**: Claim Guard proof shows forbidden broad claims blocked and scoped internal registry-only wording handled safely. +- **SC-419-004**: Static/no-runtime-capture proof shows no remote calls, no docs fetch, no capture jobs, and no concrete evidence rows. +- **SC-419-005**: Product Surface proof records focused existing-surface browser/Human Product Sanity results when active rows/scopes render, or records exact proof that no rendered output changed. + +## Assumptions + +- Specs 414, 415, 417, and 418 remain completed/validated dependency context. +- Microsoft TCM catalog names in the user draft are accepted as planning input for representative entries; implementation must keep source metadata explicit and reviewable. +- The initial implementation may use seeded/partial representative catalogs instead of a full static manifest if full catalog import is too large for a bounded review. +- Existing `metadata` JSONB is sufficient for documentation status, catalog source, aliases, review date, risk tier, and full-vs-partial marker unless implementation proves a queryable column is required. +- Existing `preview_only` restore tier is the repo-real conservative equivalent for compare/manual-review planning posture; no auto-restore claim is allowed. +- Existing Spec 418 operator surfaces may render active registry rows/scopes generically; implementation must either keep new planning rows from changing rendered output or run the focused existing-surface proof required by this spec. +- `tenantpilot` is included as an internal/platform workload value only; no TenantPilot-owned resource types or customer claims are required in this spec. + +## Open Questions + +None blocking. Implementation must make three bounded choices and record them in the implementation report: + +1. Whether to use representative seeded entries only or a full static reviewed manifest for documented Entra, Exchange, Teams, and Security and Compliance resource catalogs. +2. Whether documentation status stays in `metadata` or needs a narrow column/constraint because tests or query paths require it. +3. Whether new planning scopes are inactive, explicitly ordered, or otherwise guarded so the existing Coverage v2 operator surface default scope does not accidentally switch away from the current Intune default. + +## Risks + +| Risk | Severity | Mitigation | +|---|---:|---| +| Registry expansion becomes capture implementation | High | No remote calls, no capture jobs, no OperationRun, guard tests | +| Partial catalog presented as full | High | `is_full_catalog = false`, documentation status, tests | +| Broad M365 overclaim | High | Claim Guard tests and forbidden scope checks | +| Restore overclaim for high-risk domains | High | `not_restorable` or `preview_only`, never `restorable` | +| Domain mini-platforms appear | High | shared Coverage v2 registry only, static guards | +| Defender/Purview ambiguity | Medium | overview-only/combined metadata, no fake certified resource types | +| Provider coupling leaks into platform core | High | source metadata only, no `tenant_id`, no provider-native IDs as ownership | +| Enum/check constraint expansion causes drift | Medium | proportionality review, focused tests, PostgreSQL lane if constraints change | + +## Follow-Up Spec Candidates + +- M365 Generic Evidence Coverage Pack. +- Entra Core Comparable/Renderable Pack. +- Exchange and Teams Comparable Pack. +- Security and Compliance Readiness Pack. +- Entra Certified Compare Pack. +- Exchange/Teams Certified Pack. +- Security and Compliance Compare Pack. +- Customer-facing M365 coverage/reporting only after evidence, compare/render, and certification truth exists. diff --git a/specs/419-m365-tcm-workload-registry-expansion/tasks.md b/specs/419-m365-tcm-workload-registry-expansion/tasks.md new file mode 100644 index 00000000..bbd72eae --- /dev/null +++ b/specs/419-m365-tcm-workload-registry-expansion/tasks.md @@ -0,0 +1,122 @@ +# Tasks: Spec 419 - M365 TCM Workload Registry Expansion + +**Input**: `specs/419-m365-tcm-workload-registry-expansion/spec.md`, `specs/419-m365-tcm-workload-registry-expansion/plan.md`, `specs/419-m365-tcm-workload-registry-expansion/checklists/requirements.md` +**Prerequisites**: completed Specs 414, 415, 417, and 418 as read-only dependency context +**Tests**: Required. Runtime registry/default/claim behavior must be covered with focused Pest unit and feature/static guard tests. PostgreSQL lane is required if migrations/check constraints/indexes change. Focused browser proof is required if new active registry rows/scopes render on the existing Spec 418 Coverage v2 operator surface. + +## Test Governance Checklist + +- [x] Lane assignment is named and is the narrowest sufficient proof for registry/default/claim behavior. +- [x] New or changed tests stay in Unit/Feature lanes; PostgreSQL lane is explicit only if schema/check constraints change. +- [x] Shared helpers, factories, seeds, fixtures, and context defaults stay cheap by default and opt-in. +- [x] Planned validation commands cover the change without pulling unrelated lane cost. +- [x] Browser proof is required for data-driven existing-surface changes, or explicitly `N/A` only with proof that no new rows/scopes render. +- [x] Human Product Sanity and Product Surface implementation-report close-out cover existing-surface data impact, or are `N/A` only with proof that no rendered output changed. +- [x] Material budget, baseline, trend, or escalation notes are recorded if test cost changes. + +## Phase 1: Preflight And Dependency Guard + +- [x] T001 Capture branch, HEAD, `git status --short`, activated skills, and hard-gate status in `specs/419-m365-tcm-workload-registry-expansion/implementation-report.md`. +- [x] T002 Confirm `specs/414-tcm-first-coverage-core-cutover/implementation-report.md`, `specs/415-generic-content-backed-capture/implementation-report.md`, `specs/417-canonical-identity-engine/`, and `specs/418-coverage-v2-operator-surface/` are dependency context only and must not be modified. +- [x] T003 Confirm current Coverage v2 registry surfaces exist: `TenantConfigurationResourceType`, `TenantConfigurationSupportedScope`, `ResourceTypeRegistry`, Coverage v2 enum classes, and `ClaimGuard`. +- [x] T004 Inspect current `ResourceTypeRegistry::defaultDefinitions()`, supported-scope definitions, migrations/check constraints, factories, and Claim Guard rules before editing. +- [x] T005 Record the draft-to-repo mapping for missing draft terms: no `tenantpilot_internal` source class, no `detected_only` support state, no `compare_only` or `manual_review_required` restore tier. +- [x] T006 Stop if Coverage v2 registry or Claim Guard is missing, or if implementation would require capture, compare, render, restore, certification, customer output, runtime docs fetch, UI activation, `tenant_id`, or workload-specific mini-platforms. + +## Phase 2: Tests First - Workloads, Manifest, And Defaults + +- [x] T007 Add focused workload registry tests proving `intune`, `entra`, `exchange`, `teams`, `security_compliance`, `defender`, `purview`, `tenantpilot`, and `unknown` are accepted by the shared Coverage v2 workload enum/check path. +- [x] T008 Add manifest/default tests proving new non-Intune entries default to `default_coverage_level = detected`, `default_evidence_state = not_captured`, and `default_claim_state = internal_only` or `claim_blocked`. +- [x] T009 Add tests proving new non-Intune entries do not default to `content_backed`, `comparable`, `renderable`, `restorable`, `certified`, or `claim_allowed`. +- [x] T010 Add documentation status tests proving `documented_resource_catalog`, `documented_overview_only`, `combined_catalog`, `graph_only`, `internal`, and `unknown` are represented in metadata or a justified field. +- [x] T011 Add partial-vs-full catalog tests proving seeded/partial manifests use `is_full_catalog = false` or equivalent metadata and cannot be treated as full workload coverage. +- [x] T012 Add restore-tier default tests proving high-risk resource types use `not_restorable` or `preview_only`, never `restorable`. + +## Phase 3: Tests First - Representative Resource Types + +- [x] T013 Add Entra registry tests for `conditionalAccessPolicy`, `securityDefaults`, `application`, `servicePrincipal`, `roleDefinition`, and `administrativeUnit`. +- [x] T014 Add Exchange registry tests for `transportRule`, `acceptedDomain`, `sharedMailbox`, `remoteDomain`, `mailboxPlan`, and `organizationConfig`. +- [x] T015 Add Teams registry tests for `appPermissionPolicy`, `appSetupPolicy`, `meetingPolicy`, `messagingPolicy`, `teamsUpdateManagementPolicy`, and `voiceRoute`. +- [x] T016 Add Security and Compliance registry tests for `labelPolicy`, `retentionCompliancePolicy`, `dlpCompliancePolicy` or repo-canonical equivalent, `autoSensitivityLabelPolicy`, `protectionAlert`, and `complianceTag`. +- [x] T017 Add Defender/Purview workload status tests proving they are represented under `tenant_configuration_supported_scopes.metadata.workload_documentation_status.defender` and `.purview` on the aggregate M365 planning scope, and are not represented as fake certified resource types. + +## Phase 4: Tests First - Supported Scopes And Claim Guard + +- [x] T018 Add supported-scope tests for `m365_tcm_registry_detected`, `entra_tcm_registry_detected`, `exchange_tcm_registry_detected`, `teams_tcm_registry_detected`, `security_compliance_tcm_registry_detected`, `m365_tcm_generic_future`, and `m365_tcm_certified_none`, including proof that new planning scopes do not accidentally become the existing Coverage v2 operator surface default scope. +- [x] T019 Add tests proving forbidden scopes do not exist: `m365_full_coverage`, `m365_certified`, `all_microsoft_365_supported`, `full_tenant_coverage`, and `full_m365_restore_ready`. +- [x] T020 Add Claim Guard tests blocking `100% Microsoft 365 coverage`, `Full M365 coverage`, `Certified M365 coverage`, `Restore-ready M365 coverage`, `Complete tenant coverage`, `All Microsoft 365 resources supported`, and `All TCM resources certified`. +- [x] T021 Add Claim Guard tests proving internal registry-only percent wording is allowed only when explicitly denominator-scoped, for example seeded Entra registry entries. + +## Phase 5: Tests First - No Runtime Capture, No Tenant ID, No Mini-Platform + +- [x] T022 Add static/feature guard proving no Graph/TCM/provider remote call path or runtime Microsoft documentation fetch is introduced by Spec 419. +- [x] T023 Add guard proving no capture job, scheduler sync, queue sync, capture/start action, restore/apply action, publish/export action, or certification action is added. +- [x] T024 Add guard proving registry sync/seed does not create concrete `TenantConfigurationResource` or `TenantConfigurationResourceEvidence` rows. +- [x] T025 Add schema/source guard proving no `tenant_id` is introduced as Coverage v2 ownership truth. +- [x] T026 Add guard proving no workload-specific tables/classes/engines are introduced for Entra, Exchange, Teams, Security and Compliance, Defender, or Purview. +- [x] T027 Add guard proving no v1 gap taxonomy, v1-to-v2 adapter, fallback reader, old snapshot promotion, dual write, or customer-facing dual truth appears. + +## Phase 6: Workload Enum And Registry Metadata + +- [x] T028 Expand or confirm `apps/platform/app/Support/TenantConfiguration/Workload.php` values and related database check constraints for the required workload set. +- [x] T029 Prefer existing JSONB `metadata` for `documentation_status`, `catalog_source`, `catalog_last_reviewed_at`, `source_aliases`, `risk_tier`, `default_restore_posture`, `is_full_catalog`, and `catalog_import_batch`. +- [x] T030 If documentation status or catalog metadata needs a dedicated column/constraint, add a narrow reversible migration and record the proportionality reason in the implementation report. +- [x] T031 Ensure enum/check-constraint additions are mirrored across model casts, migrations, factories, tests, and any registry sync path. + +## Phase 7: Resource Type Manifest / Registry Expansion + +- [x] T032 Update `ResourceTypeRegistry::defaultDefinitions()` or repo-equivalent static manifest/config with M365 representative entries. +- [x] T033 Ensure TCM-documented entries use `source_class = tcm`. +- [x] T034 Ensure all new non-Intune entries use conservative defaults: support `out_of_scope` unless a new state is justified, coverage `detected`, evidence `not_captured`, claim `internal_only` or `claim_blocked`, restore `not_restorable` or `preview_only`. +- [x] T035 Add Entra representative entries with high-risk defaults for Conditional Access, Security Defaults, and role definitions. +- [x] T036 Add Exchange representative entries with high-risk defaults for transport rules and organization configuration. +- [x] T037 Add Teams representative entries with manual-review/preview-only defaults. +- [x] T038 Add Security and Compliance representative entries with high-risk defaults for labels, retention, DLP, and auto-sensitivity label policies. +- [x] T039 Represent Defender and Purview through `tenant_configuration_supported_scopes.metadata.workload_documentation_status` on the aggregate M365 planning scope without inventing fake certified resource types. +- [x] T040 Ensure aliases such as `dataLossPreventionPolicy` vs `dlpCompliancePolicy` are source aliases, not duplicate canonical types, unless implementation documents a reason. + +## Phase 8: Supported Scope Planning + +- [x] T041 Add or update supported-scope planning entries required by `spec.md`, preserving the existing operator-surface default scope unless the changed default is explicitly covered by Product Surface/browser proof. +- [x] T042 Ensure scope metadata marks registry-only/detected planning status and `customer_claims_allowed = false` for broad M365 scopes. +- [x] T043 Ensure `m365_tcm_certified_none` explicitly states no M365-wide certified scope exists. +- [x] T044 Ensure `m365_tcm_generic_future` is marked future-only and cannot imply active generic capture. + +## Phase 9: Claim Guard Expansion + +- [x] T045 Update `ClaimGuard` or repo-equivalent claim-safety path to block broad M365, certified, restore-ready, complete-tenant, all-resource, and unscoped percent claims. +- [x] T046 Allow only explicit internal/operator registry-only denominator-scoped wording when supported by scope metadata. +- [x] T047 Ensure Claim Guard results for new workloads never imply content-backed, comparable, renderable, restorable, certified, or customer-ready coverage by default. + +## Phase 10: Product Surface Data-Impact And Deployment Review + +- [x] T048 Confirm no UI route, Filament page/provider, navigation entry, Blade view, Livewire component, action, report, download, customer output, or rendered label changed; document any existing Spec 418 operator-surface data impact from active registry rows/scopes. +- [x] T049 Run focused existing-surface feature/browser proof if new rows/scopes render: workload filters/scope options are intentional, registry-only status is clear, no broad M365 coverage label appears, no capture/restore/certify/report/download action appears, and no console/Livewire/500 errors appear. +- [x] T050 If any runtime UI code, route, navigation, action, report, download, customer output, or rendered label change is required beyond data-driven existing registry rows, stop and amend `spec.md`, `plan.md`, and `tasks.md` before runtime UI edits. +- [x] T051 Document deployment impact: migrations/check constraints if changed, no env vars, no queues, no scheduler, no storage, no assets, no `filament:assets` requirement unless scope is amended. +- [x] T052 Document staging validation expectations for schema/registry changes before production promotion. + +## Phase 11: Validation And Close-Out + +- [x] T053 Run `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent`. +- [x] T054 Run `cd apps/platform && ./vendor/bin/sail artisan test tests/Unit/Support/TenantConfiguration/Spec419M365WorkloadRegistryTest.php tests/Unit/Support/TenantConfiguration/Spec419M365ClaimGuardTest.php`. +- [x] T055 Run `cd apps/platform && ./vendor/bin/sail artisan test tests/Feature/TenantConfiguration/Spec419M365RegistryExpansionTest.php`. +- [x] T056 If active registry rows/scopes render on the existing Spec 418 surface, run `cd apps/platform && ./vendor/bin/sail artisan test tests/Browser/Spec419M365RegistryOperatorSurfaceSmokeTest.php` or the repo-equivalent focused browser smoke path. +- [x] T057 If migrations/check constraints/indexes changed, run `cd apps/platform && ./vendor/bin/sail php vendor/bin/pest -c phpunit.pgsql.xml tests/Feature/TenantConfiguration/Spec419M365RegistryExpansionTest.php`. +- [x] T058 Run `git diff --check`. +- [x] T059 Complete `specs/419-m365-tcm-workload-registry-expansion/implementation-report.md` with candidate gate result, dirty state before/after, files changed, workload matrix, representative type matrix, full-vs-partial catalog decision, Claim Guard proof, restore tier proof, no-runtime-capture proof, no-tenant_id proof, no-mini-platform proof, Product Surface data-impact decision, tests/browser proof run or N/A proof, deployment impact, and deferred work. +- [x] T060 Confirm no completed historical spec was rewritten or stripped of close-out, validation, task, smoke, browser, or review history. + +## Stop Conditions + +Stop and update `spec.md`, `plan.md`, and `tasks.md` before continuing if any of these appear: + +- Capture, compare, render, restore, apply, certification, customer output, Review Pack/report, broad M365 dashboard, or customer-facing claim activation is needed. +- Graph/TCM/provider remote calls or runtime Microsoft documentation fetch are needed. +- UI route/page/navigation/action/rendered label changes are needed beyond the existing data-driven registry display. +- Existing Coverage v2 operator surface default scope would change without explicit Product Surface/browser proof. +- A partial catalog cannot be labeled as partial. +- A new source/support/restore enum value is needed without proportionality proof. +- `tenant_id` appears as Coverage v2 ownership truth. +- A workload-specific table, model, engine, or mini-platform is introduced. +- A broad M365/certified/restore-ready/all-resource claim must be allowed.