failing_stage = (string) ( $data['failing_stage'] ?? '' ); $this->reason = (string) ( $data['reason'] ?? '' ); $this->next_safe_action = (string) ( $data['next_safe_action'] ?? 'diagnostics' ); $this->retry_safe = (bool) ( $data['retry_safe'] ?? false ); $this->resume_safe = (bool) ( $data['resume_safe'] ?? false ); $this->partial_completion = isset( $data['partial_completion'] ) ? (array) $data['partial_completion'] : null; $this->restore_point_available = (bool) ( $data['restore_point_available'] ?? false ); } /** * Build recovery guidance from a normalized error payload and the migration record. * * @param array $error_details Output from ErrorReporter::normalize(). */ public static function from_error_and_record( array $error_details, MigrationRecord $record ): self { $code = (string) ( $error_details['code'] ?? 'unknown_error' ); $stage = (string) ( $record->current_stage ?? '' ); $record->completed_stages(); $completed = ! empty( $record->completed_stages_json ) ? ( json_decode( $record->completed_stages_json, true ) ?: [] ) : []; $guidance = self::guidance_for_code( $code, $stage, $completed, (int) ( $record->bytes_transferred ?? 0 ), (int) ( $record->transferred_files ?? 0 ) ); $guidance['failing_stage'] = $stage; $guidance['partial_completion'] = ! empty( $completed ) ? $completed : null; $guidance['restore_point_available'] = null !== $record->staged_tables_json; return new self( $guidance ); } /** * Map an error code to structured recovery guidance. * * @param array $completed Completed stages map. * * @return array{ reason: string, next_safe_action: string, retry_safe: bool, resume_safe: bool } */ private static function guidance_for_code( string $code, string $stage, array $completed, int $bytes_transferred = 0, int $transferred_files = 0 ): array { $stage_label = '' !== $stage ? ucfirst( str_replace( '_', ' ', $stage ) ) : 'Migration'; switch ( $code ) { case 'scheduler_unavailable': $has_file_stage = FileStages::has( $stage ); $has_transferred = $bytes_transferred > 0 || $transferred_files > 0; if ( $has_file_stage && $has_transferred ) { return [ 'reason' => sprintf( '%s paused because background workers are unavailable. Transferred files were preserved.', $stage_label ), 'next_safe_action' => 'resume', 'retry_safe' => false, 'resume_safe' => true, ]; } return [ 'reason' => sprintf( '%s could not continue because background workers are unavailable.', $stage_label ), 'next_safe_action' => 'retry', 'retry_safe' => true, 'resume_safe' => false, ]; case 'network_timeout': return [ 'reason' => sprintf( '%s stopped because the remote site took too long to respond.', $stage_label ), 'next_safe_action' => 'resume', 'retry_safe' => true, 'resume_safe' => true, ]; case 'manifest_failed': case 'file_stage_unavailable': return [ 'reason' => sprintf( '%s could not list the files to transfer. No files were sent.', $stage_label ), 'next_safe_action' => 'retry', 'retry_safe' => true, 'resume_safe' => false, ]; case 'file_transfer_failed': return [ 'reason' => sprintf( 'A file transfer error occurred during the %s stage. Transferred files so far are safe.', $stage_label ), 'next_safe_action' => 'resume', 'retry_safe' => true, 'resume_safe' => true, ]; case 'file_stage_race_detected': case 'file_stage_corrupt': return [ 'reason' => 'The file transfer state is inconsistent. Restarting the migration is recommended.', 'next_safe_action' => 'retry', 'retry_safe' => ! empty( $completed ), 'resume_safe' => false, ]; case 'db_import_failed': case 'local_backup_unavailable': return [ 'reason' => sprintf( 'The database stage could not complete%s.', 'db_import_failed' === $code ? '. File stages completed successfully' : '' ), 'next_safe_action' => 'retry', 'retry_safe' => true, 'resume_safe' => false, ]; case 'stage_not_completed': return [ 'reason' => sprintf( 'A planned stage was not completed. %d of %d configured stages finished.', count( $completed ), count( $completed ) + 1 ), 'next_safe_action' => 'diagnostics', 'retry_safe' => true, 'resume_safe' => false, ]; case 'connection_lost': case 'authentication_failed': return [ 'reason' => 'The connection to the remote site was lost or rejected.', 'next_safe_action' => 'reconnect', 'retry_safe' => false, 'resume_safe' => false, ]; case 'finalize_failed': return [ 'reason' => 'Data was transferred but the remote finalization step failed. The destination site may need manual review.', 'next_safe_action' => 'diagnostics', 'retry_safe' => true, 'resume_safe' => false, ]; case 'integrity_guard': return [ 'reason' => 'Shiftora stopped the migration to protect your data from corruption.', 'next_safe_action' => 'diagnostics', 'retry_safe' => false, 'resume_safe' => false, ]; case 'permission_denied': return [ 'reason' => sprintf( 'The %s stage was blocked by the destination server permissions.', $stage_label ), 'next_safe_action' => 'diagnostics', 'retry_safe' => false, 'resume_safe' => false, ]; case 'environment_problem': return [ 'reason' => 'The server environment is blocking the migration.', 'next_safe_action' => 'diagnostics', 'retry_safe' => false, 'resume_safe' => false, ]; case 'self_migration_protected': return [ 'reason' => 'Shiftora cannot migrate its own plugin files. Keep both sites on the same Shiftora version.', 'next_safe_action' => 'diagnostics', 'retry_safe' => true, 'resume_safe' => false, ]; case 'migration_stalled': return [ 'reason' => 'The migration appears stalled with no recent progress.', 'next_safe_action' => 'resume', 'retry_safe' => true, 'resume_safe' => true, ]; default: return [ 'reason' => sprintf( '%s encountered an unexpected error. Review the diagnostics for details.', $stage_label ), 'next_safe_action' => 'diagnostics', 'retry_safe' => empty( $completed ), 'resume_safe' => false, ]; } } }