$context */ public function translate(string $reasonCode, string $surface = 'detail', array $context = []): ?ReasonResolutionEnvelope { $normalizedCode = trim($reasonCode); if ($normalizedCode === '') { return null; } $actionability = $this->actionabilityFor($normalizedCode); $nextSteps = $this->fallbackNextStepsFor($actionability); return new ReasonResolutionEnvelope( internalCode: $normalizedCode, operatorLabel: $this->operatorLabelFor($normalizedCode), shortExplanation: $this->shortExplanationFor($actionability), actionability: $actionability, nextSteps: $nextSteps, showNoActionNeeded: $actionability === 'non_actionable', diagnosticCodeLabel: $normalizedCode, trustImpact: $this->trustImpactFor($actionability), absencePattern: $this->absencePatternFor($normalizedCode, $actionability), ); } private function operatorLabelFor(string $reasonCode): string { return Str::headline(str_replace(['.', '-'], '_', $reasonCode)); } private function actionabilityFor(string $reasonCode): string { $reasonCode = strtolower($reasonCode); if (str_contains($reasonCode, 'timeout') || str_contains($reasonCode, 'throttle') || str_contains($reasonCode, 'rate') || str_contains($reasonCode, 'network') || str_contains($reasonCode, 'unreachable') || str_contains($reasonCode, 'transient') || str_contains($reasonCode, 'retry') ) { return 'retryable_transient'; } if (str_contains($reasonCode, 'missing') || str_contains($reasonCode, 'required') || str_contains($reasonCode, 'consent') || str_contains($reasonCode, 'stale') || str_contains($reasonCode, 'prerequisite') || str_contains($reasonCode, 'invalid') ) { return 'prerequisite_missing'; } if (str_contains($reasonCode, 'already_') || str_contains($reasonCode, 'not_applicable') || str_contains($reasonCode, 'no_action') || str_contains($reasonCode, 'info') ) { return 'non_actionable'; } return 'permanent_configuration'; } private function shortExplanationFor(string $actionability): string { return match ($actionability) { 'retryable_transient' => 'TenantPilot recorded a transient dependency issue. Retry after the dependency recovers.', 'prerequisite_missing' => 'TenantPilot recorded a missing or invalid prerequisite for this workflow.', 'non_actionable' => 'TenantPilot recorded this state for visibility only. No operator action is required.', default => 'TenantPilot recorded an access, scope, or configuration issue that needs review before retrying.', }; } /** * @return array */ private function fallbackNextStepsFor(string $actionability): array { return match ($actionability) { 'retryable_transient' => [NextStepOption::instruction('Retry after the dependency recovers.')], 'prerequisite_missing' => [NextStepOption::instruction('Review the recorded prerequisite before retrying.')], 'non_actionable' => [], default => [NextStepOption::instruction('Review access and configuration before retrying.')], }; } private function trustImpactFor(string $actionability): string { return match ($actionability) { 'non_actionable' => TrustworthinessLevel::Trustworthy->value, 'retryable_transient' => TrustworthinessLevel::LimitedConfidence->value, default => TrustworthinessLevel::Unusable->value, }; } private function absencePatternFor(string $reasonCode, string $actionability): ?string { $normalizedCode = strtolower($reasonCode); if (str_contains($normalizedCode, 'suppressed')) { return 'suppressed_output'; } if (str_contains($normalizedCode, 'missing') || str_contains($normalizedCode, 'stale')) { return 'missing_input'; } if ($actionability === 'prerequisite_missing') { return 'blocked_prerequisite'; } if ($actionability === 'non_actionable') { return 'true_no_result'; } return 'unavailable'; } }