tasker-rb 0.1.3-x86_64-linux

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.so +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,234 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'singleton'
4
+ require 'json'
5
+
6
+ module TaskerCore
7
+ # DEPRECATED: Legacy logger wrapper that delegates to TaskerCore::Tracing
8
+ #
9
+ # This class is maintained for backward compatibility but all new code should
10
+ # use TaskerCore::Tracing directly for unified structured logging via FFI.
11
+ #
12
+ # TAS-29 Phase 6: This logger now delegates to the Rust tracing infrastructure
13
+ # through the FFI bridge, providing unified structured logging across Ruby and Rust.
14
+ #
15
+ # Migration Guide:
16
+ # # Old:
17
+ # logger = TaskerCore::Logger.instance
18
+ # logger.info("Message")
19
+ #
20
+ # # New:
21
+ # TaskerCore::Tracing.info("Message", { operation: "example" })
22
+ #
23
+ # The logger supports two logging approaches:
24
+ # 1. **Traditional Logging**: Simple string messages (delegates to Tracing)
25
+ # 2. **Structured Logging**: Component-based logging (converts to Tracing fields)
26
+ #
27
+ # Structured logs include:
28
+ # - Emoji prefix for visual identification
29
+ # - Component label for categorization
30
+ # - Operation description
31
+ # - Structured metadata as JSON
32
+ # - ISO 8601 timestamps
33
+ #
34
+ # @example Traditional logging (backward compatibility)
35
+ # logger = TaskerCore::Logger.instance
36
+ # logger.info "Worker started successfully"
37
+ # logger.error "Failed to process step: #{error.message}"
38
+ # logger.debug "Processing step #{step_uuid}"
39
+ #
40
+ # @example Structured logging with component context
41
+ # logger.log_task(:info, "initialization",
42
+ # task_uuid: "550e8400-e29b-41d4-a716-446655440000",
43
+ # namespace: "payments",
44
+ # step_count: 4,
45
+ # priority: "high"
46
+ # )
47
+ # # Output: [2025-10-02 12:00:00] INFO TaskerCore: 📋 TASK_OPERATION: initialization | {"task_uuid":"...","namespace":"payments",...}
48
+ #
49
+ # @example Queue worker logging
50
+ # logger.log_queue_worker(:debug, "claiming_step",
51
+ # namespace: "payments",
52
+ # step_uuid: "7c9e6679-7425-40de-944b-e07fc1f90ae7",
53
+ # retry_count: 2,
54
+ # queue_depth: 15
55
+ # )
56
+ # # Output: [2025-10-02 12:00:01] DEBUG TaskerCore: 🔄 QUEUE_WORKER: claiming_step (namespace: payments) | {...}
57
+ #
58
+ # @example Step execution logging
59
+ # logger.log_step(:info, "handler_execution",
60
+ # step_uuid: "7c9e6679-7425-40de-944b-e07fc1f90ae7",
61
+ # handler_class: "ProcessPaymentHandler",
62
+ # execution_time_ms: 125,
63
+ # result_size_bytes: 1024
64
+ # )
65
+ #
66
+ # @example FFI operation logging
67
+ # logger.log_ffi(:info, "bootstrap_worker",
68
+ # component: "RubyWorker",
69
+ # worker_id: "ruby-worker-123",
70
+ # deployment_mode: "Hybrid"
71
+ # )
72
+ #
73
+ # @example Database operation logging
74
+ # logger.log_database(:debug, "step_transition",
75
+ # step_uuid: "7c9e6679-7425-40de-944b-e07fc1f90ae7",
76
+ # from_state: "in_progress",
77
+ # to_state: "complete",
78
+ # duration_ms: 15
79
+ # )
80
+ #
81
+ # @example Configuration logging
82
+ # logger.log_config(:info, "environment_loaded",
83
+ # environment: "production",
84
+ # config_file: "/etc/tasker/config.yml",
85
+ # overrides_applied: 3
86
+ # )
87
+ #
88
+ # @example Handler registry logging
89
+ # logger.log_registry(:debug, "handler_registered",
90
+ # namespace: "payments",
91
+ # name: "ProcessPaymentHandler",
92
+ # handler_count: 12
93
+ # )
94
+ #
95
+ # Component Types and Their Emojis:
96
+ # - **📋 TASK_OPERATION**: Task-level operations (creation, completion, state changes)
97
+ # - **🔄 QUEUE_WORKER**: Queue processing and step claiming
98
+ # - **🚀 ORCHESTRATOR**: Orchestration coordination and workflow management
99
+ # - **🔧 STEP_OPERATION**: Step execution and handler invocation
100
+ # - **💾 DATABASE**: Database operations and state persistence
101
+ # - **🌉 FFI**: Rust FFI bridge operations and cross-language communication
102
+ # - **⚙️ CONFIG**: Configuration loading and validation
103
+ # - **📚 REGISTRY**: Handler registry operations and discovery
104
+ #
105
+ # Log Levels:
106
+ # - **:debug**: Detailed diagnostic information
107
+ # - **:info**: General informational messages
108
+ # - **:warn**: Warning messages for potential issues
109
+ # - **:error**: Error messages for failures
110
+ # - **:fatal**: Critical failures requiring immediate attention
111
+ #
112
+ # @see #log_task For task-level operations
113
+ # @see #log_queue_worker For queue processing
114
+ # @see #log_orchestrator For orchestration operations
115
+ # @see #log_step For step execution
116
+ # @see #log_database For database operations
117
+ # @see #log_ffi For FFI bridge operations
118
+ # @see #log_config For configuration operations
119
+ # @see #log_registry For handler registry operations
120
+ class Logger
121
+ include Singleton
122
+
123
+ # Traditional string-based logging methods (delegates to Tracing)
124
+ def info(message, &)
125
+ Tracing.info(message.to_s, extract_context_fields)
126
+ end
127
+
128
+ def warn(message, &)
129
+ Tracing.warn(message.to_s, extract_context_fields)
130
+ end
131
+
132
+ def error(message, &)
133
+ Tracing.error(message.to_s, extract_context_fields)
134
+ end
135
+
136
+ def fatal(message, &)
137
+ Tracing.error(message.to_s, extract_context_fields.merge(severity: 'fatal'))
138
+ end
139
+
140
+ def debug(message, &)
141
+ Tracing.debug(message.to_s, extract_context_fields)
142
+ end
143
+
144
+ # Enhanced structured logging methods (delegates to Tracing with appropriate fields)
145
+
146
+ def log_task(level, operation, **data)
147
+ fields = extract_context_fields.merge(
148
+ operation: operation,
149
+ component: 'task_operation'
150
+ ).merge(data)
151
+ Tracing.send(level, operation, fields)
152
+ end
153
+
154
+ def log_queue_worker(level, operation, namespace: nil, **data)
155
+ fields = extract_context_fields.merge(
156
+ operation: operation,
157
+ component: 'queue_worker'
158
+ ).merge(data)
159
+ fields[:namespace] = namespace if namespace
160
+ Tracing.send(level, operation, fields)
161
+ end
162
+
163
+ def log_orchestrator(level, operation, **data)
164
+ fields = extract_context_fields.merge(
165
+ operation: operation,
166
+ component: 'orchestrator'
167
+ ).merge(data)
168
+ Tracing.send(level, operation, fields)
169
+ end
170
+
171
+ def log_step(level, operation, **data)
172
+ fields = extract_context_fields.merge(
173
+ operation: operation,
174
+ component: 'step_operation'
175
+ ).merge(data)
176
+ Tracing.send(level, operation, fields)
177
+ end
178
+
179
+ def log_database(level, operation, **data)
180
+ fields = extract_context_fields.merge(
181
+ operation: operation,
182
+ component: 'database'
183
+ ).merge(data)
184
+ Tracing.send(level, operation, fields)
185
+ end
186
+
187
+ def log_ffi(level, operation, component: nil, **data)
188
+ fields = extract_context_fields.merge(
189
+ operation: operation,
190
+ component: component || 'ffi'
191
+ ).merge(data)
192
+ Tracing.send(level, operation, fields)
193
+ end
194
+
195
+ def log_config(level, operation, **data)
196
+ fields = extract_context_fields.merge(
197
+ operation: operation,
198
+ component: 'config'
199
+ ).merge(data)
200
+ Tracing.send(level, operation, fields)
201
+ end
202
+
203
+ def log_registry(level, operation, namespace: nil, name: nil, **data)
204
+ fields = extract_context_fields.merge(
205
+ operation: operation,
206
+ component: 'registry'
207
+ ).merge(data)
208
+ fields[:namespace] = namespace if namespace
209
+ fields[:handler_name] = name if name
210
+ Tracing.send(level, operation, fields)
211
+ end
212
+
213
+ def initialize
214
+ # No-op - everything delegates to Tracing now
215
+ end
216
+
217
+ private
218
+
219
+ # Extract context fields from thread-local or instance variables if available
220
+ def extract_context_fields
221
+ fields = {}
222
+
223
+ # Try to extract correlation_id from thread-local storage if available
224
+ fields[:correlation_id] = Thread.current[:correlation_id] if Thread.current[:correlation_id]
225
+
226
+ # Try to extract task/step UUIDs if available
227
+ fields[:task_uuid] = Thread.current[:task_uuid] if Thread.current[:task_uuid]
228
+
229
+ fields[:step_uuid] = Thread.current[:step_uuid] if Thread.current[:step_uuid]
230
+
231
+ fields
232
+ end
233
+ end
234
+ end
@@ -0,0 +1,337 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TaskerCore
4
+ module Models
5
+ # Wrapper for TaskSequenceStep data received from Rust FFI.
6
+ #
7
+ # This class provides a Ruby-friendly interface to the complete step execution context,
8
+ # including task metadata, workflow step state, dependency results, and step definition.
9
+ # The data structure is provided by the Rust orchestration system via FFI when a step
10
+ # is ready for execution.
11
+ #
12
+ # @example Accessing task context in a handler
13
+ # def call(context)
14
+ # even_number = context.task.context['even_number']
15
+ # # or use the convenience method
16
+ # even_number = context.get_task_field('even_number')
17
+ # end
18
+ #
19
+ # @example Accessing dependency results
20
+ # def call(context)
21
+ # previous_result = context.get_dependency_result('previous_step_name')
22
+ # end
23
+ class TaskSequenceStepWrapper
24
+ # @return [TaskWrapper] Wrapped task with context and metadata
25
+ # @return [WorkflowStepWrapper] Wrapped workflow step with execution state
26
+ # @return [DependencyResultsWrapper] Results from parent steps
27
+ # @return [StepDefinitionWrapper] Step definition from task template
28
+ attr_reader :task, :workflow_step, :dependency_results, :step_definition
29
+
30
+ # Creates a new TaskSequenceStepWrapper from FFI data
31
+ #
32
+ # @param step_data [Hash] The complete step execution data from Rust
33
+ # @option step_data [Hash] :task Task and namespace metadata
34
+ # @option step_data [Hash] :workflow_step Step execution state and results
35
+ # @option step_data [Hash] :dependency_results Results from parent steps
36
+ # @option step_data [Hash] :step_definition Step configuration from template
37
+ def initialize(step_data)
38
+ @task = TaskWrapper.new(step_data[:task])
39
+ @workflow_step = WorkflowStepWrapper.new(step_data[:workflow_step])
40
+ @dependency_results = DependencyResultsWrapper.new(step_data[:dependency_results])
41
+ @step_definition = StepDefinitionWrapper.new(step_data[:step_definition])
42
+ end
43
+
44
+ # Convenience method to access task context fields
45
+ #
46
+ # @param field_name [String, Symbol] The field name in task context
47
+ # @return [Object, nil] The field value or nil if not found
48
+ #
49
+ # @example
50
+ # sequence.get_task_field('even_number') # => 2
51
+ def get_task_field(field_name)
52
+ @task.context[field_name.to_s]
53
+ end
54
+
55
+ # Convenience method to access dependency results
56
+ #
57
+ # @param step_name [String, Symbol] The name of the parent step
58
+ # @return [Object, nil] The result from the parent step or nil
59
+ #
60
+ # @example
61
+ # result = sequence.get_dependency_result('linear_step_1') # => 36
62
+ def get_dependency_result(step_name)
63
+ @dependency_results.get_result(step_name)
64
+ end
65
+
66
+ # Convenience method to access dependency results (alias for compatibility)
67
+ #
68
+ # @param step_name [String, Symbol] The name of the parent step
69
+ # @return [Object, nil] The result from the parent step or nil
70
+ #
71
+ # @example
72
+ # result = sequence.get_results('linear_step_1') # => 36
73
+ def get_results(step_name)
74
+ @dependency_results.get_results(step_name)
75
+ end
76
+ end
77
+
78
+ # Wrapper for task metadata and context.
79
+ #
80
+ # Handles the nested task structure from Rust FFI where the actual task data
81
+ # is nested under a :task key within the parent task hash.
82
+ #
83
+ # @example Accessing task properties
84
+ # task.task_uuid # => "0199a46a-11a8-7d53-83da-0b13513dab49"
85
+ # task.context # => { "even_number" => 2 }
86
+ # task.namespace_name # => "linear_workflow"
87
+ class TaskWrapper
88
+ # @return [String] UUID v7 of the task instance
89
+ # @return [ActiveSupport::HashWithIndifferentAccess] Task context with input data and accumulated state
90
+ # @return [String] Namespace name from task template
91
+ # @return [String] Task template name
92
+ # @return [String] Task template version
93
+ attr_reader :task_uuid, :context, :namespace_name, :task_name, :task_version
94
+
95
+ # Creates a new TaskWrapper from FFI data
96
+ #
97
+ # @param task_data [Hash] Task data with nested structure from Rust
98
+ # @option task_data [Hash] :task Nested task object with context
99
+ # @option task_data [String] :task_name Template name
100
+ # @option task_data [String] :namespace_name Namespace from template
101
+ # @option task_data [String] :task_version Template version
102
+ def initialize(task_data)
103
+ # Handle nested task structure from Rust FFI
104
+ # Data comes as: { task: { task_uuid: ..., context: ... }, task_name: "...", namespace_name: "..." }
105
+ # The actual task fields are nested under :task key
106
+ inner_task = task_data[:task] || task_data
107
+
108
+ @task_uuid = inner_task[:task_uuid]
109
+ # Use HashWithIndifferentAccess for developer-friendly context access
110
+ @context = (inner_task[:context] || {}).with_indifferent_access
111
+ @namespace_name = task_data[:namespace_name] || inner_task[:namespace_name]
112
+ @task_name = task_data[:task_name] || inner_task[:task_name]
113
+ @task_version = task_data[:task_version] || inner_task[:task_version]
114
+ end
115
+ end
116
+
117
+ # Wrapper for workflow step execution state and metadata.
118
+ #
119
+ # Provides access to step execution tracking, retry configuration, and results.
120
+ #
121
+ # @example Checking step state
122
+ # step.name # => "linear_step_1"
123
+ # step.attempts # => 1
124
+ # step.max_attempts # => 3
125
+ # step.in_process # => false
126
+ class WorkflowStepWrapper
127
+ # @return [String] UUID v7 of the workflow step instance
128
+ # @return [String] UUID v7 of the task this step belongs to
129
+ # @return [String] UUID v7 of the named step definition
130
+ # @return [String] Step name from template
131
+ # @return [Boolean] Whether step can be retried
132
+ # @return [Integer] Maximum retry attempts
133
+ # @return [Boolean] Whether step is currently being processed
134
+ # @return [Boolean] Whether step has been processed
135
+ # @return [Time, nil] When step was last processed
136
+ # @return [Integer] Number of execution attempts
137
+ # @return [Time, nil] When step was last attempted
138
+ # @return [Integer] Backoff delay in seconds for retry
139
+ # @return [ActiveSupport::HashWithIndifferentAccess] Step inputs from template
140
+ # @return [ActiveSupport::HashWithIndifferentAccess] Step execution results
141
+
142
+ # @return [Time] When step was created
143
+ # @return [Time] When step was last updated
144
+ # @return [ActiveSupport::HashWithIndifferentAccess, nil] TAS-125: Checkpoint data for batch processing resumption
145
+ attr_reader :workflow_step_uuid, :task_uuid, :named_step_uuid, :name,
146
+ :retryable, :max_attempts, :in_process, :processed, :processed_at,
147
+ :attempts, :last_attempted_at, :backoff_request_seconds,
148
+ :inputs, :results, :created_at, :updated_at, :checkpoint
149
+
150
+ # Creates a new WorkflowStepWrapper from FFI data
151
+ #
152
+ # @param step_data [Hash] Workflow step data from Rust
153
+ def initialize(step_data)
154
+ @workflow_step_uuid = step_data[:workflow_step_uuid]
155
+ @task_uuid = step_data[:task_uuid]
156
+ @named_step_uuid = step_data[:named_step_uuid]
157
+ @name = step_data[:name]
158
+ @retryable = step_data[:retryable]
159
+ @max_attempts = step_data[:max_attempts]
160
+ @in_process = step_data[:in_process]
161
+ @processed = step_data[:processed]
162
+ @processed_at = step_data[:processed_at]
163
+ @attempts = step_data[:attempts]
164
+ @last_attempted_at = step_data[:last_attempted_at]
165
+ @backoff_request_seconds = step_data[:backoff_request_seconds]
166
+ # Use HashWithIndifferentAccess for nested hashes
167
+ @inputs = (step_data[:inputs] || {}).with_indifferent_access
168
+ @results = (step_data[:results] || {}).with_indifferent_access
169
+
170
+ @created_at = step_data[:created_at]
171
+ @updated_at = step_data[:updated_at]
172
+ # TAS-125: Checkpoint data for batch processing resumption
173
+ @checkpoint = step_data[:checkpoint]&.with_indifferent_access
174
+ end
175
+ end
176
+
177
+ # Wrapper for dependency results from parent steps.
178
+ #
179
+ # Provides access to results from steps that this step depends on.
180
+ # Results are keyed by step name.
181
+ #
182
+ # @note Two methods for accessing results:
183
+ # - `get_result(name)` returns the full result hash with metadata
184
+ # - `get_results(name)` returns just the computed value (recommended for handlers)
185
+ #
186
+ # @example Accessing dependency results
187
+ # deps.get_results('previous_step') # => 36 (just the value)
188
+ # deps.get_result('previous_step') # => { result: 36, metadata: {...} }
189
+ # deps['previous_step'] # => { result: 36, metadata: {...} }
190
+ class DependencyResultsWrapper
191
+ # Creates a new DependencyResultsWrapper
192
+ #
193
+ # @param results_data [Hash] Hash of step names to their results
194
+ def initialize(results_data)
195
+ # Use HashWithIndifferentAccess for developer-friendly key access
196
+ @results = (results_data || {}).with_indifferent_access
197
+ end
198
+
199
+ # Get result from a parent step (returns full result hash)
200
+ #
201
+ # @param step_name [String, Symbol] Name of the parent step
202
+ # @return [Hash, nil] The full result hash or nil if not found
203
+ def get_result(step_name)
204
+ @results[step_name] # HashWithIndifferentAccess handles string/symbol automatically
205
+ end
206
+
207
+ # Get the actual result value from a parent step (extracts 'result' field)
208
+ #
209
+ # This is the typical method handlers use to get the actual computed value
210
+ # from a parent step, rather than the full result metadata hash.
211
+ #
212
+ # @param step_name [String, Symbol] Name of the parent step
213
+ # @return [Object, nil] The result value or nil if not found
214
+ #
215
+ # @example
216
+ # deps.get_results('linear_step_1') # => 36 (the actual value)
217
+ # deps.get_result('linear_step_1') # => { result: 36, metadata: {...} }
218
+ def get_results(step_name)
219
+ result_hash = @results[step_name]
220
+ return nil unless result_hash
221
+
222
+ # If it's a hash with a 'result' key, extract that value
223
+ # Otherwise return the whole thing (might be a primitive value)
224
+ if result_hash.is_a?(Hash) && result_hash.key?('result')
225
+ result_hash['result']
226
+ else
227
+ result_hash
228
+ end
229
+ end
230
+
231
+ # Array-style access to dependency results
232
+ #
233
+ # @param step_name [String, Symbol] Name of the parent step
234
+ # @return [Hash, nil] The full result hash or nil if not found
235
+ def [](step_name)
236
+ get_result(step_name)
237
+ end
238
+
239
+ # Get all dependency result step names
240
+ #
241
+ # @return [Array<String>] List of step names that have results
242
+ def keys
243
+ @results.keys
244
+ end
245
+
246
+ # Check if a dependency result exists
247
+ #
248
+ # @param step_name [String, Symbol] Name of the parent step
249
+ # @return [Boolean] True if result exists
250
+ def key?(step_name)
251
+ @results.key?(step_name) # HashWithIndifferentAccess handles string/symbol automatically
252
+ end
253
+
254
+ def each_key(&)
255
+ @results.keys.each(&)
256
+ end
257
+ end
258
+
259
+ # Wrapper for step definition from task template.
260
+ #
261
+ # Provides access to step configuration including handler specification,
262
+ # dependencies, retry policy, and timeout settings.
263
+ #
264
+ # @example Accessing step definition
265
+ # step_def.name # => "linear_step_1"
266
+ # step_def.description # => "Square the initial even number..."
267
+ # step_def.handler.callable # => "LinearWorkflow::StepHandlers::LinearStep1Handler"
268
+ # step_def.dependencies # => ["previous_step"]
269
+ # step_def.timeout_seconds # => 30
270
+ class StepDefinitionWrapper
271
+ # @return [String] Step name
272
+ # @return [String] Human-readable description
273
+ # @return [HandlerWrapper] Handler configuration
274
+
275
+ # @return [Array<String>] Names of parent steps this depends on
276
+ # @return [ActiveSupport::HashWithIndifferentAccess] Retry configuration
277
+ # @return [Integer] Timeout in seconds
278
+ # @return [Array<String>] Events this step publishes
279
+ attr_reader :name, :description, :handler,
280
+ :dependencies, :retry, :timeout_seconds, :publishes_events
281
+
282
+ # Creates a new StepDefinitionWrapper from template data
283
+ #
284
+ # @param definition_data [Hash] Step definition from task template
285
+ def initialize(definition_data)
286
+ @name = definition_data[:name]
287
+ @description = definition_data[:description]
288
+ @handler = HandlerWrapper.new(definition_data[:handler])
289
+
290
+ @dependencies = definition_data[:dependencies] || []
291
+ # Use HashWithIndifferentAccess for retry configuration
292
+ @retry = (definition_data[:retry] || {}).with_indifferent_access
293
+ @timeout_seconds = definition_data[:timeout_seconds]
294
+ @publishes_events = definition_data[:publishes_events] || []
295
+ end
296
+ end
297
+
298
+ # Wrapper for handler configuration.
299
+ #
300
+ # Provides access to handler class name, method dispatch, resolver hints,
301
+ # and initialization parameters from the task template.
302
+ #
303
+ # TAS-93: Extended to support method dispatch and resolver hints from
304
+ # the Rust HandlerDefinition struct.
305
+ #
306
+ # @example Accessing handler configuration
307
+ # handler.callable # => "LinearWorkflow::StepHandlers::LinearStep1Handler"
308
+ # handler.initialization # => { operation: "square", step_number: 1 }
309
+ # handler.handler_method # => "refund" (or nil for default .call())
310
+ # handler.resolver # => "explicit" (or nil for chain traversal)
311
+ class HandlerWrapper
312
+ # @return [String] Fully-qualified handler class name
313
+ # @return [ActiveSupport::HashWithIndifferentAccess] Initialization parameters for the handler
314
+ # @return [String, nil] TAS-93: Method to invoke instead of default .call()
315
+ # @return [String, nil] TAS-93: Specific resolver to use (bypasses chain)
316
+ attr_reader :callable, :initialization, :handler_method, :resolver
317
+
318
+ # Creates a new HandlerWrapper from template data
319
+ #
320
+ # @param handler_data [Hash] Handler configuration from template
321
+ # @option handler_data [String] :callable Handler class name
322
+ # @option handler_data [Hash] :initialization Handler init params
323
+ # @option handler_data [String] :method TAS-93: Method to invoke (from Rust FFI)
324
+ # @option handler_data [String] :resolver TAS-93: Resolver hint (from Rust FFI)
325
+ def initialize(handler_data)
326
+ @callable = handler_data[:callable]
327
+ # Use HashWithIndifferentAccess for initialization parameters
328
+ @initialization = (handler_data[:initialization] || {}).with_indifferent_access
329
+ # TAS-93: Method dispatch - note Rust field is 'method' but we use 'handler_method'
330
+ # to avoid conflict with Ruby's Object#method
331
+ @handler_method = handler_data[:method]
332
+ # TAS-93: Resolver hint for direct resolver routing
333
+ @resolver = handler_data[:resolver]
334
+ end
335
+ end
336
+ end
337
+ end