cmdx 1.1.0 → 1.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.
- checksums.yaml +4 -4
- data/.cursor/prompts/docs.md +9 -0
- data/.cursor/prompts/rspec.md +13 -12
- data/.cursor/prompts/yardoc.md +11 -6
- data/CHANGELOG.md +13 -2
- data/README.md +1 -0
- data/docs/ai_prompts.md +269 -195
- data/docs/basics/call.md +124 -58
- data/docs/basics/chain.md +190 -160
- data/docs/basics/context.md +242 -154
- data/docs/basics/setup.md +302 -32
- data/docs/callbacks.md +390 -94
- data/docs/configuration.md +181 -65
- data/docs/deprecation.md +245 -0
- data/docs/getting_started.md +161 -39
- data/docs/internationalization.md +590 -70
- data/docs/interruptions/exceptions.md +135 -118
- data/docs/interruptions/faults.md +150 -125
- data/docs/interruptions/halt.md +134 -80
- data/docs/logging.md +181 -118
- data/docs/middlewares.md +150 -377
- data/docs/outcomes/result.md +140 -112
- data/docs/outcomes/states.md +134 -99
- data/docs/outcomes/statuses.md +204 -146
- data/docs/parameters/coercions.md +232 -281
- data/docs/parameters/defaults.md +224 -169
- data/docs/parameters/definitions.md +289 -141
- data/docs/parameters/namespacing.md +250 -161
- data/docs/parameters/validations.md +260 -133
- data/docs/testing.md +191 -197
- data/docs/workflows.md +143 -98
- data/lib/cmdx/callback.rb +23 -19
- data/lib/cmdx/callback_registry.rb +1 -3
- data/lib/cmdx/chain_inspector.rb +23 -23
- data/lib/cmdx/chain_serializer.rb +38 -19
- data/lib/cmdx/coercion.rb +20 -12
- data/lib/cmdx/coercion_registry.rb +51 -32
- data/lib/cmdx/configuration.rb +84 -31
- data/lib/cmdx/context.rb +32 -21
- data/lib/cmdx/core_ext/hash.rb +13 -13
- data/lib/cmdx/core_ext/module.rb +1 -1
- data/lib/cmdx/core_ext/object.rb +12 -12
- data/lib/cmdx/correlator.rb +60 -39
- data/lib/cmdx/errors.rb +105 -131
- data/lib/cmdx/fault.rb +66 -45
- data/lib/cmdx/immutator.rb +20 -21
- data/lib/cmdx/lazy_struct.rb +78 -70
- data/lib/cmdx/log_formatters/json.rb +1 -1
- data/lib/cmdx/log_formatters/key_value.rb +1 -1
- data/lib/cmdx/log_formatters/line.rb +1 -1
- data/lib/cmdx/log_formatters/logstash.rb +1 -1
- data/lib/cmdx/log_formatters/pretty_json.rb +1 -1
- data/lib/cmdx/log_formatters/pretty_key_value.rb +1 -1
- data/lib/cmdx/log_formatters/pretty_line.rb +1 -1
- data/lib/cmdx/log_formatters/raw.rb +2 -2
- data/lib/cmdx/logger.rb +19 -14
- data/lib/cmdx/logger_ansi.rb +33 -17
- data/lib/cmdx/logger_serializer.rb +85 -24
- data/lib/cmdx/middleware.rb +39 -21
- data/lib/cmdx/middleware_registry.rb +4 -3
- data/lib/cmdx/parameter.rb +151 -89
- data/lib/cmdx/parameter_inspector.rb +34 -21
- data/lib/cmdx/parameter_registry.rb +36 -30
- data/lib/cmdx/parameter_serializer.rb +21 -14
- data/lib/cmdx/result.rb +136 -135
- data/lib/cmdx/result_ansi.rb +31 -17
- data/lib/cmdx/result_inspector.rb +32 -27
- data/lib/cmdx/result_logger.rb +23 -14
- data/lib/cmdx/result_serializer.rb +65 -27
- data/lib/cmdx/task.rb +234 -113
- data/lib/cmdx/task_deprecator.rb +22 -25
- data/lib/cmdx/task_processor.rb +89 -88
- data/lib/cmdx/task_serializer.rb +27 -14
- data/lib/cmdx/utils/monotonic_runtime.rb +2 -4
- data/lib/cmdx/validator.rb +25 -16
- data/lib/cmdx/validator_registry.rb +53 -31
- data/lib/cmdx/validators/exclusion.rb +1 -1
- data/lib/cmdx/validators/format.rb +2 -2
- data/lib/cmdx/validators/inclusion.rb +2 -2
- data/lib/cmdx/validators/length.rb +2 -2
- data/lib/cmdx/validators/numeric.rb +3 -3
- data/lib/cmdx/validators/presence.rb +2 -2
- data/lib/cmdx/version.rb +1 -1
- data/lib/cmdx/workflow.rb +54 -33
- data/lib/generators/cmdx/task_generator.rb +6 -6
- data/lib/generators/cmdx/workflow_generator.rb +6 -6
- metadata +3 -1
data/docs/basics/context.md
CHANGED
@@ -1,82 +1,127 @@
|
|
1
1
|
# Basics - Context
|
2
2
|
|
3
|
-
|
4
|
-
Built on `LazyStruct`, context enables dynamic attribute access, parameter
|
5
|
-
validation, and seamless data flow between related tasks.
|
3
|
+
Task context provides flexible data storage and sharing for task execution. Built on `LazyStruct`, context enables dynamic attribute access, parameter validation, and seamless data flow between related tasks.
|
6
4
|
|
7
5
|
## Table of Contents
|
8
6
|
|
9
7
|
- [TLDR](#tldr)
|
10
|
-
- [
|
8
|
+
- [Context Fundamentals](#context-fundamentals)
|
11
9
|
- [Accessing Data](#accessing-data)
|
12
10
|
- [Modifying Context](#modifying-context)
|
13
|
-
- [Context Features](#context-features)
|
14
11
|
- [Data Sharing Between Tasks](#data-sharing-between-tasks)
|
15
12
|
- [Result Object Context Passing](#result-object-context-passing)
|
16
|
-
- [Context Inspection](#context-inspection)
|
13
|
+
- [Context Inspection and Debugging](#context-inspection-and-debugging)
|
14
|
+
- [Error Handling](#error-handling)
|
17
15
|
|
18
16
|
## TLDR
|
19
17
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
18
|
+
```ruby
|
19
|
+
# Automatic parameter loading
|
20
|
+
ProcessOrderTask.call(user_id: 123, amount: 99.99)
|
21
|
+
|
22
|
+
# Dynamic attribute access
|
23
|
+
context.user_id # → 123
|
24
|
+
context[:amount] # → 99.99
|
25
|
+
|
26
|
+
# Safe modification
|
27
|
+
context.total = amount * 1.08
|
28
|
+
context.merge!(status: "processed", completed_at: Time.now)
|
29
|
+
|
30
|
+
# Task chaining with context preservation
|
31
|
+
result = ValidateOrderTask.call(context)
|
32
|
+
ProcessPaymentTask.call(result) if result.success?
|
33
|
+
```
|
34
|
+
|
35
|
+
## Context Fundamentals
|
25
36
|
|
26
|
-
|
37
|
+
> [!IMPORTANT]
|
38
|
+
> Context is automatically populated with all parameters passed to a task. Parameters become accessible as dynamic attributes using both method and hash-style access patterns.
|
27
39
|
|
28
|
-
|
29
|
-
|
40
|
+
### Automatic Parameter Loading
|
41
|
+
|
42
|
+
When calling tasks, all parameters automatically become context attributes:
|
30
43
|
|
31
44
|
```ruby
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
45
|
+
class ProcessOrderTask < CMDx::Task
|
46
|
+
required :user_id, type: :integer
|
47
|
+
required :amount, type: :float
|
48
|
+
optional :currency, default: "USD"
|
49
|
+
|
50
|
+
def call
|
51
|
+
# Parameters automatically available in context
|
52
|
+
context.user_id # → 123
|
53
|
+
context.amount # → 99.99
|
54
|
+
context.currency # → "USD"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
ProcessOrderTask.call(user_id: 123, amount: 99.99)
|
37
59
|
```
|
38
60
|
|
39
|
-
|
61
|
+
### Key Normalization
|
40
62
|
|
41
|
-
|
42
|
-
normalization to symbols:
|
63
|
+
All keys are automatically normalized to symbols for consistent access:
|
43
64
|
|
44
65
|
```ruby
|
45
|
-
|
66
|
+
# String and symbol keys both work
|
67
|
+
ProcessOrderTask.call("user_id" => 123, :amount => 99.99)
|
68
|
+
|
69
|
+
# Both accessible as symbols
|
70
|
+
context.user_id # → 123
|
71
|
+
context.amount # → 99.99
|
72
|
+
```
|
73
|
+
|
74
|
+
## Accessing Data
|
46
75
|
|
76
|
+
Context provides multiple access patterns with automatic nil safety:
|
77
|
+
|
78
|
+
```ruby
|
79
|
+
class ProcessOrderTask < CMDx::Task
|
47
80
|
def call
|
48
81
|
# Method-style access (preferred)
|
49
|
-
context.user_id
|
50
|
-
context.
|
82
|
+
user_id = context.user_id
|
83
|
+
amount = context.amount
|
51
84
|
|
52
85
|
# Hash-style access
|
53
|
-
context[:order_id]
|
54
|
-
context["
|
86
|
+
order_id = context[:order_id]
|
87
|
+
metadata = context["metadata"]
|
55
88
|
|
56
89
|
# Safe access with defaults
|
57
|
-
context.fetch!(:priority, "normal")
|
90
|
+
priority = context.fetch!(:priority, "normal")
|
91
|
+
source = context.dig(:metadata, :source)
|
58
92
|
|
59
|
-
#
|
60
|
-
|
61
|
-
|
62
|
-
# Alias for shorter code
|
63
|
-
ctx.user_id #=> 123 (ctx is alias for context)
|
93
|
+
# Shorter alias
|
94
|
+
total = ctx.amount * ctx.tax_rate # ctx aliases context
|
64
95
|
end
|
65
|
-
|
66
96
|
end
|
67
97
|
```
|
68
98
|
|
99
|
+
> [!NOTE]
|
100
|
+
> Accessing undefined attributes returns `nil` instead of raising errors, enabling graceful handling of optional parameters.
|
101
|
+
|
102
|
+
### Type Safety
|
103
|
+
|
104
|
+
Context accepts any data type without restrictions:
|
105
|
+
|
106
|
+
```ruby
|
107
|
+
context.string_value = "Order #12345"
|
108
|
+
context.numeric_value = 42
|
109
|
+
context.array_value = [1, 2, 3]
|
110
|
+
context.hash_value = { total: 99.99, tax: 8.99 }
|
111
|
+
context.object_value = User.find(123)
|
112
|
+
context.timestamp = Time.now
|
113
|
+
```
|
114
|
+
|
69
115
|
## Modifying Context
|
70
116
|
|
71
117
|
Context supports dynamic modification during task execution:
|
72
118
|
|
73
119
|
```ruby
|
74
|
-
class
|
75
|
-
|
120
|
+
class ProcessOrderTask < CMDx::Task
|
76
121
|
def call
|
77
122
|
# Direct assignment
|
78
|
-
context.user = User.find(user_id)
|
79
|
-
context.order = Order.find(order_id)
|
123
|
+
context.user = User.find(context.user_id)
|
124
|
+
context.order = Order.find(context.order_id)
|
80
125
|
context.processed_at = Time.now
|
81
126
|
|
82
127
|
# Hash-style assignment
|
@@ -89,185 +134,228 @@ class ProcessUserOrderTask < CMDx::Task
|
|
89
134
|
# Batch updates
|
90
135
|
context.merge!(
|
91
136
|
status: "completed",
|
92
|
-
|
137
|
+
total_amount: calculate_total,
|
93
138
|
completion_time: Time.now
|
94
139
|
)
|
95
140
|
|
96
|
-
#
|
97
|
-
context.delete!(:
|
141
|
+
# Remove sensitive data
|
142
|
+
context.delete!(:credit_card_number)
|
98
143
|
end
|
99
144
|
|
145
|
+
private
|
146
|
+
|
147
|
+
def calculate_total
|
148
|
+
context.amount + (context.amount * context.tax_rate)
|
149
|
+
end
|
100
150
|
end
|
101
151
|
```
|
102
152
|
|
103
153
|
> [!TIP]
|
104
|
-
> Use context for both input parameters and intermediate results. This creates
|
105
|
-
> a natural data flow through your task execution pipeline.
|
154
|
+
> Use context for both input parameters and intermediate results. This creates natural data flow through your task execution pipeline.
|
106
155
|
|
107
|
-
##
|
156
|
+
## Data Sharing Between Tasks
|
108
157
|
|
109
|
-
|
158
|
+
Context enables seamless data flow between related tasks in complex workflows:
|
110
159
|
|
111
|
-
|
160
|
+
### Task Composition
|
112
161
|
|
113
162
|
```ruby
|
114
|
-
|
115
|
-
|
116
|
-
#
|
117
|
-
context
|
118
|
-
|
119
|
-
```
|
163
|
+
class ProcessOrderWorkflowTask < CMDx::Task
|
164
|
+
def call
|
165
|
+
# Validate order data
|
166
|
+
validation_result = ValidateOrderTask.call(context)
|
167
|
+
throw!(validation_result) unless validation_result.success?
|
120
168
|
|
121
|
-
|
169
|
+
# Process payment with enriched context
|
170
|
+
payment_result = ProcessPaymentTask.call(context)
|
171
|
+
throw!(payment_result) unless payment_result.success?
|
122
172
|
|
123
|
-
|
173
|
+
# Send notifications with complete context
|
174
|
+
NotifyOrderProcessedTask.call(context)
|
124
175
|
|
125
|
-
|
126
|
-
context.
|
127
|
-
context
|
176
|
+
# Context now contains accumulated data from all tasks
|
177
|
+
context.order_validated # → true (from validation)
|
178
|
+
context.payment_processed # → true (from payment)
|
179
|
+
context.notification_sent # → true (from notification)
|
180
|
+
end
|
181
|
+
end
|
128
182
|
```
|
129
183
|
|
130
|
-
|
131
|
-
> Context attributes that are **NOT** loaded will return `nil` rather than
|
132
|
-
> raising an error. This allows for graceful handling of optional parameters.
|
184
|
+
### Workflow Chains
|
133
185
|
|
134
|
-
|
186
|
+
```ruby
|
187
|
+
# Initialize workflow context
|
188
|
+
initial_data = { user_id: 123, product_ids: [1, 2, 3] }
|
135
189
|
|
136
|
-
|
190
|
+
# Chain tasks with context flow
|
191
|
+
validation_result = ValidateCartTask.call(initial_data)
|
137
192
|
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
context.proc_value = -> { "dynamic value" }
|
193
|
+
if validation_result.success?
|
194
|
+
# Context accumulates data through the chain
|
195
|
+
inventory_result = CheckInventoryTask.call(validation_result.context)
|
196
|
+
payment_result = ProcessPaymentTask.call(inventory_result.context)
|
197
|
+
shipping_result = CreateShipmentTask.call(payment_result.context)
|
198
|
+
end
|
145
199
|
```
|
146
200
|
|
147
|
-
##
|
201
|
+
## Result Object Context Passing
|
148
202
|
|
149
|
-
|
150
|
-
|
203
|
+
> [!IMPORTANT]
|
204
|
+
> CMDx automatically extracts context when Result objects are passed to task methods, enabling powerful workflow compositions where task output becomes the next task's input.
|
151
205
|
|
152
|
-
|
206
|
+
```ruby
|
207
|
+
# Seamless task chaining
|
208
|
+
extraction_result = ExtractDataTask.call(source_id: 123)
|
209
|
+
processing_result = ProcessDataTask.call(extraction_result)
|
210
|
+
|
211
|
+
# Context flows automatically between tasks
|
212
|
+
processing_result.context.source_id # → 123 (from first task)
|
213
|
+
processing_result.context.extracted_records # → [...] (from first task)
|
214
|
+
processing_result.context.processed_count # → 50 (from second task)
|
215
|
+
```
|
216
|
+
|
217
|
+
### Error Propagation in Chains
|
153
218
|
|
154
219
|
```ruby
|
155
|
-
|
220
|
+
# Non-raising chain with error handling
|
221
|
+
extraction_result = ExtractDataTask.call(source_id: 123)
|
156
222
|
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
223
|
+
if extraction_result.failed?
|
224
|
+
# Context preserved even in failure scenarios
|
225
|
+
error_handler_result = HandleExtractionErrorTask.call(extraction_result)
|
226
|
+
return error_handler_result
|
227
|
+
end
|
161
228
|
|
162
|
-
|
163
|
-
|
229
|
+
# Continue processing with successful result
|
230
|
+
ProcessDataTask.call(extraction_result)
|
231
|
+
```
|
164
232
|
|
165
|
-
|
166
|
-
context.order_validated #=> true (from ValidateUserOrderTask)
|
167
|
-
context.payment_processed #=> true (from ProcessOrderPaymentTask)
|
168
|
-
end
|
233
|
+
### Exception-Based Chains
|
169
234
|
|
235
|
+
```ruby
|
236
|
+
begin
|
237
|
+
# Raising version propagates exceptions while preserving context
|
238
|
+
extraction_result = ExtractDataTask.call!(source_id: 123)
|
239
|
+
processing_result = ProcessDataTask.call!(extraction_result)
|
240
|
+
notification_result = NotifyCompletionTask.call!(processing_result)
|
241
|
+
rescue CMDx::Failed => e
|
242
|
+
# Access failed task's context for error analysis
|
243
|
+
ErrorReportingTask.call(
|
244
|
+
error: e.message,
|
245
|
+
failed_context: e.result.context,
|
246
|
+
user_id: e.result.context.user_id
|
247
|
+
)
|
170
248
|
end
|
171
249
|
```
|
172
250
|
|
173
|
-
|
251
|
+
## Context Inspection and Debugging
|
252
|
+
|
253
|
+
Context provides comprehensive inspection capabilities for debugging and logging:
|
174
254
|
|
175
255
|
```ruby
|
176
|
-
|
177
|
-
|
256
|
+
class DebuggableTask < CMDx::Task
|
257
|
+
def call
|
258
|
+
# Log current context state
|
259
|
+
Rails.logger.info "Context: #{context.inspect}"
|
178
260
|
|
179
|
-
|
180
|
-
|
181
|
-
|
261
|
+
# Convert to hash for serialization
|
262
|
+
context_data = context.to_h
|
263
|
+
# → { user_id: 123, amount: 99.99, status: "processing" }
|
182
264
|
|
183
|
-
|
184
|
-
|
265
|
+
# Iterate over context data
|
266
|
+
context.each_pair do |key, value|
|
267
|
+
puts "#{key}: #{value.class} = #{value}"
|
268
|
+
end
|
269
|
+
|
270
|
+
# Check for specific keys
|
271
|
+
has_user = context.key?(:user_id) # → true
|
272
|
+
has_admin = context.key?(:admin_mode) # → false
|
273
|
+
end
|
185
274
|
end
|
186
275
|
```
|
187
276
|
|
188
|
-
###
|
277
|
+
### Production Logging
|
189
278
|
|
190
279
|
```ruby
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
action: "bulk_order_processing"
|
195
|
-
}
|
196
|
-
|
197
|
-
results = [
|
198
|
-
ValidateOrderDataTask.call(initial_context),
|
199
|
-
ProcessOrderPaymentTask.call(initial_context),
|
200
|
-
UpdateInventoryTask.call(initial_context)
|
201
|
-
]
|
202
|
-
|
203
|
-
# All tasks share and modify the same context data
|
204
|
-
results.first.context.validation_completed #=> true
|
205
|
-
results.last.context.inventory_updated #=> true
|
206
|
-
```
|
280
|
+
class OrderProcessingTask < CMDx::Task
|
281
|
+
def call
|
282
|
+
log_context_snapshot("start")
|
207
283
|
|
208
|
-
|
284
|
+
process_order
|
209
285
|
|
210
|
-
|
211
|
-
|
212
|
-
one task becomes the input for the next, creating powerful workflow compositions.
|
286
|
+
log_context_snapshot("complete")
|
287
|
+
end
|
213
288
|
|
214
|
-
|
215
|
-
# Chain tasks by passing Result objects
|
216
|
-
extraction_result = ExtractDataTask.call(source_id: 123)
|
217
|
-
processing_result = ProcessDataTask.call(extraction_result)
|
289
|
+
private
|
218
290
|
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
291
|
+
def log_context_snapshot(stage)
|
292
|
+
Rails.logger.info({
|
293
|
+
stage: stage,
|
294
|
+
task: self.class.name,
|
295
|
+
context: context.to_h.except(:sensitive_data)
|
296
|
+
}.to_json)
|
297
|
+
end
|
298
|
+
end
|
225
299
|
```
|
226
300
|
|
227
|
-
|
301
|
+
## Error Handling
|
302
|
+
|
303
|
+
> [!WARNING]
|
304
|
+
> Context operations are generally safe, but understanding error scenarios helps build robust applications.
|
228
305
|
|
229
|
-
|
306
|
+
### Safe Access Patterns
|
230
307
|
|
231
308
|
```ruby
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
error_result = HandleErrorTask.call(extraction_result)
|
237
|
-
end
|
309
|
+
class RobustTask < CMDx::Task
|
310
|
+
def call
|
311
|
+
# Safe: returns nil for missing attributes
|
312
|
+
user_id = context.user_id || 'anonymous'
|
238
313
|
|
239
|
-
#
|
240
|
-
|
241
|
-
extraction_result = ExtractDataTask.call!(source_id: 123)
|
242
|
-
processing_result = ProcessDataTask.call!(extraction_result)
|
243
|
-
rescue CMDx::Failed => e
|
244
|
-
# Handle any failure in the chain
|
245
|
-
error_result = HandleErrorTask.call(e.result)
|
246
|
-
end
|
247
|
-
```
|
314
|
+
# Safe: fetch with default
|
315
|
+
timeout = context.fetch!(:timeout, 30)
|
248
316
|
|
249
|
-
|
250
|
-
|
251
|
-
> processing, where multiple tasks can operate on shared context while maintaining
|
252
|
-
> individual result tracking.
|
317
|
+
# Safe: deep access with nil protection
|
318
|
+
api_key = context.dig(:credentials, :api_key)
|
253
319
|
|
254
|
-
|
320
|
+
# Safe: conditional assignment
|
321
|
+
context.processed_at ||= Time.now
|
322
|
+
end
|
323
|
+
end
|
324
|
+
```
|
255
325
|
|
256
|
-
|
326
|
+
### Common Error Scenarios
|
257
327
|
|
258
328
|
```ruby
|
259
|
-
#
|
260
|
-
|
329
|
+
# Missing required context data
|
330
|
+
class PaymentTask < CMDx::Task
|
331
|
+
def call
|
332
|
+
# Check for required context before proceeding
|
333
|
+
unless context.user_id && context.amount
|
334
|
+
context.error_message = "Missing required payment data"
|
335
|
+
fail!(reason: "Cannot process payment")
|
336
|
+
end
|
261
337
|
|
262
|
-
|
263
|
-
|
338
|
+
process_payment
|
339
|
+
end
|
340
|
+
end
|
264
341
|
|
265
|
-
#
|
266
|
-
|
342
|
+
# Invalid context modifications
|
343
|
+
class ValidationTask < CMDx::Task
|
344
|
+
def call
|
345
|
+
# Context cannot be replaced entirely
|
346
|
+
# context = {} # This won't work as expected
|
347
|
+
|
348
|
+
# Instead, clear individual keys or use merge!
|
349
|
+
context.delete!(:temporary_data)
|
350
|
+
context.merge!(validation_status: "complete")
|
351
|
+
end
|
352
|
+
end
|
267
353
|
```
|
268
354
|
|
269
|
-
[
|
270
|
-
|
355
|
+
> [!TIP]
|
356
|
+
> Use context inspection methods liberally during development and testing. The `to_h` method is particularly useful for logging and debugging complex workflows.
|
357
|
+
|
358
|
+
[Learn more](../../lib/cmdx/lazy_struct.rb) about the `LazyStruct` implementation that powers context functionality.
|
271
359
|
|
272
360
|
---
|
273
361
|
|