tasker-rb 0.1.1

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 (78) hide show
  1. checksums.yaml +7 -0
  2. data/DEVELOPMENT.md +548 -0
  3. data/README.md +87 -0
  4. data/ext/tasker_core/Cargo.lock +4720 -0
  5. data/ext/tasker_core/Cargo.toml +76 -0
  6. data/ext/tasker_core/extconf.rb +38 -0
  7. data/ext/tasker_core/src/CLAUDE.md +7 -0
  8. data/ext/tasker_core/src/bootstrap.rs +320 -0
  9. data/ext/tasker_core/src/bridge.rs +400 -0
  10. data/ext/tasker_core/src/client_ffi.rs +173 -0
  11. data/ext/tasker_core/src/conversions.rs +131 -0
  12. data/ext/tasker_core/src/diagnostics.rs +57 -0
  13. data/ext/tasker_core/src/event_handler.rs +179 -0
  14. data/ext/tasker_core/src/event_publisher_ffi.rs +239 -0
  15. data/ext/tasker_core/src/ffi_logging.rs +245 -0
  16. data/ext/tasker_core/src/global_event_system.rs +16 -0
  17. data/ext/tasker_core/src/in_process_event_ffi.rs +319 -0
  18. data/ext/tasker_core/src/lib.rs +41 -0
  19. data/ext/tasker_core/src/observability_ffi.rs +339 -0
  20. data/lib/tasker_core/batch_processing/batch_aggregation_scenario.rb +85 -0
  21. data/lib/tasker_core/batch_processing/batch_worker_context.rb +238 -0
  22. data/lib/tasker_core/bootstrap.rb +394 -0
  23. data/lib/tasker_core/domain_events/base_publisher.rb +220 -0
  24. data/lib/tasker_core/domain_events/base_subscriber.rb +178 -0
  25. data/lib/tasker_core/domain_events/publisher_registry.rb +253 -0
  26. data/lib/tasker_core/domain_events/subscriber_registry.rb +152 -0
  27. data/lib/tasker_core/domain_events.rb +43 -0
  28. data/lib/tasker_core/errors/CLAUDE.md +7 -0
  29. data/lib/tasker_core/errors/common.rb +305 -0
  30. data/lib/tasker_core/errors/error_classifier.rb +61 -0
  31. data/lib/tasker_core/errors.rb +4 -0
  32. data/lib/tasker_core/event_bridge.rb +330 -0
  33. data/lib/tasker_core/handlers.rb +159 -0
  34. data/lib/tasker_core/internal.rb +31 -0
  35. data/lib/tasker_core/logger.rb +234 -0
  36. data/lib/tasker_core/models.rb +337 -0
  37. data/lib/tasker_core/observability/types.rb +158 -0
  38. data/lib/tasker_core/observability.rb +292 -0
  39. data/lib/tasker_core/registry/handler_registry.rb +453 -0
  40. data/lib/tasker_core/registry/resolver_chain.rb +258 -0
  41. data/lib/tasker_core/registry/resolvers/base_resolver.rb +90 -0
  42. data/lib/tasker_core/registry/resolvers/class_constant_resolver.rb +156 -0
  43. data/lib/tasker_core/registry/resolvers/explicit_mapping_resolver.rb +146 -0
  44. data/lib/tasker_core/registry/resolvers/method_dispatch_wrapper.rb +144 -0
  45. data/lib/tasker_core/registry/resolvers/registry_resolver.rb +229 -0
  46. data/lib/tasker_core/registry/resolvers.rb +42 -0
  47. data/lib/tasker_core/registry.rb +12 -0
  48. data/lib/tasker_core/step_handler/api.rb +48 -0
  49. data/lib/tasker_core/step_handler/base.rb +354 -0
  50. data/lib/tasker_core/step_handler/batchable.rb +50 -0
  51. data/lib/tasker_core/step_handler/decision.rb +53 -0
  52. data/lib/tasker_core/step_handler/mixins/api.rb +452 -0
  53. data/lib/tasker_core/step_handler/mixins/batchable.rb +465 -0
  54. data/lib/tasker_core/step_handler/mixins/decision.rb +252 -0
  55. data/lib/tasker_core/step_handler/mixins.rb +66 -0
  56. data/lib/tasker_core/subscriber.rb +212 -0
  57. data/lib/tasker_core/task_handler/base.rb +254 -0
  58. data/lib/tasker_core/tasker_rb.so +0 -0
  59. data/lib/tasker_core/template_discovery.rb +181 -0
  60. data/lib/tasker_core/tracing.rb +166 -0
  61. data/lib/tasker_core/types/batch_processing_outcome.rb +301 -0
  62. data/lib/tasker_core/types/client_types.rb +145 -0
  63. data/lib/tasker_core/types/decision_point_outcome.rb +177 -0
  64. data/lib/tasker_core/types/error_types.rb +72 -0
  65. data/lib/tasker_core/types/simple_message.rb +151 -0
  66. data/lib/tasker_core/types/step_context.rb +328 -0
  67. data/lib/tasker_core/types/step_handler_call_result.rb +307 -0
  68. data/lib/tasker_core/types/step_message.rb +112 -0
  69. data/lib/tasker_core/types/step_types.rb +207 -0
  70. data/lib/tasker_core/types/task_template.rb +240 -0
  71. data/lib/tasker_core/types/task_types.rb +148 -0
  72. data/lib/tasker_core/types.rb +132 -0
  73. data/lib/tasker_core/version.rb +13 -0
  74. data/lib/tasker_core/worker/CLAUDE.md +7 -0
  75. data/lib/tasker_core/worker/event_poller.rb +224 -0
  76. data/lib/tasker_core/worker/in_process_domain_event_poller.rb +271 -0
  77. data/lib/tasker_core.rb +160 -0
  78. metadata +322 -0
