TenantAtlas/apps/platform/app/Support/Governance/PlatformVocabularyGlossary.php
ahmido ad16eee591 Spec 204: harden platform core vocabulary (#234)
## Summary
- add the Spec 204 platform vocabulary foundation, including canonical glossary terms, registry ownership descriptors, canonical operation type and alias resolution, and explicit reason ownership and platform reason-family metadata
- harden platform-facing compare, snapshot, evidence, monitoring, review, and reporting surfaces so they prefer governed-subject and canonical operation semantics while preserving intentional Intune-owned terminology
- extend Spec 204 unit, feature, Filament, and architecture coverage and add the full spec artifacts, checklist, and completed task ledger

## Verification
- ran the focused recent-change Sail verification pack for the new glossary and reason-semantics work
- ran the full Spec 204 quickstart verification pack under Sail
- ran `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent`
- ran an integrated-browser smoke pass covering tenant dashboard, operations, operation detail, baseline compare, evidence, reviews, review packs, provider connections, inventory items, backup schedules, onboarding, and the system dashboard/operations/failures/run-detail surfaces

## Notes
- provider registration is unchanged and remains in `bootstrap/providers.php`
- no new destructive actions or asset-registration changes are introduced by this branch

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #234
2026-04-14 06:09:42 +00:00

570 lines
24 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Support\Governance;
use App\Support\Inventory\InventoryPolicyTypeMeta;
use App\Support\OperationCatalog;
use App\Support\Providers\ProviderReasonCodes;
final class PlatformVocabularyGlossary
{
public const string BOUNDARY_PLATFORM_CORE = 'platform_core';
public const string BOUNDARY_CROSS_DOMAIN_GOVERNANCE = 'cross_domain_governance';
public const string BOUNDARY_INTUNE_SPECIFIC = 'intune_specific';
public const string OWNER_PLATFORM_CORE = 'platform_core';
public const string OWNER_DOMAIN_OWNED = 'domain_owned';
public const string OWNER_PROVIDER_OWNED = 'provider_owned';
public const string OWNER_COMPATIBILITY_ALIAS = 'compatibility_alias';
public const string OWNER_COMPATIBILITY_ONLY = 'compatibility_only';
/**
* @return list<PlatformVocabularyTerm>
*/
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<string>
*/
public function canonicalTerms(): array
{
return array_values(array_map(
static fn (PlatformVocabularyTerm $term): string => $term->termKey,
$this->terms(),
));
}
/**
* @return array<string, array{
* term_key: string,
* canonical_label: string,
* canonical_description: string,
* boundary_classification: string,
* owner_layer: string,
* allowed_contexts: list<string>,
* legacy_aliases: list<string>,
* alias_retirement_path: ?string,
* forbidden_platform_aliases: list<string>
* }>
*/
public function termInventory(): array
{
return collect($this->terms())
->mapWithKeys(static fn (PlatformVocabularyTerm $term): array => [
$term->termKey => $term->toArray(),
])
->all();
}
/**
* @return array<string, list<string>>
*/
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<string, array{
* canonical_name: string,
* legacy_aliases: list<string>,
* retirement_path: ?string,
* forbidden_platform_aliases: list<string>
* }>
*/
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<string>
*/
public function allowedBoundaryClassifications(): array
{
return [
self::BOUNDARY_PLATFORM_CORE,
self::BOUNDARY_CROSS_DOMAIN_GOVERNANCE,
self::BOUNDARY_INTUNE_SPECIFIC,
];
}
/**
* @return list<RegistryOwnershipDescriptor>
*/
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<string, array{
* registry_key: string,
* boundary_classification: string,
* owner_layer: string,
* source_class_or_file: string,
* canonical_nouns: list<string>,
* allowed_consumers: list<string>,
* compatibility_notes: ?string
* }>
*/
public function registryInventory(): array
{
return collect($this->registries())
->mapWithKeys(static fn (RegistryOwnershipDescriptor $descriptor): array => [
$descriptor->registryKey => $descriptor->toArray(),
])
->all();
}
/**
* @return array<string, array{
* owner_namespace: string,
* boundary_classification: string,
* owner_layer: string,
* compatibility_notes: ?string
* }>
*/
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<string, array<string, mixed>>
*/
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<string, array<string, mixed>>
*/
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<string, array{
* owner_namespace: string,
* boundary_classification: string,
* owner_layer: string,
* compatibility_notes: ?string
* }>
*/
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.',
],
];
}
}