$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; } return 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), }; } /** * @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.', ], 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 for the in-scope subjects without recording drift findings.', 'non_actionable', 'No action needed unless you expected findings.', ], 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::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.', ], }; return new ReasonResolutionEnvelope( internalCode: $reasonCode, operatorLabel: $operatorLabel, shortExplanation: $shortExplanation, actionability: $actionability, nextSteps: [ NextStepOption::instruction($nextStep), ], diagnosticCodeLabel: $reasonCode, trustImpact: $enum->trustworthinessLevel()->value, absencePattern: $enum->absencePattern(), ); } }