tasker-rb 0.1.3-arm64-darwin

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +55 -0
  3. data/lib/tasker_core/batch_processing/batch_aggregation_scenario.rb +85 -0
  4. data/lib/tasker_core/batch_processing/batch_worker_context.rb +238 -0
  5. data/lib/tasker_core/bootstrap.rb +394 -0
  6. data/lib/tasker_core/client.rb +165 -0
  7. data/lib/tasker_core/domain_events/base_publisher.rb +220 -0
  8. data/lib/tasker_core/domain_events/base_subscriber.rb +178 -0
  9. data/lib/tasker_core/domain_events/publisher_registry.rb +253 -0
  10. data/lib/tasker_core/domain_events/subscriber_registry.rb +152 -0
  11. data/lib/tasker_core/domain_events.rb +43 -0
  12. data/lib/tasker_core/errors/CLAUDE.md +7 -0
  13. data/lib/tasker_core/errors/common.rb +305 -0
  14. data/lib/tasker_core/errors/error_classifier.rb +61 -0
  15. data/lib/tasker_core/errors.rb +4 -0
  16. data/lib/tasker_core/event_bridge.rb +330 -0
  17. data/lib/tasker_core/handlers.rb +159 -0
  18. data/lib/tasker_core/internal.rb +31 -0
  19. data/lib/tasker_core/logger.rb +234 -0
  20. data/lib/tasker_core/models.rb +337 -0
  21. data/lib/tasker_core/observability/types.rb +158 -0
  22. data/lib/tasker_core/observability.rb +292 -0
  23. data/lib/tasker_core/registry/handler_registry.rb +453 -0
  24. data/lib/tasker_core/registry/resolver_chain.rb +258 -0
  25. data/lib/tasker_core/registry/resolvers/base_resolver.rb +90 -0
  26. data/lib/tasker_core/registry/resolvers/class_constant_resolver.rb +156 -0
  27. data/lib/tasker_core/registry/resolvers/explicit_mapping_resolver.rb +146 -0
  28. data/lib/tasker_core/registry/resolvers/method_dispatch_wrapper.rb +144 -0
  29. data/lib/tasker_core/registry/resolvers/registry_resolver.rb +229 -0
  30. data/lib/tasker_core/registry/resolvers.rb +42 -0
  31. data/lib/tasker_core/registry.rb +12 -0
  32. data/lib/tasker_core/step_handler/api.rb +48 -0
  33. data/lib/tasker_core/step_handler/base.rb +354 -0
  34. data/lib/tasker_core/step_handler/batchable.rb +50 -0
  35. data/lib/tasker_core/step_handler/decision.rb +53 -0
  36. data/lib/tasker_core/step_handler/mixins/api.rb +452 -0
  37. data/lib/tasker_core/step_handler/mixins/batchable.rb +465 -0
  38. data/lib/tasker_core/step_handler/mixins/decision.rb +252 -0
  39. data/lib/tasker_core/step_handler/mixins.rb +66 -0
  40. data/lib/tasker_core/subscriber.rb +212 -0
  41. data/lib/tasker_core/task_handler/base.rb +254 -0
  42. data/lib/tasker_core/tasker_rb.bundle +0 -0
  43. data/lib/tasker_core/template_discovery.rb +181 -0
  44. data/lib/tasker_core/test_environment.rb +313 -0
  45. data/lib/tasker_core/tracing.rb +166 -0
  46. data/lib/tasker_core/types/batch_processing_outcome.rb +301 -0
  47. data/lib/tasker_core/types/client_types.rb +145 -0
  48. data/lib/tasker_core/types/decision_point_outcome.rb +177 -0
  49. data/lib/tasker_core/types/error_types.rb +72 -0
  50. data/lib/tasker_core/types/simple_message.rb +151 -0
  51. data/lib/tasker_core/types/step_context.rb +328 -0
  52. data/lib/tasker_core/types/step_handler_call_result.rb +307 -0
  53. data/lib/tasker_core/types/step_message.rb +112 -0
  54. data/lib/tasker_core/types/step_types.rb +207 -0
  55. data/lib/tasker_core/types/task_template.rb +240 -0
  56. data/lib/tasker_core/types/task_types.rb +148 -0
  57. data/lib/tasker_core/types.rb +132 -0
  58. data/lib/tasker_core/version.rb +13 -0
  59. data/lib/tasker_core/worker/CLAUDE.md +7 -0
  60. data/lib/tasker_core/worker/event_poller.rb +224 -0
  61. data/lib/tasker_core/worker/in_process_domain_event_poller.rb +271 -0
  62. data/lib/tasker_core.rb +161 -0
  63. metadata +292 -0
