$context */ public function translate( ?string $reasonCode, ?string $artifactKey = null, string $surface = 'detail', array $context = [], ): ?ReasonResolutionEnvelope { $reasonCode = is_string($reasonCode) ? trim($reasonCode) : ''; if ($reasonCode === '') { return null; } $envelope = match (true) { $artifactKey === ProviderReasonTranslator::ARTIFACT_KEY, $artifactKey === null && $this->providerReasonTranslator->canTranslate($reasonCode) => $this->providerReasonTranslator->translate($reasonCode, $surface, $context), $artifactKey === self::GOVERNANCE_ARTIFACT_TRUTH_ARTIFACT && BaselineCompareReasonCode::tryFrom($reasonCode) instanceof BaselineCompareReasonCode => $this->translateBaselineCompareReason($reasonCode), $artifactKey === null && BaselineCompareReasonCode::tryFrom($reasonCode) instanceof BaselineCompareReasonCode => $this->translateBaselineCompareReason($reasonCode), $artifactKey === self::GOVERNANCE_ARTIFACT_TRUTH_ARTIFACT && BaselineReasonCodes::isKnown($reasonCode) => $this->translateBaselineReason($reasonCode), $artifactKey === null && BaselineReasonCodes::isKnown($reasonCode) => $this->translateBaselineReason($reasonCode), $artifactKey === self::EXECUTION_DENIAL_ARTIFACT, $artifactKey === null && ExecutionDenialReasonCode::tryFrom($reasonCode) instanceof ExecutionDenialReasonCode => ExecutionDenialReasonCode::tryFrom($reasonCode)?->toReasonResolutionEnvelope($surface, $context), $artifactKey === null && LifecycleReconciliationReason::tryFrom($reasonCode) instanceof LifecycleReconciliationReason => LifecycleReconciliationReason::tryFrom($reasonCode)?->toReasonResolutionEnvelope($surface, $context), $artifactKey === self::TENANT_OPERABILITY_ARTIFACT, $artifactKey === null && TenantOperabilityReasonCode::tryFrom($reasonCode) instanceof TenantOperabilityReasonCode => TenantOperabilityReasonCode::tryFrom($reasonCode)?->toReasonResolutionEnvelope($surface, $context), $artifactKey === self::RBAC_ARTIFACT, $artifactKey === null && RbacReason::tryFrom($reasonCode) instanceof RbacReason => RbacReason::tryFrom($reasonCode)?->toReasonResolutionEnvelope($surface, $context), $artifactKey === self::GOVERNANCE_ARTIFACT_TRUTH_ARTIFACT => $this->fallbackReasonTranslator->translate($reasonCode, $surface, $context), $artifactKey === null && ProviderReasonCodes::isKnown($reasonCode) => $this->providerReasonTranslator->translate($reasonCode, $surface, $context), default => $this->fallbackTranslate($reasonCode, $artifactKey, $surface, $context), }; return $this->withOwnership($envelope, $reasonCode, $artifactKey); } /** * @param array $context */ public function boundaryClassification( ?string $reasonCode, ?string $artifactKey = null, string $surface = 'detail', array $context = [], ): ?string { return $this->boundaryClassificationForEnvelope( $this->translate($reasonCode, $artifactKey, $surface, $context), ); } public function boundaryClassificationForEnvelope(?ReasonResolutionEnvelope $envelope): ?string { return $this->boundaryClassificationForNamespace($envelope?->ownerNamespace()); } public function boundaryClassificationForNamespace(?string $ownerNamespace): ?string { if (! is_string($ownerNamespace) || trim($ownerNamespace) === '') { return null; } return $this->glossary->classifyReasonNamespace($ownerNamespace); } /** * @param array $context */ private function fallbackTranslate( string $reasonCode, ?string $artifactKey, string $surface, array $context, ): ?ReasonResolutionEnvelope { if ($artifactKey === null) { $normalizedCode = \App\Support\OpsUx\RunFailureSanitizer::normalizeReasonCode($reasonCode); if ($normalizedCode !== $reasonCode) { return $this->translate($normalizedCode, null, $surface, $context + ['source_reason_code' => $reasonCode]); } } return $this->fallbackReasonTranslator->translate($reasonCode, $surface, $context); } private function translateBaselineReason(string $reasonCode): ReasonResolutionEnvelope { [$operatorLabel, $shortExplanation, $actionability, $nextStep] = match ($reasonCode) { BaselineReasonCodes::CAPTURE_MISSING_SOURCE_TENANT => [ 'Source tenant unavailable', 'The selected tenant is not available in this workspace for baseline capture.', 'prerequisite_missing', 'Select a source tenant from the same workspace before capturing again.', ], BaselineReasonCodes::CAPTURE_PROFILE_NOT_ACTIVE => [ 'Baseline profile inactive', 'Only active baseline profiles can be captured or compared.', 'prerequisite_missing', 'Activate the baseline profile before retrying this action.', ], BaselineReasonCodes::CAPTURE_ROLLOUT_DISABLED, BaselineReasonCodes::COMPARE_ROLLOUT_DISABLED => [ 'Full-content rollout disabled', 'This workflow is disabled by rollout configuration in the current environment.', 'prerequisite_missing', 'Enable the rollout before retrying full-content baseline work.', ], BaselineReasonCodes::SNAPSHOT_BUILDING, BaselineReasonCodes::COMPARE_SNAPSHOT_BUILDING => [ 'Baseline still building', 'The selected baseline snapshot is still building and cannot be trusted for compare yet.', 'prerequisite_missing', 'Wait for capture to finish or use the current complete snapshot instead.', ], BaselineReasonCodes::SNAPSHOT_INCOMPLETE, BaselineReasonCodes::COMPARE_SNAPSHOT_INCOMPLETE => [ 'Baseline snapshot incomplete', 'The snapshot did not finish cleanly, so TenantPilot will not use it for compare.', 'prerequisite_missing', 'Capture a new baseline and wait for it to complete before comparing.', ], BaselineReasonCodes::SNAPSHOT_SUPERSEDED, BaselineReasonCodes::COMPARE_SNAPSHOT_SUPERSEDED => [ 'Snapshot superseded', 'A newer complete baseline snapshot is current, so this historical snapshot is not compare input anymore.', 'prerequisite_missing', 'Use the current complete snapshot for compare instead of this historical copy.', ], BaselineReasonCodes::SNAPSHOT_CAPTURE_FAILED => [ 'Baseline capture failed', 'Snapshot capture stopped after the row was created, so the artifact remains unusable.', 'retryable_transient', 'Review the run details, then retry the capture once the failure is addressed.', ], BaselineReasonCodes::SNAPSHOT_COMPLETION_PROOF_FAILED => [ 'Completion proof failed', 'TenantPilot could not prove that every expected snapshot item was persisted successfully.', 'prerequisite_missing', 'Capture the baseline again so a complete snapshot can be finalized.', ], BaselineReasonCodes::SNAPSHOT_LEGACY_NO_PROOF => [ 'Legacy completion unproven', 'This older snapshot has no reliable completion proof, so it is blocked from compare.', 'prerequisite_missing', 'Recapture the baseline to create a complete snapshot with explicit lifecycle proof.', ], BaselineReasonCodes::SNAPSHOT_LEGACY_CONTRADICTORY => [ 'Legacy completion contradictory', 'Stored counts or producer-run evidence disagree, so TenantPilot treats this snapshot as incomplete.', 'prerequisite_missing', 'Recapture the baseline to replace this ambiguous historical snapshot.', ], BaselineReasonCodes::COMPARE_NO_ASSIGNMENT => [ 'No baseline assigned', 'This tenant has no assigned baseline profile yet.', 'prerequisite_missing', 'Assign a baseline profile to the tenant before starting compare.', ], BaselineReasonCodes::COMPARE_PROFILE_NOT_ACTIVE => [ 'Assigned baseline inactive', 'The assigned baseline profile is not active, so compare cannot start.', 'prerequisite_missing', 'Activate the assigned baseline profile or assign a different active profile.', ], BaselineReasonCodes::COMPARE_NO_ACTIVE_SNAPSHOT, BaselineReasonCodes::COMPARE_NO_CONSUMABLE_SNAPSHOT => [ 'Current baseline unavailable', 'No complete baseline snapshot is currently available for compare.', 'prerequisite_missing', 'Capture a baseline and wait for it to complete before comparing.', ], BaselineReasonCodes::COMPARE_NO_ELIGIBLE_TARGET => [ 'No eligible compare target', 'No assigned tenant with compare access is currently available for this baseline profile.', 'prerequisite_missing', 'Assign this baseline to a tenant you can compare, or use an account with access to an assigned tenant.', ], BaselineReasonCodes::COMPARE_INVALID_SNAPSHOT => [ 'Selected snapshot unavailable', 'The requested baseline snapshot could not be found for this profile.', 'prerequisite_missing', 'Refresh the page and select a valid snapshot for this baseline profile.', ], BaselineReasonCodes::COMPARE_MIXED_SCOPE => [ 'Mixed compare scope', 'The selected governed subjects span multiple compare strategy families, so TenantPilot will not start one misleading combined compare run.', 'prerequisite_missing', 'Narrow the governed subject selection so one compare strategy family owns the requested scope.', ], default => [ 'Baseline workflow blocked', 'TenantPilot recorded a baseline precondition that prevents this workflow from continuing safely.', 'prerequisite_missing', 'Review the recorded baseline state before retrying.', ], }; return new ReasonResolutionEnvelope( internalCode: $reasonCode, operatorLabel: $operatorLabel, shortExplanation: $shortExplanation, actionability: $actionability, nextSteps: [ NextStepOption::instruction($nextStep), ], diagnosticCodeLabel: $reasonCode, trustImpact: BaselineReasonCodes::trustImpact($reasonCode) ?? TrustworthinessLevel::Unusable->value, absencePattern: BaselineReasonCodes::absencePattern($reasonCode), ); } private function translateBaselineCompareReason(string $reasonCode): ReasonResolutionEnvelope { $enum = BaselineCompareReasonCode::tryFrom($reasonCode); if (! $enum instanceof BaselineCompareReasonCode) { return $this->fallbackReasonTranslator->translate($reasonCode) ?? new ReasonResolutionEnvelope( internalCode: $reasonCode, operatorLabel: 'Baseline compare needs review', shortExplanation: 'TenantPilot recorded a baseline-compare state that needs operator review.', actionability: 'permanent_configuration', ); } [$operatorLabel, $shortExplanation, $actionability, $nextStep] = match ($enum) { BaselineCompareReasonCode::NoDriftDetected => [ 'No drift detected', 'The comparison completed with enough coverage to treat the absence of drift findings as trustworthy.', 'non_actionable', 'No action needed unless you expected a newer compare result.', ], BaselineCompareReasonCode::CoverageUnproven => [ 'Coverage proof missing', 'The comparison finished, but missing coverage proof means some findings may have been suppressed for safety.', 'prerequisite_missing', 'Run inventory sync and compare again before treating this as complete.', ], BaselineCompareReasonCode::EvidenceCaptureIncomplete => [ 'Evidence capture incomplete', 'The comparison finished, but incomplete evidence capture limits how much confidence you should place in the visible result.', 'prerequisite_missing', 'Resume or rerun evidence capture before relying on this compare result.', ], BaselineCompareReasonCode::UnsupportedSubjects => [ 'Unsupported subjects remained', 'The comparison finished, but one or more in-scope subjects are not currently supported by the selected compare strategy.', 'prerequisite_missing', 'Narrow scope or wait for support before treating zero visible findings as complete.', ], BaselineCompareReasonCode::AmbiguousSubjects => [ 'Subject identity stayed ambiguous', 'The comparison finished, but one or more in-scope subjects could not be matched cleanly enough to produce a trustworthy result.', 'prerequisite_missing', 'Review the ambiguous subject mapping before relying on this compare result.', ], BaselineCompareReasonCode::StrategyFailed => [ 'Strategy processing failed', 'The comparison finished without a fully usable result because strategy-owned subject processing failed for one or more in-scope subjects.', 'retryable_transient', 'Inspect the compare run diagnostics and retry once the subject-processing failure is addressed.', ], BaselineCompareReasonCode::RolloutDisabled => [ 'Compare rollout disabled', 'The comparison path was limited by rollout configuration, so the result is not decision-grade.', 'prerequisite_missing', 'Enable the rollout or use the supported compare mode before retrying.', ], BaselineCompareReasonCode::NoSubjectsInScope => [ 'Nothing was eligible to compare', 'No in-scope subjects were available for evaluation, so the compare could not produce a normal result.', 'prerequisite_missing', 'Review scope selection and baseline inputs before comparing again.', ], BaselineCompareReasonCode::OverdueFindingsRemain => [ 'Overdue findings remain', 'The latest compare did not produce new drift, but overdue findings still require attention.', 'prerequisite_missing', 'Review and resolve the overdue findings before treating this posture as healthy.', ], BaselineCompareReasonCode::GovernanceExpiring => [ 'Accepted-risk governance is expiring', 'Accepted-risk coverage is still valid, but renewal is approaching and needs review.', 'prerequisite_missing', 'Review the expiring governance before it lapses.', ], BaselineCompareReasonCode::GovernanceLapsed => [ 'Accepted-risk governance lapsed', 'Accepted-risk coverage has lapsed, so the current posture still needs follow-up.', 'prerequisite_missing', 'Restore valid governance or move the affected findings back into active remediation.', ], }; return new ReasonResolutionEnvelope( internalCode: $reasonCode, operatorLabel: $operatorLabel, shortExplanation: $shortExplanation, actionability: $actionability, nextSteps: [ NextStepOption::instruction($nextStep), ], diagnosticCodeLabel: $reasonCode, trustImpact: $enum->trustworthinessLevel()->value, absencePattern: $enum->absencePattern(), ); } private function withOwnership( ?ReasonResolutionEnvelope $envelope, string $reasonCode, ?string $artifactKey, ): ?ReasonResolutionEnvelope { if (! $envelope instanceof ReasonResolutionEnvelope) { return null; } if ($envelope->reasonOwnership instanceof ReasonOwnershipDescriptor) { return $envelope; } $ownership = match (true) { $artifactKey === ProviderReasonTranslator::ARTIFACT_KEY, $artifactKey === null && ProviderReasonCodes::isKnown($reasonCode) => ProviderReasonCodes::ownershipDescriptor($reasonCode), $artifactKey === self::RBAC_ARTIFACT, $artifactKey === null && RbacReason::tryFrom($reasonCode) instanceof RbacReason => new ReasonOwnershipDescriptor( ownerLayer: 'domain_owned', ownerNamespace: 'rbac.intune', reasonCode: $reasonCode, platformReasonFamily: PlatformReasonFamily::Authorization, ), $artifactKey === self::TENANT_OPERABILITY_ARTIFACT, $artifactKey === null && TenantOperabilityReasonCode::tryFrom($reasonCode) instanceof TenantOperabilityReasonCode => new ReasonOwnershipDescriptor( ownerLayer: 'platform_core', ownerNamespace: 'tenant_operability', reasonCode: $reasonCode, platformReasonFamily: PlatformReasonFamily::Availability, ), $artifactKey === self::EXECUTION_DENIAL_ARTIFACT, $artifactKey === null && ExecutionDenialReasonCode::tryFrom($reasonCode) instanceof ExecutionDenialReasonCode => new ReasonOwnershipDescriptor( ownerLayer: 'platform_core', ownerNamespace: 'execution_denial', reasonCode: $reasonCode, platformReasonFamily: PlatformReasonFamily::Authorization, ), $artifactKey === null && LifecycleReconciliationReason::tryFrom($reasonCode) instanceof LifecycleReconciliationReason => new ReasonOwnershipDescriptor( ownerLayer: 'platform_core', ownerNamespace: 'operation_lifecycle', reasonCode: $reasonCode, platformReasonFamily: PlatformReasonFamily::Execution, ), $artifactKey === self::GOVERNANCE_ARTIFACT_TRUTH_ARTIFACT && BaselineCompareReasonCode::tryFrom($reasonCode) instanceof BaselineCompareReasonCode, $artifactKey === null && BaselineCompareReasonCode::tryFrom($reasonCode) instanceof BaselineCompareReasonCode => new ReasonOwnershipDescriptor( ownerLayer: 'domain_owned', ownerNamespace: 'governance.baseline_compare', reasonCode: $reasonCode, platformReasonFamily: $this->baselineCompareFamily($reasonCode), ), $artifactKey === self::GOVERNANCE_ARTIFACT_TRUTH_ARTIFACT && BaselineReasonCodes::isKnown($reasonCode), $artifactKey === null && BaselineReasonCodes::isKnown($reasonCode) => new ReasonOwnershipDescriptor( ownerLayer: 'domain_owned', ownerNamespace: 'governance.artifact_truth', reasonCode: $reasonCode, platformReasonFamily: $this->baselineReasonFamily($reasonCode), ), default => new ReasonOwnershipDescriptor( ownerLayer: 'platform_core', ownerNamespace: 'reason_translation.fallback', reasonCode: $reasonCode, platformReasonFamily: PlatformReasonFamily::Compatibility, ), }; return $envelope->withReasonOwnership($ownership); } private function baselineCompareFamily(string $reasonCode): PlatformReasonFamily { return match (BaselineCompareReasonCode::tryFrom($reasonCode)) { BaselineCompareReasonCode::CoverageUnproven, BaselineCompareReasonCode::EvidenceCaptureIncomplete, BaselineCompareReasonCode::UnsupportedSubjects, BaselineCompareReasonCode::AmbiguousSubjects, BaselineCompareReasonCode::NoSubjectsInScope, BaselineCompareReasonCode::NoDriftDetected => PlatformReasonFamily::Coverage, BaselineCompareReasonCode::StrategyFailed => PlatformReasonFamily::Execution, BaselineCompareReasonCode::RolloutDisabled => PlatformReasonFamily::Compatibility, BaselineCompareReasonCode::OverdueFindingsRemain, BaselineCompareReasonCode::GovernanceExpiring, BaselineCompareReasonCode::GovernanceLapsed => PlatformReasonFamily::Prerequisite, default => PlatformReasonFamily::Compatibility, }; } private function baselineReasonFamily(string $reasonCode): PlatformReasonFamily { return match ($reasonCode) { BaselineReasonCodes::COMPARE_MIXED_SCOPE, BaselineReasonCodes::CAPTURE_ROLLOUT_DISABLED, BaselineReasonCodes::COMPARE_ROLLOUT_DISABLED => PlatformReasonFamily::Compatibility, BaselineReasonCodes::SNAPSHOT_CAPTURE_FAILED => PlatformReasonFamily::Execution, default => PlatformReasonFamily::Prerequisite, }; } }