@@ -0,0 +1,301 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry-struct'
4
+ require 'dry-types'
5
+
6
+ module TaskerCore
7
+ module Types
8
+ # BatchProcessingOutcome - TAS-59 Batch Processing Implementation
9
+ #
10
+ # Represents the outcome of a batch processing handler execution.
11
+ # Batch processing handlers determine whether to create worker batches
12
+ # based on business logic and data volume.
13
+ #
14
+ # ## Usage Patterns
15
+ #
16
+ # ### No Batches (No worker batches needed)
17
+ # ```ruby
18
+ # BatchProcessingOutcome.no_batches
19
+ # ```
20
+ #
21
+ # ### Create Worker Batches
22
+ # ```ruby
23
+ # # Create batches for processing large datasets
24
+ # cursor_configs = [
25
+ # { 'batch_id' => '001', 'start_cursor' => 1, 'end_cursor' => 200 },
26
+ # { 'batch_id' => '002', 'start_cursor' => 201, 'end_cursor' => 400 },
27
+ # { 'batch_id' => '003', 'start_cursor' => 401, 'end_cursor' => 600 }
28
+ # ]
29
+ #
30
+ # BatchProcessingOutcome.create_batches(
31
+ # worker_template_name: 'process_batch_worker',
32
+ # worker_count: 3,
33
+ # cursor_configs: cursor_configs,
34
+ # total_items: 250
35
+ # )
36
+ # ```
37
+ #
38
+ # ## Integration with Step Handlers
39
+ #
40
+ # Batch processing handlers should return this outcome in their result:
41
+ #
42
+ # ```ruby
43
+ # def call(context)
44
+ # # Business logic to determine if batching is needed
45
+ # total_records = context.get_task_field('total_records')
46
+ #
47
+ # outcome = if total_records > 1000
48
+ # # Create batches for parallel processing
49
+ # cursor_configs = calculate_cursor_configs(total_records)
50
+ # BatchProcessingOutcome.create_batches(
51
+ # worker_template_name: 'batch_processor',
52
+ # worker_count: cursor_configs.size,
53
+ # cursor_configs: cursor_configs,
54
+ # total_items: total_records
55
+ # )
56
+ # else
57
+ # # Process inline, no batches needed
58
+ # BatchProcessingOutcome.no_batches
59
+ # end
60
+ #
61
+ # StepHandlerCallResult.success(
62
+ # result: {
63
+ # batch_processing_outcome: outcome.to_h
64
+ # },
65
+ # metadata: {
66
+ # operation: 'batch_decision',
67
+ # batches_created: outcome.requires_batch_creation? ? cursor_configs.size : 0
68
+ # }
69
+ # )
70
+ # end
71
+ # ```
72
+ module BatchProcessingOutcome
73
+ module Types
74
+ include Dry.Types()
75
+ end
76
+
77
+ # NoBatches outcome - no worker batches needed
78
+ class NoBatches < Dry::Struct
79
+ # Type discriminator (matches Rust serde tag field)
80
+ attribute :type, Types::String.default('no_batches')
81
+
82
+ # Convert to hash for serialization to Rust
83
+ # Note: Rust expects lowercase snake_case due to #[serde(rename_all = "snake_case")]
84
+ def to_h
85
+ { 'type' => 'no_batches' }
86
+ end
87
+
88
+ # Check if this outcome requires batch creation
89
+ def requires_batch_creation?
90
+ false
91
+ end
92
+ end
93
+
94
+ # CreateBatches outcome - create worker batches for parallel processing
95
+ class CreateBatches < Dry::Struct
96
+ # Type discriminator (matches Rust serde tag field)
97
+ attribute :type, Types::String.default('create_batches')
98
+
99
+ # Name of the worker template to use for batch processing
100
+ attribute :worker_template_name, Types::Strict::String
101
+
102
+ # Number of worker batches to create
103
+ attribute :worker_count, Types::Strict::Integer.constrained(gteq: 0)
104
+
105
+ # Array of cursor configurations for batch processing
106
+ # Each config is a flexible hash (e.g., batch_id, start_cursor, end_cursor, batch_size)
107
+ attribute :cursor_configs, Types::Array.of(Types::Hash).constrained(min_size: 1)
108
+
109
+ # Total number of items to be processed across all batches
110
+ attribute :total_items, Types::Strict::Integer.constrained(gteq: 0)
111
+
112
+ # Convert to hash for serialization to Rust
113
+ # Note: Rust expects lowercase snake_case due to #[serde(rename_all = "snake_case")]
114
+ def to_h
115
+ {
116
+ 'type' => 'create_batches',
117
+ 'worker_template_name' => worker_template_name,
118
+ 'worker_count' => worker_count,
119
+ 'cursor_configs' => cursor_configs,
120
+ 'total_items' => total_items
121
+ }
122
+ end
123
+
124
+ # Check if this outcome requires batch creation
125
+ def requires_batch_creation?
126
+ true
127
+ end
128
+ end
129
+
130
+ # Factory methods for creating outcomes
131
+ class << self
132
+ # Create a NoBatches outcome
133
+ #
134
+ # Use when the batch processing handler determines that no worker
135
+ # batches are needed (e.g., small dataset, inline processing).
136
+ #
137
+ # @return [NoBatches] A no-batches outcome instance
138
+ #
139
+ # @example
140
+ # BatchProcessingOutcome.no_batches
141
+ def no_batches
142
+ NoBatches.new
143
+ end
144
+
145
+ # Create a CreateBatches outcome with specified batch configuration
146
+ #
147
+ # Use when the batch processing handler determines that worker batches
148
+ # should be created for parallel processing.
149
+ #
150
+ # @param worker_template_name [String] Name of worker template to use
151
+ # @param worker_count [Integer] Number of worker batches to create (must be >= 0)
152
+ # @param cursor_configs [Array<Hash>] Array of cursor configurations
153
+ # @param total_items [Integer] Total number of items to process (must be >= 0)
154
+ # @return [CreateBatches] A create-batches outcome instance
155
+ # @raise [ArgumentError] if required parameters are invalid
156
+ #
157
+ # @example Create batches for CSV row processing
158
+ # cursor_configs = [
159
+ # { 'batch_id' => '001', 'start_cursor' => 1, 'end_cursor' => 200, 'batch_size' => 200 },
160
+ # { 'batch_id' => '002', 'start_cursor' => 201, 'end_cursor' => 400, 'batch_size' => 200 }
161
+ # ]
162
+ # BatchProcessingOutcome.create_batches(
163
+ # worker_template_name: 'process_csv_batch',
164
+ # worker_count: 2,
165
+ # cursor_configs: cursor_configs,
166
+ # total_items: 400
167
+ # )
168
+ def create_batches(worker_template_name:, worker_count:, cursor_configs:, total_items:)
169
+ if worker_template_name.nil? || worker_template_name.empty?
170
+ raise ArgumentError,
171
+ 'worker_template_name cannot be empty'
172
+ end
173
+ raise ArgumentError, 'cursor_configs cannot be empty' if cursor_configs.nil? || cursor_configs.empty?
174
+
175
+ # Validate cursor_configs count matches worker_count
176
+ if cursor_configs.size != worker_count
177
+ raise ArgumentError,
178
+ "cursor_configs length (#{cursor_configs.size}) must equal worker_count (#{worker_count})"
179
+ end
180
+
181
+ # Validate batch_size consistency for numeric cursors
182
+ # Only validate when both cursors are integers (flexible cursors like UUIDs/timestamps don't have batch_size)
183
+ cursor_configs.each_with_index do |config, index|
184
+ next unless config.key?('batch_size') || config.key?(:batch_size)
185
+
186
+ # Extract values supporting both string and symbol keys
187
+ start_cursor = config['start_cursor'] || config[:start_cursor]
188
+ end_cursor = config['end_cursor'] || config[:end_cursor]
189
+ batch_size = config['batch_size'] || config[:batch_size]
190
+
191
+ # Only validate for numeric cursors
192
+ next unless start_cursor.is_a?(Integer) && end_cursor.is_a?(Integer)
193
+
194
+ expected_batch_size = end_cursor - start_cursor
195
+
196
+ next unless batch_size != expected_batch_size
197
+
198
+ raise ArgumentError,
199
+ "cursor_configs[#{index}] batch_size (#{batch_size}) must equal " \
200
+ "end_cursor - start_cursor (#{end_cursor} - #{start_cursor} = #{expected_batch_size})"
201
+ end
202
+
203
+ CreateBatches.new(
204
+ worker_template_name: worker_template_name,
205
+ worker_count: worker_count,
206
+ cursor_configs: cursor_configs,
207
+ total_items: total_items
208
+ )
209
+ end
210
+
211
+ # Parse a BatchProcessingOutcome from a hash
212
+ #
213
+ # Used to deserialize outcomes from step handler results.
214
+ # Supports both symbol and string keys for maximum flexibility.
215
+ #
216
+ # @param hash [Hash] The hash representation of an outcome
217
+ # @return [NoBatches, CreateBatches] The parsed outcome
218
+ # @raise [ArgumentError] If hash is invalid or missing required fields
219
+ #
220
+ # @example Parse no-batches outcome
221
+ # outcome = BatchProcessingOutcome.from_hash({
222
+ # type: 'no_batches'
223
+ # })
224
+ #
225
+ # @example Parse create-batches outcome
226
+ # outcome = BatchProcessingOutcome.from_hash({
227
+ # 'type' => 'create_batches',
228
+ # 'worker_template_name' => 'batch_worker',
229
+ # 'worker_count' => 3,
230
+ # 'cursor_configs' => [
231
+ # { 'cursor_key' => 'offset', 'cursor_value' => 0 }
232
+ # ],
233
+ # 'total_items' => 300
234
+ # })
235
+ def from_hash(hash)
236
+ raise ArgumentError, 'Expected Hash, got nil' if hash.nil?
237
+ raise ArgumentError, "Expected Hash, got #{hash.class}" unless hash.is_a?(Hash)
238
+
239
+ # Support both symbol and string keys
240
+ # Note: Rust sends lowercase snake_case due to #[serde(rename_all = "snake_case")]
241
+ hash = deep_symbolize_keys(hash)
242
+ outcome_type = hash[:type]
243
+
244
+ case outcome_type
245
+ when 'no_batches'
246
+ NoBatches.new
247
+ when 'create_batches'
248
+ worker_template_name = hash[:worker_template_name]
249
+ worker_count = hash[:worker_count]
250
+ cursor_configs = hash[:cursor_configs]
251
+ total_items = hash[:total_items]
252
+
253
+ if worker_template_name.nil? || worker_template_name.empty?
254
+ raise ArgumentError, 'worker_template_name is required for create_batches outcome'
255
+ end
256
+
257
+ if worker_count.nil? || worker_count.negative?
258
+ raise ArgumentError,
259
+ 'worker_count is required and must be >= 0'
260
+ end
261
+
262
+ unless cursor_configs.is_a?(Array) && !cursor_configs.empty?
263
+ raise ArgumentError, 'cursor_configs must be a non-empty array'
264
+ end
265
+ raise ArgumentError, 'total_items is required and must be >= 0' if total_items.nil? || total_items.negative?
266
+
267
+ CreateBatches.new(
268
+ worker_template_name: worker_template_name,
269
+ worker_count: worker_count,
270
+ cursor_configs: cursor_configs,
271
+ total_items: total_items
272
+ )
273
+ else
274
+ raise ArgumentError, "Unknown outcome type: #{outcome_type.inspect}"
275
+ end
276
+ end
277
+
278
+ private
279
+
280
+ # Deep symbolize hash keys for consistent access
281
+ # Handles nested hashes and arrays of hashes
282
+ def deep_symbolize_keys(hash)
283
+ return hash unless hash.is_a?(Hash)
284
+
285
+ hash.each_with_object({}) do |(key, value), result|
286
+ new_key = key.to_sym
287
+ new_value = case value
288
+ when Hash
289
+ deep_symbolize_keys(value)
290
+ when Array
291
+ value.map { |v| v.is_a?(Hash) ? deep_symbolize_keys(v) : v }
292
+ else
293
+ value
294
+ end
295
+ result[new_key] = new_value
296
+ end
297
+ end
298
+ end
299
+ end
300
+ end
301
+ end
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry-struct'
4
+ require 'dry-types'
5
+
6
+ module TaskerCore
7
+ module Types
8
+ # TAS-231: Client API response types for orchestration client FFI.
9
+ #
10
+ # These types match the JSON responses from the orchestration API,
11
+ # as returned by the client FFI functions (client_create_task, etc.).
12
+ module ClientTypes
13
+ module Types
14
+ include Dry.Types()
15
+ end
16
+
17
+ # Step readiness status within a task response
18
+ class StepReadiness < Dry::Struct
19
+ attribute :workflow_step_uuid, Types::String
20
+ attribute :task_uuid, Types::String
21
+ attribute :named_step_uuid, Types::String
22
+ attribute :name, Types::String
23
+ attribute :current_state, Types::String
24
+ attribute :dependencies_satisfied, Types::Bool
25
+ attribute :retry_eligible, Types::Bool
26
+ attribute :ready_for_execution, Types::Bool
27
+ attribute? :last_failure_at, Types::String.optional
28
+ attribute? :next_retry_at, Types::String.optional
29
+ attribute :total_parents, Types::Integer
30
+ attribute :completed_parents, Types::Integer
31
+ attribute :attempts, Types::Integer
32
+ attribute :max_attempts, Types::Integer
33
+ attribute? :backoff_request_seconds, Types::Integer.optional
34
+ attribute? :last_attempted_at, Types::String.optional
35
+ end
36
+
37
+ # Task response from the orchestration API.
38
+ # Returned by client_create_task and client_get_task.
39
+ class TaskResponse < Dry::Struct
40
+ attribute :task_uuid, Types::String
41
+ attribute :name, Types::String
42
+ attribute :namespace, Types::String
43
+ attribute :version, Types::String
44
+ attribute :status, Types::String
45
+ attribute :created_at, Types::String
46
+ attribute :updated_at, Types::String
47
+ attribute? :completed_at, Types::String.optional
48
+ attribute :context, Types::Hash
49
+ attribute :initiator, Types::String
50
+ attribute :source_system, Types::String
51
+ attribute :reason, Types::String
52
+ attribute? :priority, Types::Integer.optional
53
+ attribute? :tags, Types::Array.of(Types::String).optional
54
+ attribute :correlation_id, Types::String
55
+ attribute? :parent_correlation_id, Types::String.optional
56
+
57
+ # Execution context
58
+ attribute :total_steps, Types::Integer
59
+ attribute :pending_steps, Types::Integer
60
+ attribute :in_progress_steps, Types::Integer
61
+ attribute :completed_steps, Types::Integer
62
+ attribute :failed_steps, Types::Integer
63
+ attribute :ready_steps, Types::Integer
64
+ attribute :execution_status, Types::String
65
+ attribute :recommended_action, Types::String
66
+ attribute :completion_percentage, Types::Float
67
+ attribute :health_status, Types::String
68
+
69
+ # Step readiness
70
+ attribute :steps, Types::Array.default([].freeze)
71
+
72
+ def to_s
73
+ "#<ClientTaskResponse #{namespace}/#{name}:#{version} status=#{status}>"
74
+ end
75
+ end
76
+
77
+ # Pagination information in list responses
78
+ class PaginationInfo < Dry::Struct
79
+ attribute :page, Types::Integer
80
+ attribute :per_page, Types::Integer
81
+ attribute :total_count, Types::Integer
82
+ attribute :total_pages, Types::Integer
83
+ attribute :has_next, Types::Bool
84
+ attribute :has_previous, Types::Bool
85
+ end
86
+
87
+ # Task list response with pagination.
88
+ # Returned by client_list_tasks.
89
+ class TaskListResponse < Dry::Struct
90
+ attribute :tasks, Types::Array.default([].freeze)
91
+ attribute :pagination, Types::Hash
92
+ end
93
+
94
+ # Step response from the orchestration API.
95
+ # Returned by client_get_step.
96
+ class StepResponse < Dry::Struct
97
+ attribute :step_uuid, Types::String
98
+ attribute :task_uuid, Types::String
99
+ attribute :name, Types::String
100
+ attribute :created_at, Types::String
101
+ attribute :updated_at, Types::String
102
+ attribute? :completed_at, Types::String.optional
103
+ attribute? :results, Types::Hash.optional
104
+
105
+ # Readiness fields
106
+ attribute :current_state, Types::String
107
+ attribute :dependencies_satisfied, Types::Bool
108
+ attribute :retry_eligible, Types::Bool
109
+ attribute :ready_for_execution, Types::Bool
110
+ attribute :total_parents, Types::Integer
111
+ attribute :completed_parents, Types::Integer
112
+ attribute :attempts, Types::Integer
113
+ attribute :max_attempts, Types::Integer
114
+ attribute? :last_failure_at, Types::String.optional
115
+ attribute? :next_retry_at, Types::String.optional
116
+ attribute? :last_attempted_at, Types::String.optional
117
+ end
118
+
119
+ # Step audit history response (SOC2 compliance).
120
+ # Returned by client_get_step_audit_history.
121
+ class StepAuditResponse < Dry::Struct
122
+ attribute :audit_uuid, Types::String
123
+ attribute :workflow_step_uuid, Types::String
124
+ attribute :transition_uuid, Types::String
125
+ attribute :task_uuid, Types::String
126
+ attribute :recorded_at, Types::String
127
+ attribute? :worker_uuid, Types::String.optional
128
+ attribute? :correlation_id, Types::String.optional
129
+ attribute :success, Types::Bool
130
+ attribute? :execution_time_ms, Types::Integer.optional
131
+ attribute? :result, Types::Hash.optional
132
+ attribute :step_name, Types::String
133
+ attribute? :from_state, Types::String.optional
134
+ attribute :to_state, Types::String
135
+ end
136
+
137
+ # Health check response from the orchestration API.
138
+ # Returned by client_health_check.
139
+ class HealthResponse < Dry::Struct
140
+ attribute :status, Types::String
141
+ attribute :timestamp, Types::String
142
+ end
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,177 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry-struct'
4
+ require 'dry-types'
5
+
6
+ module TaskerCore
7
+ module Types
8
+ # DecisionPointOutcome - TAS-53 Dynamic Workflow Decision Points
9
+ #
10
+ # Represents the outcome of a decision point handler execution.
11
+ # Decision point handlers make runtime decisions about which workflow steps
12
+ # to create dynamically based on business logic.
13
+ #
14
+ # ## Usage Patterns
15
+ #
16
+ # ### No Branches (No additional steps needed)
17
+ # ```ruby
18
+ # DecisionPointOutcome.no_branches
19
+ # ```
20
+ #
21
+ # ### Create Specific Steps
22
+ # ```ruby
23
+ # # Simple: create one or more steps by name
24
+ # DecisionPointOutcome.create_steps(["approval_required"])
25
+ #
26
+ # # Multiple branches based on condition
27
+ # if high_value?
28
+ # DecisionPointOutcome.create_steps(["manager_approval", "finance_review"])
29
+ # else
30
+ # DecisionPointOutcome.create_steps(["auto_approve"])
31
+ # end
32
+ # ```
33
+ #
34
+ # ## Integration with Step Handlers
35
+ #
36
+ # Decision point handlers should return this outcome in their result:
37
+ #
38
+ # ```ruby
39
+ # def call(context)
40
+ # # Business logic to determine which steps to create
41
+ # steps_to_create = if context.get_task_field('amount') > 1000
42
+ # ['manager_approval', 'finance_review']
43
+ # else
44
+ # ['auto_approve']
45
+ # end
46
+ #
47
+ # outcome = DecisionPointOutcome.create_steps(steps_to_create)
48
+ #
49
+ # StepHandlerCallResult.success(
50
+ # result: {
51
+ # decision_point_outcome: outcome.to_h
52
+ # },
53
+ # metadata: {
54
+ # operation: 'routing_decision',
55
+ # branches_created: steps_to_create.size
56
+ # }
57
+ # )
58
+ # end
59
+ # ```
60
+ module DecisionPointOutcome
61
+ module Types
62
+ include Dry.Types()
63
+ end
64
+
65
+ # NoBranches outcome - no additional steps needed
66
+ class NoBranches < Dry::Struct
67
+ # Type discriminator (matches Rust serde tag field)
68
+ attribute :type, Types::String.default('no_branches')
69
+
70
+ # Convert to hash for serialization to Rust
71
+ # Note: Rust expects lowercase snake_case due to #[serde(rename_all = "snake_case")]
72
+ def to_h
73
+ { type: 'no_branches' }
74
+ end
75
+
76
+ # Check if this outcome requires step creation
77
+ def requires_step_creation?
78
+ false
79
+ end
80
+
81
+ # Get step names (empty for NoBranches)
82
+ def step_names
83
+ []
84
+ end
85
+ end
86
+
87
+ # CreateSteps outcome - dynamically create specified workflow steps
88
+ class CreateSteps < Dry::Struct
89
+ # Type discriminator (matches Rust serde tag field)
90
+ attribute :type, Types::String.default('create_steps')
91
+
92
+ # Array of step names to create
93
+ attribute :step_names, Types::Array.of(Types::Strict::String).constrained(min_size: 1)
94
+
95
+ # Convert to hash for serialization to Rust
96
+ # Note: Rust expects lowercase snake_case due to #[serde(rename_all = "snake_case")]
97
+ def to_h
98
+ {
99
+ type: 'create_steps',
100
+ step_names: step_names
101
+ }
102
+ end
103
+
104
+ # Check if this outcome requires step creation
105
+ def requires_step_creation?
106
+ true
107
+ end
108
+ end
109
+
110
+ # Factory methods for creating outcomes
111
+ class << self
112
+ # Create a NoBranches outcome
113
+ #
114
+ # Use when the decision point determines that no additional workflow
115
+ # steps are needed.
116
+ #
117
+ # @return [NoBranches] A no-branches outcome instance
118
+ #
119
+ # @example
120
+ # DecisionPointOutcome.no_branches
121
+ def no_branches
122
+ NoBranches.new
123
+ end
124
+
125
+ # Create a CreateSteps outcome with specified step names
126
+ #
127
+ # Use when the decision point determines that one or more workflow
128
+ # steps should be dynamically created.
129
+ #
130
+ # @param step_names [Array<String>] Names of steps to create (must be valid step names from template)
131
+ # @return [CreateSteps] A create-steps outcome instance
132
+ # @raise [ArgumentError] if step_names is empty
133
+ #
134
+ # @example Create single step
135
+ # DecisionPointOutcome.create_steps(['approval_required'])
136
+ #
137
+ # @example Create multiple steps
138
+ # DecisionPointOutcome.create_steps(['manager_approval', 'finance_review'])
139
+ def create_steps(step_names)
140
+ raise ArgumentError, 'step_names cannot be empty' if step_names.nil? || step_names.empty?
141
+
142
+ CreateSteps.new(step_names: step_names)
143
+ end
144
+
145
+ # Parse a DecisionPointOutcome from a hash
146
+ #
147
+ # Used to deserialize outcomes from step handler results.
148
+ #
149
+ # @param hash [Hash] The hash representation of an outcome
150
+ # @return [NoBranches, CreateSteps, nil] The parsed outcome or nil if invalid
151
+ #
152
+ # @example
153
+ # outcome = DecisionPointOutcome.from_hash({
154
+ # type: 'create_steps',
155
+ # step_names: ['approval_required']
156
+ # })
157
+ def from_hash(hash)
158
+ return nil unless hash.is_a?(Hash)
159
+
160
+ # Support both symbol and string keys
161
+ # Note: Rust sends lowercase snake_case due to #[serde(rename_all = "snake_case")]
162
+ outcome_type = hash[:type] || hash['type']
163
+
164
+ case outcome_type
165
+ when 'no_branches'
166
+ NoBranches.new
167
+ when 'create_steps'
168
+ step_names = hash[:step_names] || hash['step_names']
169
+ return nil unless step_names.is_a?(Array) && !step_names.empty?
170
+
171
+ CreateSteps.new(step_names: step_names)
172
+ end
173
+ end
174
+ end
175
+ end
176
+ end
177
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TaskerCore
4
+ module Types
5
+ # Standard error type constants for cross-language consistency.
6
+ #
7
+ # These error types are used across Ruby, Python, and Rust workers
8
+ # to categorize step execution failures consistently.
9
+ #
10
+ # Note: Values use PascalCase to match StepHandlerCallResult::Error constraints.
11
+ #
12
+ # @example Using error types in a handler
13
+ # failure(
14
+ # message: "Invalid order total",
15
+ # error_type: TaskerCore::Types::ErrorTypes::VALIDATION_ERROR,
16
+ # retryable: false
17
+ # )
18
+ #
19
+ # @example Checking if an error type is valid
20
+ # TaskerCore::Types::ErrorTypes.valid?('ValidationError') # => true
21
+ # TaskerCore::Types::ErrorTypes.valid?('unknown_type') # => false
22
+ module ErrorTypes
23
+ # Permanent error that should not be retried
24
+ PERMANENT_ERROR = 'PermanentError'
25
+
26
+ # Retryable error that may succeed on subsequent attempts
27
+ RETRYABLE_ERROR = 'RetryableError'
28
+
29
+ # Validation error due to invalid input data
30
+ VALIDATION_ERROR = 'ValidationError'
31
+
32
+ # Unexpected error from unknown causes
33
+ UNEXPECTED_ERROR = 'UnexpectedError'
34
+
35
+ # Step completion error
36
+ STEP_COMPLETION_ERROR = 'StepCompletionError'
37
+
38
+ # All valid error types (matching StepHandlerCallResult::Error constraints)
39
+ ALL = [
40
+ PERMANENT_ERROR,
41
+ RETRYABLE_ERROR,
42
+ VALIDATION_ERROR,
43
+ UNEXPECTED_ERROR,
44
+ STEP_COMPLETION_ERROR
45
+ ].freeze
46
+
47
+ # Check if an error type is a recognized standard type
48
+ #
49
+ # @param error_type [String] The error type to check
50
+ # @return [Boolean] True if the error type is recognized
51
+ def self.valid?(error_type)
52
+ ALL.include?(error_type)
53
+ end
54
+
55
+ # Check if an error type is typically retryable
56
+ #
57
+ # @param error_type [String] The error type to check
58
+ # @return [Boolean] True if the error type typically allows retry
59
+ def self.typically_retryable?(error_type)
60
+ [RETRYABLE_ERROR, UNEXPECTED_ERROR].include?(error_type)
61
+ end
62
+
63
+ # Check if an error type is typically permanent (not retryable)
64
+ #
65
+ # @param error_type [String] The error type to check
66
+ # @return [Boolean] True if the error type is typically permanent
67
+ def self.typically_permanent?(error_type)
68
+ [PERMANENT_ERROR, VALIDATION_ERROR].include?(error_type)
69
+ end
70
+ end
71
+ end
72
+ end