@@ -0,0 +1,152 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'singleton'
4
+
5
+ module TaskerCore
6
+ module DomainEvents
7
+ # TAS-65: Registry for domain event subscribers
8
+ #
9
+ # Manages the lifecycle of domain event subscribers. Subscribers are registered
10
+ # at bootstrap time and started/stopped together with the worker.
11
+ #
12
+ # @example Registering subscribers at bootstrap
13
+ # registry = TaskerCore::DomainEvents::SubscriberRegistry.instance
14
+ #
15
+ # # Register subscriber classes (instantiation deferred)
16
+ # registry.register(PaymentEventSubscriber)
17
+ # registry.register(MetricsSubscriber)
18
+ #
19
+ # # Or register instances directly
20
+ # registry.register_instance(CustomSubscriber.new(some_config))
21
+ #
22
+ # # Start all subscribers
23
+ # registry.start_all!
24
+ #
25
+ # @example Stopping all subscribers
26
+ # registry.stop_all!
27
+ #
28
+ class SubscriberRegistry
29
+ include Singleton
30
+
31
+ attr_reader :logger, :subscribers
32
+
33
+ def initialize
34
+ @logger = TaskerCore::Logger.instance
35
+ @subscribers = []
36
+ @started = false
37
+ end
38
+
39
+ # Register a subscriber class
40
+ #
41
+ # The class will be instantiated when start_all! is called.
42
+ #
43
+ # @param subscriber_class [Class] A subclass of BaseSubscriber
44
+ # @return [void]
45
+ def register(subscriber_class)
46
+ unless subscriber_class < BaseSubscriber
47
+ raise ArgumentError, "#{subscriber_class} must be a subclass of BaseSubscriber"
48
+ end
49
+
50
+ logger.info "SubscriberRegistry: Registered #{subscriber_class.name}"
51
+ @subscriber_classes ||= []
52
+ @subscriber_classes << subscriber_class
53
+ end
54
+
55
+ # Register a subscriber instance directly
56
+ #
57
+ # Use this when your subscriber needs custom initialization.
58
+ #
59
+ # @param subscriber [BaseSubscriber] A subscriber instance
60
+ # @return [void]
61
+ def register_instance(subscriber)
62
+ raise ArgumentError, "Expected BaseSubscriber, got #{subscriber.class}" unless subscriber.is_a?(BaseSubscriber)
63
+
64
+ logger.info "SubscriberRegistry: Registered instance of #{subscriber.class.name}"
65
+ @subscribers << subscriber
66
+ end
67
+
68
+ # Start all registered subscribers
69
+ #
70
+ # Instantiates registered classes and starts all subscribers.
71
+ #
72
+ # @return [void]
73
+ def start_all!
74
+ return if @started
75
+
76
+ # Instantiate registered classes
77
+ (@subscriber_classes || []).each do |klass|
78
+ @subscribers << klass.new
79
+ end
80
+
81
+ # Start all subscribers
82
+ @subscribers.each do |subscriber|
83
+ subscriber.start!
84
+ rescue StandardError => e
85
+ logger.error "Failed to start #{subscriber.class.name}: #{e.message}"
86
+ end
87
+
88
+ @started = true
89
+ logger.info "SubscriberRegistry: Started #{@subscribers.size} subscribers"
90
+ end
91
+
92
+ # Stop all subscribers
93
+ #
94
+ # @return [void]
95
+ def stop_all!
96
+ return unless @started
97
+
98
+ @subscribers.each do |subscriber|
99
+ subscriber.stop!
100
+ rescue StandardError => e
101
+ logger.error "Failed to stop #{subscriber.class.name}: #{e.message}"
102
+ end
103
+
104
+ @started = false
105
+ logger.info 'SubscriberRegistry: Stopped all subscribers'
106
+ end
107
+
108
+ # Check if subscribers have been started
109
+ #
110
+ # @return [Boolean]
111
+ def started?
112
+ @started
113
+ end
114
+
115
+ # Get count of registered subscribers
116
+ #
117
+ # @return [Integer]
118
+ def count
119
+ @subscribers.size + (@subscriber_classes&.size || 0)
120
+ end
121
+
122
+ # Get subscriber statistics
123
+ #
124
+ # @return [Hash]
125
+ def stats
126
+ {
127
+ started: @started,
128
+ subscriber_count: @subscribers.size,
129
+ active_count: @subscribers.count(&:active?),
130
+ subscribers: @subscribers.map do |s|
131
+ {
132
+ class: s.class.name,
133
+ active: s.active?,
134
+ patterns: s.class.patterns
135
+ }
136
+ end
137
+ }
138
+ end
139
+
140
+ # Reset the registry (for testing)
141
+ #
142
+ # @note This stops all subscribers first
143
+ def reset!
144
+ stop_all! if @started
145
+ @subscribers.clear
146
+ @subscriber_classes&.clear
147
+ @started = false
148
+ logger.info 'SubscriberRegistry: Reset'
149
+ end
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ # TAS-65: Domain Events Module
4
+ #
5
+ # Provides infrastructure for custom domain event publishers.
6
+ # Domain events are business events (e.g., "order.processed", "payment.completed")
7
+ # published after step execution based on YAML declarations.
8
+ #
9
+ # @example Using the domain events infrastructure
10
+ # # Register custom publishers at bootstrap
11
+ # require 'tasker_core/domain_events'
12
+ #
13
+ # class PaymentEventPublisher < TaskerCore::DomainEvents::BasePublisher
14
+ # def name
15
+ # 'PaymentEventPublisher'
16
+ # end
17
+ #
18
+ # def transform_payload(step_result, event_declaration, step_context)
19
+ # {
20
+ # payment_id: step_result[:result][:payment_id],
21
+ # amount: step_result[:result][:amount],
22
+ # status: step_result[:success] ? 'succeeded' : 'failed'
23
+ # }
24
+ # end
25
+ # end
26
+ #
27
+ # TaskerCore::DomainEvents::PublisherRegistry.instance.register(
28
+ # PaymentEventPublisher.new
29
+ # )
30
+ #
31
+ module TaskerCore
32
+ module DomainEvents
33
+ # Load domain events components
34
+ end
35
+ end
36
+
37
+ # Publishers
38
+ require_relative 'domain_events/base_publisher'
39
+ require_relative 'domain_events/publisher_registry'
40
+
41
+ # Subscribers
42
+ require_relative 'domain_events/base_subscriber'
43
+ require_relative 'domain_events/subscriber_registry'
@@ -0,0 +1,7 @@
1
+ <claude-mem-context>
2
+ # Recent Activity
3
+
4
+ <!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
5
+
6
+ *No recent activity*
7
+ </claude-mem-context>
@@ -0,0 +1,305 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TaskerCore
4
+ module Errors
5
+ # Base error class for all TaskerCore-related errors
6
+ class Error < StandardError; end
7
+
8
+ # Raised when there are configuration-related issues in TaskerCore
9
+ class ConfigurationError < Error; end
10
+
11
+ # Base class for all TaskerCore-specific errors that occur during workflow execution
12
+ # Maps to the comprehensive error system in Rust orchestration/errors.rs
13
+ class ProceduralError < Error; end
14
+
15
+ # Raised when there are orchestration-related issues in TaskerCore
16
+ class OrchestrationError < Error; end
17
+
18
+ # Raised when there are database-related issues in TaskerCore
19
+ class DatabaseError < Error; end
20
+
21
+ # Error indicating a step failed but should be retried with backoff
22
+ # Maps to Rust StepExecutionError::Retryable
23
+ #
24
+ # Use this error when an operation fails due to temporary conditions like:
25
+ # - Network timeouts
26
+ # - Rate limiting (429 status)
27
+ # - Server errors (5xx status)
28
+ # - Temporary service unavailability
29
+ # - Resource exhaustion that may resolve
30
+ #
31
+ # @example Basic retryable error
32
+ # raise TaskerCore::Errors::RetryableError, "Payment service timeout"
33
+ #
34
+ # @example With retry delay
35
+ # raise TaskerCore::Errors::RetryableError.new("Rate limited", retry_after: 60)
36
+ #
37
+ # @example With context for monitoring
38
+ # raise TaskerCore::Errors::RetryableError.new(
39
+ # "External API unavailable",
40
+ # retry_after: 30,
41
+ # context: { service: 'billing_api', error_code: 503 }
42
+ # )
43
+ class RetryableError < ProceduralError
44
+ # @return [Integer, nil] Suggested retry delay in seconds
45
+ attr_reader :retry_after
46
+
47
+ # @return [Hash] Additional context for error monitoring and debugging
48
+ attr_reader :context
49
+
50
+ # @param message [String] Error message
51
+ # @param retry_after [Integer, nil] Suggested retry delay in seconds
52
+ # @param context [Hash] Additional context for monitoring
53
+ def initialize(message, retry_after: nil, context: {})
54
+ super(message)
55
+ @retry_after = retry_after
56
+ @context = context
57
+ end
58
+
59
+ # Get the error class name for Rust FFI compatibility
60
+ def error_class
61
+ 'RetryableError'
62
+ end
63
+ end
64
+
65
+ # Error indicating a step failed permanently and should not be retried
66
+ # Maps to Rust StepExecutionError::Permanent
67
+ #
68
+ # Use this error when an operation fails due to permanent conditions like:
69
+ # - Invalid request data (400 status)
70
+ # - Authentication/authorization failures (401/403 status)
71
+ # - Validation errors (422 status)
72
+ # - Resource not found when it should exist (404 status in some contexts)
73
+ # - Business logic violations
74
+ # - Configuration errors
75
+ #
76
+ # @example Basic permanent error
77
+ # raise TaskerCore::Errors::PermanentError, "Invalid user ID format"
78
+ #
79
+ # @example With error code for categorization
80
+ # raise TaskerCore::Errors::PermanentError.new(
81
+ # "Insufficient funds for transaction",
82
+ # error_code: 'INSUFFICIENT_FUNDS'
83
+ # )
84
+ #
85
+ # @example With context for monitoring
86
+ # raise TaskerCore::Errors::PermanentError.new(
87
+ # "User not authorized for this operation",
88
+ # error_code: 'AUTHORIZATION_FAILED',
89
+ # context: { user_id: 123, operation: 'admin_access' }
90
+ # )
91
+ class PermanentError < ProceduralError
92
+ # @return [String, nil] Machine-readable error code for categorization
93
+ attr_reader :error_code
94
+
95
+ # @return [Hash] Additional context for error monitoring and debugging
96
+ attr_reader :context
97
+
98
+ # @param message [String] Error message
99
+ # @param error_code [String, nil] Machine-readable error code
100
+ # @param context [Hash] Additional context for monitoring
101
+ def initialize(message, error_code: nil, context: {})
102
+ super(message)
103
+ @error_code = error_code
104
+ @context = context
105
+ end
106
+
107
+ # Get the error class name for Rust FFI compatibility
108
+ def error_class
109
+ 'PermanentError'
110
+ end
111
+ end
112
+
113
+ # Error indicating a timeout occurred during step execution
114
+ # Maps to Rust StepExecutionError::Timeout
115
+ #
116
+ # Use this error when an operation fails due to timeout conditions:
117
+ # - Step execution timeout
118
+ # - Network request timeout
119
+ # - Database operation timeout
120
+ # - External service timeout
121
+ #
122
+ # @example Basic timeout error
123
+ # raise TaskerCore::Errors::TimeoutError, "Payment processing timed out"
124
+ #
125
+ # @example With timeout duration
126
+ # raise TaskerCore::Errors::TimeoutError.new(
127
+ # "Database query timed out",
128
+ # timeout_duration: 30
129
+ # )
130
+ class TimeoutError < ProceduralError
131
+ # @return [Integer, nil] Timeout duration in seconds
132
+ attr_reader :timeout_duration
133
+
134
+ # @return [Hash] Additional context for error monitoring and debugging
135
+ attr_reader :context
136
+
137
+ # @param message [String] Error message
138
+ # @param timeout_duration [Integer, nil] Timeout duration in seconds
139
+ # @param context [Hash] Additional context for monitoring
140
+ def initialize(message, timeout_duration: nil, context: {})
141
+ super(message)
142
+ @timeout_duration = timeout_duration
143
+ @context = context
144
+ end
145
+
146
+ # Get the error class name for Rust FFI compatibility
147
+ def error_class
148
+ 'TimeoutError'
149
+ end
150
+ end
151
+
152
+ # Error indicating a network-related failure
153
+ # Maps to Rust StepExecutionError::NetworkError
154
+ #
155
+ # Use this error when an operation fails due to network conditions:
156
+ # - Connection failures
157
+ # - DNS resolution errors
158
+ # - HTTP errors (4xx/5xx when network-related)
159
+ # - TLS/SSL errors
160
+ # - Network timeouts
161
+ #
162
+ # @example Basic network error
163
+ # raise TaskerCore::Errors::NetworkError, "Connection refused"
164
+ #
165
+ # @example With HTTP status code
166
+ # raise TaskerCore::Errors::NetworkError.new(
167
+ # "Service unavailable",
168
+ # status_code: 503
169
+ # )
170
+ class NetworkError < ProceduralError
171
+ # @return [Integer, nil] HTTP status code if applicable
172
+ attr_reader :status_code
173
+
174
+ # @return [Hash] Additional context for error monitoring and debugging
175
+ attr_reader :context
176
+
177
+ # @param message [String] Error message
178
+ # @param status_code [Integer, nil] HTTP status code if applicable
179
+ # @param context [Hash] Additional context for monitoring
180
+ def initialize(message, status_code: nil, context: {})
181
+ super(message)
182
+ @status_code = status_code
183
+ @context = context
184
+ end
185
+
186
+ # Get the error class name for Rust FFI compatibility
187
+ def error_class
188
+ 'NetworkError'
189
+ end
190
+ end
191
+
192
+ # Error indicating validation failed
193
+ # Maps to Rust OrchestrationError::ValidationError
194
+ #
195
+ # Use this error when data validation fails:
196
+ # - Schema validation errors
197
+ # - Business rule validation
198
+ # - Input format validation
199
+ # - Required field validation
200
+ class ValidationError < PermanentError
201
+ # @return [String, nil] Field that failed validation
202
+ attr_reader :field
203
+
204
+ # @param message [String] Error message
205
+ # @param field [String, nil] Field that failed validation
206
+ # @param error_code [String, nil] Machine-readable error code
207
+ # @param context [Hash] Additional context for monitoring
208
+ def initialize(message, field: nil, error_code: nil, context: {})
209
+ super(message, error_code: error_code, context: context)
210
+ @field = field
211
+ end
212
+
213
+ # Get the error class name for Rust FFI compatibility
214
+ def error_class
215
+ 'ValidationError'
216
+ end
217
+ end
218
+
219
+ # Error indicating a handler or step was not found
220
+ # Maps to Rust OrchestrationError::HandlerNotFound and StepHandlerNotFound
221
+ class NotFoundError < PermanentError
222
+ # @return [String, nil] Type of resource not found (handler, step, etc.)
223
+ attr_reader :resource_type
224
+
225
+ # @param message [String] Error message
226
+ # @param resource_type [String, nil] Type of resource not found
227
+ # @param error_code [String, nil] Machine-readable error code
228
+ # @param context [Hash] Additional context for monitoring
229
+ def initialize(message, resource_type: nil, error_code: nil, context: {})
230
+ super(message, error_code: error_code, context: context)
231
+ @resource_type = resource_type
232
+ end
233
+
234
+ # Get the error class name for Rust FFI compatibility
235
+ def error_class
236
+ 'NotFoundError'
237
+ end
238
+ end
239
+
240
+ # Error indicating an FFI operation failed
241
+ # Maps to Rust OrchestrationError::FfiBridgeError
242
+ class FFIError < Error
243
+ # @return [String, nil] FFI operation that failed
244
+ attr_reader :operation
245
+
246
+ # @return [Hash] Additional context for error monitoring and debugging
247
+ attr_reader :context
248
+
249
+ # @param message [String] Error message
250
+ # @param operation [String, nil] FFI operation that failed
251
+ # @param context [Hash] Additional context for monitoring
252
+ def initialize(message, operation: nil, context: {})
253
+ super(message)
254
+ @operation = operation
255
+ @context = context
256
+ end
257
+
258
+ # Get the error class name for Rust FFI compatibility
259
+ def error_class
260
+ 'FFIError'
261
+ end
262
+ end
263
+
264
+ # Error indicating embedded server operation failed
265
+ # Maps to Rust ServerError types
266
+ #
267
+ # Use this error when embedded server operations fail:
268
+ # - Server startup failures
269
+ # - Server shutdown failures
270
+ # - Configuration errors
271
+ # - Runtime errors
272
+ # - Server already running/not running
273
+ #
274
+ # @example Basic server error
275
+ # raise TaskerCore::Errors::ServerError, "Failed to start embedded server"
276
+ #
277
+ # @example With server operation context
278
+ # raise TaskerCore::Errors::ServerError.new(
279
+ # "Server startup failed",
280
+ # operation: 'start',
281
+ # context: { bind_address: '127.0.0.1:8080', error_code: 'ADDRESS_IN_USE' }
282
+ # )
283
+ class ServerError < Error
284
+ # @return [String, nil] Server operation that failed
285
+ attr_reader :operation
286
+
287
+ # @return [Hash] Additional context for error monitoring and debugging
288
+ attr_reader :context
289
+
290
+ # @param message [String] Error message
291
+ # @param operation [String, nil] Server operation that failed (start, stop, config, etc.)
292
+ # @param context [Hash] Additional context for monitoring
293
+ def initialize(message, operation: nil, context: {})
294
+ super(message)
295
+ @operation = operation
296
+ @context = context
297
+ end
298
+
299
+ # Get the error class name for Rust FFI compatibility
300
+ def error_class
301
+ 'ServerError'
302
+ end
303
+ end
304
+ end
305
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'common'
4
+
5
+ module TaskerCore
6
+ module Errors
7
+ # ErrorClassifier provides systematic classification of errors for retry logic
8
+ #
9
+ # This classifier determines whether errors should be retried based on their type.
10
+ # It maintains explicit lists of permanent (non-retryable) and retryable error classes.
11
+ #
12
+ # @example Classify a configuration error
13
+ # ErrorClassifier.retryable?(ConfigurationError.new("Missing handler"))
14
+ # # => false
15
+ #
16
+ # @example Classify a network error
17
+ # ErrorClassifier.retryable?(NetworkError.new("Connection timeout"))
18
+ # # => true
19
+ class ErrorClassifier
20
+ # System-level errors that should NEVER be retried
21
+ # These represent fundamental configuration or setup issues that won't resolve on retry
22
+ PERMANENT_ERROR_CLASSES = [
23
+ ConfigurationError, # Configuration issues (missing config, invalid settings)
24
+ PermanentError, # Explicitly marked permanent errors
25
+ ValidationError, # Data validation failures
26
+ NotFoundError, # Resource not found (handler, step, etc.)
27
+ DatabaseError, # Database schema/constraint errors
28
+ FFIError # FFI bridge failures
29
+ ].freeze
30
+
31
+ # Application errors that are retryable by default
32
+ # These represent transient failures that may resolve on subsequent attempts
33
+ RETRYABLE_ERROR_CLASSES = [
34
+ RetryableError, # Explicitly marked retryable errors
35
+ NetworkError, # Network/connection failures
36
+ TimeoutError # Timeout errors
37
+ ].freeze
38
+
39
+ # Determine if an error should be retried
40
+ #
41
+ # @param error [StandardError] The error to classify
42
+ # @return [Boolean] true if the error should be retried, false otherwise
43
+ #
44
+ # @note The default behavior is to mark errors as retryable unless they are
45
+ # explicitly in the PERMANENT_ERROR_CLASSES list. This follows the principle
46
+ # that it's safer to retry an error that shouldn't be retried (will eventually
47
+ # hit retry limit) than to not retry an error that should be (will fail immediately).
48
+ def self.retryable?(error)
49
+ # Explicit permanent errors - never retry
50
+ return false if PERMANENT_ERROR_CLASSES.any? { |klass| error.is_a?(klass) }
51
+
52
+ # Explicit retryable errors - always retry
53
+ return true if RETRYABLE_ERROR_CLASSES.any? { |klass| error.is_a?(klass) }
54
+
55
+ # Default: StandardError and subclasses are retryable unless explicitly marked permanent
56
+ # This is a safe default - worst case, we retry until max_attempts is hit
57
+ true
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'errors/common'
4
+ require_relative 'errors/error_classifier'