*/ public function terms(): array { return array_values(array_map( static fn (array $term): PlatformVocabularyTerm => PlatformVocabularyTerm::fromArray($term), $this->configuredTerms(), )); } public function term(string $term): ?PlatformVocabularyTerm { $normalized = trim(mb_strtolower($term)); foreach ($this->terms() as $candidate) { if (trim(mb_strtolower($candidate->termKey)) === $normalized) { return $candidate; } } return $this->resolveAlias($term); } public function resolveAlias(string $term, ?string $context = null): ?PlatformVocabularyTerm { $normalized = trim(mb_strtolower($term)); $normalizedContext = is_string($context) ? trim(mb_strtolower($context)) : null; foreach ($this->terms() as $candidate) { $aliases = array_map( static fn (string $alias): string => trim(mb_strtolower($alias)), $candidate->legacyAliases, ); if (! in_array($normalized, $aliases, true)) { continue; } if ($normalizedContext !== null && $candidate->allowedContexts !== []) { $contexts = array_map( static fn (string $allowedContext): string => trim(mb_strtolower($allowedContext)), $candidate->allowedContexts, ); if (! in_array($normalizedContext, $contexts, true)) { continue; } } return $candidate; } return null; } public function canonicalName(string $term): ?string { return $this->term($term)?->termKey; } public function isCanonical(string $term): bool { $resolved = $this->term($term); if (! $resolved instanceof PlatformVocabularyTerm) { return false; } return trim(mb_strtolower($term)) === trim(mb_strtolower($resolved->termKey)); } public function ownership(string $term): ?string { return $this->term($term)?->ownerLayer; } /** * @return list */ public function canonicalTerms(): array { return array_values(array_map( static fn (PlatformVocabularyTerm $term): string => $term->termKey, $this->terms(), )); } /** * @return array, * legacy_aliases: list, * alias_retirement_path: ?string, * forbidden_platform_aliases: list * }> */ public function termInventory(): array { return collect($this->terms()) ->mapWithKeys(static fn (PlatformVocabularyTerm $term): array => [ $term->termKey => $term->toArray(), ]) ->all(); } /** * @return array> */ public function legacyAliases(): array { return collect($this->terms()) ->filter(static fn (PlatformVocabularyTerm $term): bool => $term->legacyAliases !== []) ->mapWithKeys(static fn (PlatformVocabularyTerm $term): array => [ $term->termKey => $term->legacyAliases, ]) ->all(); } /** * @return array, * retirement_path: ?string, * forbidden_platform_aliases: list * }> */ public function aliasRetirementInventory(): array { return collect($this->terms()) ->filter(static fn (PlatformVocabularyTerm $term): bool => $term->legacyAliases !== []) ->mapWithKeys(static fn (PlatformVocabularyTerm $term): array => [ $term->termKey => [ 'canonical_name' => $term->termKey, 'legacy_aliases' => $term->legacyAliases, 'retirement_path' => $term->aliasRetirementPath, 'forbidden_platform_aliases' => $term->forbiddenPlatformAliases, ], ]) ->all(); } public function boundaryClassification(string $term): ?string { return $this->term($term)?->boundaryClassification; } /** * @return list */ public function allowedBoundaryClassifications(): array { return [ self::BOUNDARY_PLATFORM_CORE, self::BOUNDARY_CROSS_DOMAIN_GOVERNANCE, self::BOUNDARY_INTUNE_SPECIFIC, ]; } /** * @return list */ public function registries(): array { return array_values(array_map( static fn (array $descriptor): RegistryOwnershipDescriptor => RegistryOwnershipDescriptor::fromArray($descriptor), $this->configuredRegistries(), )); } public function registry(string $registryKey): ?RegistryOwnershipDescriptor { $normalized = trim(mb_strtolower($registryKey)); foreach ($this->registries() as $descriptor) { if (trim(mb_strtolower($descriptor->registryKey)) === $normalized) { return $descriptor; } } return null; } /** * @return array, * allowed_consumers: list, * compatibility_notes: ?string * }> */ public function registryInventory(): array { return collect($this->registries()) ->mapWithKeys(static fn (RegistryOwnershipDescriptor $descriptor): array => [ $descriptor->registryKey => $descriptor->toArray(), ]) ->all(); } /** * @return array */ public function reasonNamespaceInventory(): array { return $this->configuredReasonNamespaces(); } /** * @return array{ * owner_namespace: string, * boundary_classification: string, * owner_layer: string, * compatibility_notes: ?string * }|null */ public function reasonNamespace(string $ownerNamespace): ?array { $normalized = trim(mb_strtolower($ownerNamespace)); foreach ($this->reasonNamespaceInventory() as $descriptor) { $candidate = is_string($descriptor['owner_namespace'] ?? null) ? trim(mb_strtolower((string) $descriptor['owner_namespace'])) : null; if ($candidate === $normalized) { return $descriptor; } } return null; } public function classifyReasonNamespace(string $ownerNamespace): ?string { return $this->reasonNamespace($ownerNamespace)['boundary_classification'] ?? null; } public function classifyOperationType(string $operationType): ?string { if (trim($operationType) === '') { return null; } return $this->registry('operation_catalog')?->boundaryClassification; } /** * @return array> */ private function configuredTerms(): array { $configured = config('tenantpilot.platform_vocabulary.terms'); if (is_array($configured) && $configured !== []) { return $configured; } return [ 'governed_subject' => [ 'term_key' => 'governed_subject', 'canonical_label' => 'Governed subject', 'canonical_description' => 'The platform-facing noun for a governed object across compare, snapshot, evidence, and review surfaces.', 'boundary_classification' => self::BOUNDARY_PLATFORM_CORE, 'owner_layer' => self::OWNER_PLATFORM_CORE, 'allowed_contexts' => ['compare', 'snapshot', 'evidence', 'review', 'reporting'], 'legacy_aliases' => ['policy_type'], 'alias_retirement_path' => 'Retire false-universal policy_type wording from platform-owned summaries and descriptors while preserving Intune-owned storage.', 'forbidden_platform_aliases' => ['policy_type'], ], 'domain_key' => [ 'term_key' => 'domain_key', 'canonical_label' => 'Governance domain', 'canonical_description' => 'The canonical domain discriminator for cross-domain and platform-near contracts.', 'boundary_classification' => self::BOUNDARY_CROSS_DOMAIN_GOVERNANCE, 'owner_layer' => self::OWNER_PLATFORM_CORE, 'allowed_contexts' => ['governance', 'compare', 'snapshot', 'reporting'], 'legacy_aliases' => [], 'alias_retirement_path' => null, 'forbidden_platform_aliases' => [], ], 'subject_class' => [ 'term_key' => 'subject_class', 'canonical_label' => 'Subject class', 'canonical_description' => 'The canonical subject-class discriminator for governed-subject contracts.', 'boundary_classification' => self::BOUNDARY_CROSS_DOMAIN_GOVERNANCE, 'owner_layer' => self::OWNER_PLATFORM_CORE, 'allowed_contexts' => ['governance', 'compare', 'snapshot'], 'legacy_aliases' => [], 'alias_retirement_path' => null, 'forbidden_platform_aliases' => [], ], 'subject_type_key' => [ 'term_key' => 'subject_type_key', 'canonical_label' => 'Governed subject key', 'canonical_description' => 'The domain-owned subject-family key used by platform-near descriptors.', 'boundary_classification' => self::BOUNDARY_CROSS_DOMAIN_GOVERNANCE, 'owner_layer' => self::OWNER_PLATFORM_CORE, 'allowed_contexts' => ['governance', 'compare', 'snapshot', 'evidence'], 'legacy_aliases' => ['policy_type'], 'alias_retirement_path' => 'Prefer subject_type_key on platform-near payloads and keep policy_type only where the owning model is Intune-specific.', 'forbidden_platform_aliases' => ['policy_type'], ], 'subject_type_label' => [ 'term_key' => 'subject_type_label', 'canonical_label' => 'Governed subject label', 'canonical_description' => 'The operator-facing label for a governed subject family.', 'boundary_classification' => self::BOUNDARY_CROSS_DOMAIN_GOVERNANCE, 'owner_layer' => self::OWNER_PLATFORM_CORE, 'allowed_contexts' => ['snapshot', 'evidence', 'compare', 'review'], 'legacy_aliases' => [], 'alias_retirement_path' => null, 'forbidden_platform_aliases' => [], ], 'resource_type' => [ 'term_key' => 'resource_type', 'canonical_label' => 'Resource type', 'canonical_description' => 'Optional resource-shaped noun for platform-facing summaries when a governed subject also needs a resource family label.', 'boundary_classification' => self::BOUNDARY_PLATFORM_CORE, 'owner_layer' => self::OWNER_PLATFORM_CORE, 'allowed_contexts' => ['reporting', 'review'], 'legacy_aliases' => [], 'alias_retirement_path' => null, 'forbidden_platform_aliases' => [], ], 'operation_type' => [ 'term_key' => 'operation_type', 'canonical_label' => 'Operation type', 'canonical_description' => 'The canonical platform operation identifier shown on monitoring and launch surfaces.', 'boundary_classification' => self::BOUNDARY_PLATFORM_CORE, 'owner_layer' => self::OWNER_PLATFORM_CORE, 'allowed_contexts' => ['monitoring', 'reporting', 'launch_surfaces'], 'legacy_aliases' => ['type'], 'alias_retirement_path' => 'Expose canonical operation_type on read models while operation_runs.type remains a compatibility storage seam during rollout.', 'forbidden_platform_aliases' => [], ], 'platform_reason_family' => [ 'term_key' => 'platform_reason_family', 'canonical_label' => 'Platform reason family', 'canonical_description' => 'The cross-domain platform family that explains the top-level cause category without erasing domain ownership.', 'boundary_classification' => self::BOUNDARY_PLATFORM_CORE, 'owner_layer' => self::OWNER_PLATFORM_CORE, 'allowed_contexts' => ['reason_translation', 'monitoring', 'review', 'reporting'], 'legacy_aliases' => [], 'alias_retirement_path' => null, 'forbidden_platform_aliases' => [], ], 'reason_owner.owner_namespace' => [ 'term_key' => 'reason_owner.owner_namespace', 'canonical_label' => 'Reason owner namespace', 'canonical_description' => 'The explicit namespace that marks whether a translated reason is provider-owned, governance-owned, access-owned, or runtime-owned.', 'boundary_classification' => self::BOUNDARY_PLATFORM_CORE, 'owner_layer' => self::OWNER_PLATFORM_CORE, 'allowed_contexts' => ['reason_translation', 'review', 'reporting'], 'legacy_aliases' => [], 'alias_retirement_path' => null, 'forbidden_platform_aliases' => [], ], 'reason_code' => [ 'term_key' => 'reason_code', 'canonical_label' => 'Reason code', 'canonical_description' => 'The original domain-owned or provider-owned reason identifier preserved for diagnostics and translation lookup.', 'boundary_classification' => self::BOUNDARY_PLATFORM_CORE, 'owner_layer' => self::OWNER_PLATFORM_CORE, 'allowed_contexts' => ['reason_translation', 'diagnostics'], 'legacy_aliases' => [], 'alias_retirement_path' => null, 'forbidden_platform_aliases' => [], ], 'registry_key' => [ 'term_key' => 'registry_key', 'canonical_label' => 'Registry key', 'canonical_description' => 'The stable identifier for a contributor-facing registry or catalog.', 'boundary_classification' => self::BOUNDARY_PLATFORM_CORE, 'owner_layer' => self::OWNER_PLATFORM_CORE, 'allowed_contexts' => ['governance', 'contributor_guidance'], 'legacy_aliases' => [], 'alias_retirement_path' => null, 'forbidden_platform_aliases' => [], ], 'boundary_classification' => [ 'term_key' => 'boundary_classification', 'canonical_label' => 'Boundary classification', 'canonical_description' => 'The explicit classification that marks a term or registry as platform_core, cross_domain_governance, or intune_specific.', 'boundary_classification' => self::BOUNDARY_PLATFORM_CORE, 'owner_layer' => self::OWNER_PLATFORM_CORE, 'allowed_contexts' => ['governance', 'contributor_guidance'], 'legacy_aliases' => [], 'alias_retirement_path' => null, 'forbidden_platform_aliases' => [], ], 'policy_type' => [ 'term_key' => 'policy_type', 'canonical_label' => 'Intune policy type', 'canonical_description' => 'The Intune-specific discriminator that remains valid on adapter-owned or Intune-owned models but must not be treated as a universal platform noun.', 'boundary_classification' => self::BOUNDARY_INTUNE_SPECIFIC, 'owner_layer' => self::OWNER_DOMAIN_OWNED, 'allowed_contexts' => ['intune_adapter', 'intune_inventory', 'intune_backup'], 'legacy_aliases' => [], 'alias_retirement_path' => null, 'forbidden_platform_aliases' => ['governed_subject'], ], ]; } /** * @return array> */ private function configuredRegistries(): array { $configured = config('tenantpilot.platform_vocabulary.registries'); if (is_array($configured) && $configured !== []) { return $configured; } return [ 'governance_subject_taxonomy_registry' => [ 'registry_key' => 'governance_subject_taxonomy_registry', 'boundary_classification' => self::BOUNDARY_CROSS_DOMAIN_GOVERNANCE, 'owner_layer' => self::OWNER_PLATFORM_CORE, 'source_class_or_file' => GovernanceSubjectTaxonomyRegistry::class, 'canonical_nouns' => ['domain_key', 'subject_class', 'subject_type_key', 'subject_type_label'], 'allowed_consumers' => ['baseline_scope', 'compare', 'snapshot', 'review'], 'compatibility_notes' => 'Provides the governed-subject source of truth, resolves legacy policy-type payloads, and preserves inactive or future-domain entries without exposing them as active operator choices.', ], 'operation_catalog' => [ 'registry_key' => 'operation_catalog', 'boundary_classification' => self::BOUNDARY_PLATFORM_CORE, 'owner_layer' => self::OWNER_PLATFORM_CORE, 'source_class_or_file' => OperationCatalog::class, 'canonical_nouns' => ['operation_type'], 'allowed_consumers' => ['monitoring', 'reporting', 'launch_surfaces', 'audit'], 'compatibility_notes' => 'Resolves canonical operation meaning from historical storage values without treating every stored raw string as equally canonical.', ], 'provider_reason_codes' => [ 'registry_key' => 'provider_reason_codes', 'boundary_classification' => self::BOUNDARY_INTUNE_SPECIFIC, 'owner_layer' => self::OWNER_PROVIDER_OWNED, 'source_class_or_file' => ProviderReasonCodes::class, 'canonical_nouns' => ['reason_code'], 'allowed_consumers' => ['reason_translation'], 'compatibility_notes' => 'Provider-owned reason codes remain namespaced domain details and become platform-safe only through the reason-translation boundary.', ], 'inventory_policy_type_catalog' => [ 'registry_key' => 'inventory_policy_type_catalog', 'boundary_classification' => self::BOUNDARY_INTUNE_SPECIFIC, 'owner_layer' => self::OWNER_DOMAIN_OWNED, 'source_class_or_file' => InventoryPolicyTypeMeta::class, 'canonical_nouns' => ['policy_type'], 'allowed_consumers' => ['intune_adapter', 'inventory', 'backup'], 'compatibility_notes' => 'The supported Intune policy-type list is not a universal platform registry and must be wrapped before reuse on platform-near summaries.', ], ]; } /** * @return array */ private function configuredReasonNamespaces(): array { $configured = config('tenantpilot.platform_vocabulary.reason_namespaces'); if (is_array($configured) && $configured !== []) { return $configured; } return [ 'tenant_operability' => [ 'owner_namespace' => 'tenant_operability', 'boundary_classification' => self::BOUNDARY_PLATFORM_CORE, 'owner_layer' => self::OWNER_PLATFORM_CORE, 'compatibility_notes' => 'Tenant operability reasons are platform-core guardrails for workspace and tenant context.', ], 'execution_denial' => [ 'owner_namespace' => 'execution_denial', 'boundary_classification' => self::BOUNDARY_PLATFORM_CORE, 'owner_layer' => self::OWNER_PLATFORM_CORE, 'compatibility_notes' => 'Execution denial reasons remain platform-core run-legitimacy semantics.', ], 'operation_lifecycle' => [ 'owner_namespace' => 'operation_lifecycle', 'boundary_classification' => self::BOUNDARY_PLATFORM_CORE, 'owner_layer' => self::OWNER_PLATFORM_CORE, 'compatibility_notes' => 'Lifecycle reconciliation reasons remain platform-core monitoring semantics.', ], 'governance.baseline_compare' => [ 'owner_namespace' => 'governance.baseline_compare', 'boundary_classification' => self::BOUNDARY_CROSS_DOMAIN_GOVERNANCE, 'owner_layer' => self::OWNER_DOMAIN_OWNED, 'compatibility_notes' => 'Baseline-compare reason codes are governance-owned details translated through the platform boundary.', ], 'governance.artifact_truth' => [ 'owner_namespace' => 'governance.artifact_truth', 'boundary_classification' => self::BOUNDARY_CROSS_DOMAIN_GOVERNANCE, 'owner_layer' => self::OWNER_DOMAIN_OWNED, 'compatibility_notes' => 'Artifact-truth reason codes remain governance-owned and are surfaced through platform-safe summaries.', ], 'provider.microsoft_graph' => [ 'owner_namespace' => 'provider.microsoft_graph', 'boundary_classification' => self::BOUNDARY_INTUNE_SPECIFIC, 'owner_layer' => self::OWNER_PROVIDER_OWNED, 'compatibility_notes' => 'Microsoft Graph provider reasons remain provider-owned and Intune-specific.', ], 'provider.intune_rbac' => [ 'owner_namespace' => 'provider.intune_rbac', 'boundary_classification' => self::BOUNDARY_INTUNE_SPECIFIC, 'owner_layer' => self::OWNER_PROVIDER_OWNED, 'compatibility_notes' => 'Provider-owned Intune RBAC reasons remain Intune-specific.', ], 'rbac.intune' => [ 'owner_namespace' => 'rbac.intune', 'boundary_classification' => self::BOUNDARY_INTUNE_SPECIFIC, 'owner_layer' => self::OWNER_DOMAIN_OWNED, 'compatibility_notes' => 'RBAC detail remains Intune-specific domain context.', ], 'reason_translation.fallback' => [ 'owner_namespace' => 'reason_translation.fallback', 'boundary_classification' => self::BOUNDARY_PLATFORM_CORE, 'owner_layer' => self::OWNER_PLATFORM_CORE, 'compatibility_notes' => 'Fallback translation remains a platform-core compatibility seam until a domain owner is known.', ], ]; } }