cmdx 1.11.0 → 1.13.0

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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/.cursor/rules/cursor-instructions.mdc +8 -0
  3. data/.yard-lint.yml +174 -0
  4. data/CHANGELOG.md +24 -0
  5. data/docs/attributes/definitions.md +5 -1
  6. data/docs/attributes/validations.md +47 -6
  7. data/docs/basics/execution.md +56 -0
  8. data/docs/basics/setup.md +26 -3
  9. data/docs/deprecation.md +0 -2
  10. data/docs/getting_started.md +11 -0
  11. data/docs/logging.md +6 -10
  12. data/docs/outcomes/result.md +12 -8
  13. data/docs/outcomes/states.md +4 -4
  14. data/docs/outcomes/statuses.md +6 -6
  15. data/docs/tips_and_tricks.md +4 -0
  16. data/examples/active_record_database_transaction.md +27 -0
  17. data/examples/flipper_feature_flags.md +50 -0
  18. data/examples/redis_idempotency.md +71 -0
  19. data/examples/sentry_error_tracking.md +46 -0
  20. data/lib/cmdx/attribute.rb +6 -0
  21. data/lib/cmdx/callback_registry.rb +2 -1
  22. data/lib/cmdx/chain.rb +20 -7
  23. data/lib/cmdx/coercion_registry.rb +2 -1
  24. data/lib/cmdx/coercions/boolean.rb +3 -3
  25. data/lib/cmdx/coercions/complex.rb +1 -0
  26. data/lib/cmdx/coercions/string.rb +1 -0
  27. data/lib/cmdx/coercions/symbol.rb +1 -0
  28. data/lib/cmdx/configuration.rb +1 -3
  29. data/lib/cmdx/context.rb +5 -2
  30. data/lib/cmdx/executor.rb +29 -24
  31. data/lib/cmdx/middleware_registry.rb +1 -0
  32. data/lib/cmdx/result.rb +32 -89
  33. data/lib/cmdx/task.rb +10 -11
  34. data/lib/cmdx/utils/call.rb +3 -1
  35. data/lib/cmdx/utils/condition.rb +0 -3
  36. data/lib/cmdx/utils/format.rb +1 -1
  37. data/lib/cmdx/validators/exclusion.rb +2 -0
  38. data/lib/cmdx/validators/inclusion.rb +2 -0
  39. data/lib/cmdx/validators/length.rb +6 -0
  40. data/lib/cmdx/validators/numeric.rb +6 -0
  41. data/lib/cmdx/version.rb +1 -1
  42. data/lib/generators/cmdx/locale_generator.rb +6 -5
  43. data/mkdocs.yml +5 -1
  44. metadata +20 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4a9b1cdaccb648dcffd8c5b50417b2891d2c263f922884b4e8163aa8389a7602
4
- data.tar.gz: 7ab3292c7441d0a279c1609e3976c7599ae10ae4168b22aae027667578d0352a
3
+ metadata.gz: 0b1cd7841d40aeac40b6eb36a7ff87a5ace03deae3496e1aef5cb10e34f8f42d
4
+ data.tar.gz: 303333d4635690575ceda40a2564be417b47f5ec4c2992af542c509710289ae3
5
5
  SHA512:
6
- metadata.gz: a3c544688dd90eb3e117460f591cd1d85dd3590a1aae8c90e37930c2728ecab5b1ff8ccce06be2cb894627d5d7beca7edebfa313667879e863c0e5338dc7bc29
7
- data.tar.gz: a0b6e9c99fc46d3b4bbc627d9967acb8e39fe0fcf1ff51dcc3ab841acabc7b6d6e1bacf3cbaf4a08f0fc9381b409b2829d5f830ecadeefaab9d84647ce28c7cb
6
+ metadata.gz: 18cde5cf4b6c7fa974ffb7a3827e09d0a5c2ac5f3c2c01427abbc94de71ef1bc03aee78e25032900761f09c8d39e89e52a4ea1d5fa33e07c86f060b483fe839f
7
+ data.tar.gz: 5ed05ecdb682ae160be47cf6399c76081486011b6143f95f9884a06b588474ba64e3e000efd72ba2dcd6abb343e3e12985d2691ca15ee7575873f90daff6e70f
@@ -17,6 +17,11 @@ Reference the CMDx documentation in https://github.com/drexed/cmdx/blob/main/LLM
17
17
  - Ruby 3.4+
18
18
  - RSpec 3.1+
19
19
 
20
+ ## Development Guidelines
21
+ - Performance is critical - benchmark any changes that could affect speed
22
+ - Follow existing code patterns and conventions
23
+ - Maintain backward compatibility for public API
24
+
20
25
  ## Code Style and Structure
21
26
  - Write concise, idiomatic Ruby code with accurate examples
22
27
  - Follow Ruby conventions and best practices
@@ -24,6 +29,7 @@ Reference the CMDx documentation in https://github.com/drexed/cmdx/blob/main/LLM
24
29
  - Prefer iteration and modularization over code duplication
25
30
  - Use descriptive variable and method names (e.g., user_signed_in?, calculate_total)
26
31
  - Write comprehensive code documentation using the Yardoc format
32
+ - Minimize object allocations in hot paths
27
33
 
28
34
  ## Naming Conventions
29
35
  - Use snake_case for file names, method names, and variables
@@ -35,6 +41,7 @@ Reference the CMDx documentation in https://github.com/drexed/cmdx/blob/main/LLM
35
41
  - Use Ruby's expressive syntax (e.g., unless, ||=, &.)
36
42
  - Prefer double quotes for strings
37
43
  - Respect my Rubocop options
44
+ - Run `bundle exec rubocop .` before finalizing any code changes
38
45
 
39
46
  ## Performance Optimization
40
47
  - Use memoization for expensive operations
@@ -50,6 +57,7 @@ Reference the CMDx documentation in https://github.com/drexed/cmdx/blob/main/LLM
50
57
  - Don't test declarative configuration
51
58
  - Use appropriate matchers
52
59
  - Update tests and update Yardocs after you write code
60
+ - Run `bundle rspec .` before finalizing any code changes
53
61
 
54
62
  ## Documentation
55
63
  - Utilize the YARDoc format when documenting Ruby code
data/.yard-lint.yml ADDED
@@ -0,0 +1,174 @@
1
+ # YARD-Lint Configuration
2
+ # See https://github.com/mensfeld/yard-lint for documentation
3
+
4
+ # Global settings for all validators
5
+ AllValidators:
6
+ # YARD command-line options (applied to all validators by default)
7
+ YardOptions:
8
+ - --private
9
+ - --protected
10
+
11
+ # Global file exclusion patterns
12
+ Exclude:
13
+ - '\.git'
14
+ - 'vendor/**/*'
15
+ - 'node_modules/**/*'
16
+ - 'spec/**/*'
17
+ - 'test/**/*'
18
+
19
+ # Exit code behavior (error, warning, convention, never)
20
+ FailOnSeverity: warning
21
+
22
+ # Minimum documentation coverage percentage (0-100)
23
+ # Fails if coverage is below this threshold
24
+ # MinCoverage: 80.0
25
+
26
+ # Diff mode settings
27
+ DiffMode:
28
+ # Default base ref for --diff (auto-detects main/master if not specified)
29
+ DefaultBaseRef: ~
30
+
31
+ # Documentation validators
32
+ Documentation/UndocumentedObjects:
33
+ Description: 'Checks for classes, modules, and methods without documentation.'
34
+ Enabled: false
35
+ Severity: warning
36
+ ExcludedMethods:
37
+ - 'initialize/0' # Exclude parameter-less initialize
38
+ - '/^_/' # Exclude private methods (by convention)
39
+
40
+ Documentation/UndocumentedMethodArguments:
41
+ Description: 'Checks for method parameters without @param tags.'
42
+ Enabled: true
43
+ Severity: warning
44
+
45
+ Documentation/UndocumentedBooleanMethods:
46
+ Description: 'Checks that question mark methods document their boolean return.'
47
+ Enabled: true
48
+ Severity: warning
49
+
50
+ Documentation/UndocumentedOptions:
51
+ Description: 'Detects methods with options hash parameters but no @option tags.'
52
+ Enabled: true
53
+ Severity: warning
54
+
55
+ Documentation/MarkdownSyntax:
56
+ Description: 'Detects common markdown syntax errors in documentation.'
57
+ Enabled: true
58
+ Severity: warning
59
+
60
+ # Tags validators
61
+ Tags/Order:
62
+ Description: 'Enforces consistent ordering of YARD tags.'
63
+ Enabled: true
64
+ Severity: convention
65
+ EnforcedOrder:
66
+ - param
67
+ - option
68
+ - return
69
+ - raise
70
+ - example
71
+
72
+ Tags/InvalidTypes:
73
+ Description: 'Validates type definitions in @param, @return, @option tags.'
74
+ Enabled: true
75
+ Severity: warning
76
+ ValidatedTags:
77
+ - param
78
+ - option
79
+ - return
80
+
81
+ Tags/TypeSyntax:
82
+ Description: 'Validates YARD type syntax using YARD parser.'
83
+ Enabled: true
84
+ Severity: warning
85
+ ValidatedTags:
86
+ - param
87
+ - option
88
+ - return
89
+ - yieldreturn
90
+
91
+ Tags/MeaninglessTag:
92
+ Description: 'Detects @param/@option tags on classes, modules, or constants.'
93
+ Enabled: true
94
+ Severity: warning
95
+ CheckedTags:
96
+ - param
97
+ - option
98
+ InvalidObjectTypes:
99
+ - class
100
+ - module
101
+ - constant
102
+
103
+ Tags/CollectionType:
104
+ Description: 'Validates Hash collection syntax consistency.'
105
+ Enabled: true
106
+ Severity: convention
107
+ EnforcedStyle: long # 'long' for Hash{K => V} (YARD standard), 'short' for {K => V}
108
+ ValidatedTags:
109
+ - param
110
+ - option
111
+ - return
112
+ - yieldreturn
113
+
114
+ Tags/TagTypePosition:
115
+ Description: 'Validates type annotation position in tags.'
116
+ Enabled: true
117
+ Severity: convention
118
+ CheckedTags:
119
+ - param
120
+ - option
121
+ # EnforcedStyle: 'type_after_name' (YARD standard: @param name [Type])
122
+ # or 'type_first' (@param [Type] name)
123
+ EnforcedStyle: type_after_name
124
+
125
+ Tags/ApiTags:
126
+ Description: 'Enforces @api tags on public objects.'
127
+ Enabled: false # Opt-in validator
128
+ Severity: warning
129
+ AllowedApis:
130
+ - public
131
+ - private
132
+ - internal
133
+
134
+ Tags/OptionTags:
135
+ Description: 'Requires @option tags for methods with options parameters.'
136
+ Enabled: true
137
+ Severity: warning
138
+
139
+ # Warnings validators - catches YARD parser errors
140
+ Warnings/UnknownTag:
141
+ Description: 'Detects unknown YARD tags.'
142
+ Enabled: false
143
+ Severity: error
144
+
145
+ Warnings/UnknownDirective:
146
+ Description: 'Detects unknown YARD directives.'
147
+ Enabled: true
148
+ Severity: error
149
+
150
+ Warnings/InvalidTagFormat:
151
+ Description: 'Detects malformed tag syntax.'
152
+ Enabled: true
153
+ Severity: error
154
+
155
+ Warnings/InvalidDirectiveFormat:
156
+ Description: 'Detects malformed directive syntax.'
157
+ Enabled: true
158
+ Severity: error
159
+
160
+ Warnings/DuplicatedParameterName:
161
+ Description: 'Detects duplicate @param tags.'
162
+ Enabled: true
163
+ Severity: error
164
+
165
+ Warnings/UnknownParameterName:
166
+ Description: 'Detects @param tags for non-existent parameters.'
167
+ Enabled: true
168
+ Severity: error
169
+
170
+ # Semantic validators
171
+ Semantic/AbstractMethods:
172
+ Description: 'Ensures @abstract methods do not have real implementations.'
173
+ Enabled: true
174
+ Severity: warning
data/CHANGELOG.md CHANGED
@@ -6,6 +6,30 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
 
7
7
  ## [UNRELEASED]
8
8
 
9
+ ## [1.13.0] - 2025-12-23
10
+
11
+ ### Added
12
+
13
+ - Added task execution rollback tracking and logging
14
+ - Added `dry_run` option to task execution with inheritance support for nested tasks
15
+ - Added context `delete` alias for `delete!`
16
+ - Added context `merge` alias for `merge!`
17
+
18
+ ## [1.12.0] - 2025-12-18
19
+
20
+ ### Added
21
+ - Added active record database transaction example
22
+ - Added Sentry error tracking example
23
+ - Added Redis idempotency example
24
+ - Added Flipper feature flag example
25
+
26
+ ### Updated
27
+ - Remove `handle_*` methods and provide `on(*states_or_statuses)` method for more flexibility
28
+ - Optimize logging ancestor lookup
29
+ - Use chop instead of range for better string performance
30
+ - Update boolean coercion `TRUTHY` and `FALSEY` regexp to be case insensitive
31
+ - Improve YARD documentation with `yard-lint`
32
+
9
33
  ## [1.11.0] - 2025-11-08
10
34
 
11
35
  ### Updated
@@ -4,6 +4,10 @@ Attributes define your task's interface with automatic validation, type coercion
4
4
 
5
5
  ## Declarations
6
6
 
7
+ !!! warning "Important"
8
+
9
+ Attributes are order-dependent, so if you need to reference them as a source or use them in conditions, make sure they’re defined in the correct order.
10
+
7
11
  !!! tip
8
12
 
9
13
  Prefer using the `required` and `optional` alias for `attributes` for brevity and to clearly signal intent.
@@ -54,7 +58,7 @@ class PublishArticle < CMDx::Task
54
58
 
55
59
  # Conditionally required
56
60
  required :publisher, if: :magazine?
57
- required :approver, unless: proc { status == :published }
61
+ attribute :approver, required: true, unless: proc { status == :published }
58
62
 
59
63
  def work
60
64
  title #=> "Getting Started with Ruby"
@@ -14,10 +14,10 @@ class ProcessSubscription < CMDx::Task
14
14
  attribute :user_id, presence: true
15
15
 
16
16
  # String with length constraints
17
- attribute :preferences, length: { minimum: 10, maximum: 500 }
17
+ optional :preferences, length: { minimum: 10, maximum: 500 }
18
18
 
19
19
  # Numeric range validation
20
- attribute :tier_level, inclusion: { in: 1..5 }
20
+ required :tier_level, inclusion: { in: 1..5 }
21
21
 
22
22
  # Format validation for email
23
23
  attribute :contact_email, format: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
@@ -46,6 +46,42 @@ ProcessSubscription.execute(
46
46
 
47
47
  ### Common Options
48
48
 
49
+ ```ruby
50
+ class ProcessProduct < CMDx::Task
51
+ # Allow nil
52
+ attribute :tier_level, inclusion: {
53
+ in: 1..5,
54
+ allow_nil: true
55
+ }
56
+
57
+ # Conditionals
58
+ optional :contact_email, format: {
59
+ with: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i,
60
+ if: ->(value) { value.includes?("@") }
61
+ }
62
+ required :status, exclusion: {
63
+ in: %w[recalled archived],
64
+ unless: :product_sunsetted?
65
+ }
66
+
67
+ # Custom message
68
+ attribute :title, length: {
69
+ within: 5..100,
70
+ message: "must be in optimal size"
71
+ }
72
+
73
+ def work
74
+ # Your logic here...
75
+ end
76
+
77
+ private
78
+
79
+ def product_defunct?(value)
80
+ context.company.out_of_business? || value == "deprecated"
81
+ end
82
+ end
83
+ ```
84
+
49
85
  This list of options is available to all validators:
50
86
 
51
87
  | Option | Description |
@@ -261,10 +297,15 @@ Validation failures provide detailed, structured error messages:
261
297
 
262
298
  ```ruby
263
299
  class CreateProject < CMDx::Task
264
- attribute :project_name, presence: true, length: { minimum: 3, maximum: 50 }
265
- attribute :budget, numeric: { greater_than: 1000, less_than: 1000000 }
266
- attribute :priority, inclusion: { in: [:low, :medium, :high] }
267
- attribute :contact_email, format: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
300
+ attribute :project_name,
301
+ presence: true,
302
+ length: { minimum: 3, maximum: 50 }
303
+ optional :budget,
304
+ numeric: { greater_than: 1000, less_than: 1000000 }
305
+ required :priority,
306
+ inclusion: { in: [:low, :medium, :high] }
307
+ attribute :contact_email,
308
+ format: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
268
309
 
269
310
  def work
270
311
  # Your logic here...
@@ -11,6 +11,33 @@ Both methods return results, but handle failures differently:
11
11
  | `execute` | Always returns `CMDx::Result` | Never raises | Predictable result handling |
12
12
  | `execute!` | Returns `CMDx::Result` on success | Raises `CMDx::Fault` when skipped or failed | Exception-based control flow |
13
13
 
14
+ ```mermaid
15
+ flowchart LR
16
+ subgraph Methods
17
+ E[execute]
18
+ EB[execute!]
19
+ end
20
+
21
+ subgraph Returns [Returns CMDx::Result]
22
+ Success
23
+ Failed
24
+ Skipped
25
+ end
26
+
27
+ subgraph Raises [Raises CMDx::Fault]
28
+ FailFault
29
+ SkipFault
30
+ end
31
+
32
+ E --> Success
33
+ E --> Failed
34
+ E --> Skipped
35
+
36
+ EB --> Success
37
+ EB --> FailFault
38
+ EB --> SkipFault
39
+ ```
40
+
14
41
  ## Non-bang Execution
15
42
 
16
43
  Always returns a `CMDx::Result`, never raises exceptions. Perfect for most use cases.
@@ -94,3 +121,32 @@ result.chain #=> Task execution chain
94
121
  result.context #=> Context with all task data
95
122
  result.metadata #=> Hash with execution metadata
96
123
  ```
124
+
125
+ ## Dry Run
126
+
127
+ Execute tasks in dry-run mode to simulate execution without performing side effects. Pass `dry_run: true` in the context when initializing or executing the task.
128
+
129
+ Inside your task, use the `dry_run?` method to conditionally skip side effects.
130
+
131
+ ```ruby
132
+ class CloseStripeCard < CMDx::Task
133
+ def work
134
+ context.stripe_result =
135
+ if dry_run?
136
+ FactoryBot.build(:stripe_closed_card)
137
+ else
138
+ StripeApi.close_card(context.card_id)
139
+ end
140
+ end
141
+ end
142
+
143
+ # Execute in dry-run mode
144
+ result = CloseStripeCard.execute(card_id: "card_abc123", dry_run: true)
145
+ result.success? # => true
146
+
147
+ # FactoryBot object
148
+ result.context.stripe_result = {
149
+ card_id: "card_abc123",
150
+ status: "closed"
151
+ }
152
+ ```
data/docs/basics/setup.md CHANGED
@@ -29,13 +29,14 @@ IncompleteTask.execute #=> raises CMDx::UndefinedMethodError
29
29
  Undo any operations linked to the given status, helping to restore a pristine state.
30
30
 
31
31
  ```ruby
32
- class ValidateDocument < CMDx::Task
32
+ class ChargeCard < CMDx::Task
33
33
  def work
34
- # Your logic here...
34
+ # Your logic here, ex: charge $100
35
35
  end
36
36
 
37
+ # Called automatically if a later step in the workflow fails
37
38
  def rollback
38
- # Your undo logic...
39
+ # Your undo logic, ex: void $100 charge
39
40
  end
40
41
  end
41
42
  ```
@@ -70,6 +71,28 @@ end
70
71
 
71
72
  Tasks follow a predictable execution pattern:
72
73
 
74
+ ```mermaid
75
+ stateDiagram-v2
76
+ Initialized: Instantiation
77
+ Initialized --> Validating: execute
78
+ Validating --> Executing: Valid?
79
+ Validating --> Failed: Invalid
80
+ Executing --> Success: Work done
81
+ Executing --> Skipped: skip!
82
+ Executing --> Failed: fail! / Exception
83
+ Executed
84
+
85
+ state Executed {
86
+ Success
87
+ Skipped
88
+ Failed
89
+ Rollback
90
+
91
+ Skipped --> Rollback
92
+ Failed --> Rollback
93
+ }
94
+ ```
95
+
73
96
  !!! danger "Caution"
74
97
 
75
98
  Tasks are single-use objects. Once executed, they're frozen and immutable.
data/docs/deprecation.md CHANGED
@@ -32,8 +32,6 @@ Allow execution while tracking deprecation in logs. Ideal for gradual migrations
32
32
  ```ruby
33
33
  class ProcessLegacyFormat < CMDx::Task
34
34
  settings(deprecated: :log)
35
-
36
- # Same
37
35
  settings(deprecated: true)
38
36
 
39
37
  def work
@@ -43,6 +43,13 @@ If not using Rails, manually copy the [configuration file](https://github.com/dr
43
43
 
44
44
  CMDx embraces the Compose, Execute, React, Observe (CERO, pronounced "zero") pattern—a simple yet powerful approach to building reliable business logic.
45
45
 
46
+ ```mermaid
47
+ flowchart LR
48
+ Compose --> Execute
49
+ Execute --> React
50
+ Execute -.-> Observe
51
+ ```
52
+
46
53
  ### Compose
47
54
 
48
55
  Build reusable, single-responsibility tasks with typed attributes, validation, and callbacks. Tasks can be chained together in workflows to create complex business processes from simple building blocks.
@@ -93,6 +100,10 @@ I, [2022-07-17T18:43:15.000000 #3784] INFO -- CMDx:
93
100
  index=0 chain_id="018c2b95-b764-7615-a924-cc5b910ed1e5" type="Task" class="AnalyzeMetrics" state="complete" status="success" metadata={runtime: 187}
94
101
  ```
95
102
 
103
+ !!! note
104
+
105
+ This represents a log-only event-sourcing approach, enabling full traceability and a complete, time-ordered view of system behavior.
106
+
96
107
  ## Task Generator
97
108
 
98
109
  Generate new CMDx tasks quickly using the built-in generator:
data/docs/logging.md CHANGED
@@ -18,20 +18,16 @@ Sample output:
18
18
 
19
19
  ```log
20
20
  <!-- Success (INFO level) -->
21
- I, [2022-07-17T18:43:15.000000 #3784] INFO -- GenerateInvoice:
22
- index=0 chain_id="018c2b95-b764-7615-a924-cc5b910ed1e5" type="Task" class="GenerateInvoice" state="complete" status="success" metadata={runtime: 187}
21
+ I, [2025-12-23T17:04:07.292614Z #20108] INFO -- cmdx: {index: 1, chain_id: "019b4c2b-087b-79be-8ef2-96c11b659df5", type: "Task", tags: [], class: "GenerateInvoice", dry_run: false, id: "019b4c2b-0878-704d-ba0b-daa5410123ec", state: "complete", status: "success", outcome: "success", metadata: {runtime: 187}}
23
22
 
24
- <!-- Skipped (WARN level) -->
25
- W, [2022-07-17T18:43:15.000000 #3784] WARN -- ValidateCustomer:
26
- index=1 chain_id="018c2b95-b764-7615-a924-cc5b910ed1e5" type="Task" class="ValidateCustomer" state="interrupted" status="skipped" reason="Customer already validated"
23
+ <!-- Skipped (INFO level) -->
24
+ I, [2025-12-23T17:04:11.496881Z #20139] INFO -- cmdx: {index: 2, chain_id: "019b4c2b-18e8-7af6-a38b-63b042c4fbed", type: "Task", tags: [], class: "ValidateCustomer", dry_run: false, id: "019b4c2b-18e5-7230-af7e-5b4a4bd7cda2", state: "interrupted", status: "skipped", outcome: "skipped", metadata: {}, reason: "Customer already validated", cause: #<CMDx::SkipFault: Customer already validated>, rolled_back: false}
27
25
 
28
- <!-- Failed (ERROR level) -->
29
- E, [2022-07-17T18:43:15.000000 #3784] ERROR -- CalculateTax:
30
- index=2 chain_id="018c2b95-b764-7615-a924-cc5b910ed1e5" type="Task" class="CalculateTax" state="interrupted" status="failed" metadata={error_code: "TAX_SERVICE_UNAVAILABLE"}
26
+ <!-- Failed (INFO level) -->
27
+ I, [2025-12-23T17:04:15.875306Z #20173] INFO -- cmdx: {index: 3, chain_id: "019b4c2b-2a02-7dbc-b713-b20a7379704f", type: "Task", tags: [], class: "CalculateTax", dry_run: false, id: "019b4c2b-2a00-70b7-9fab-2f14db9139ef", state: "interrupted", status: "failed", outcome: "failed", metadata: {error_code: "TAX_SERVICE_UNAVAILABLE"}, reason: "Validation failed", cause: #<CMDx::FailFault: Validation failed>, rolled_back: false}
31
28
 
32
29
  <!-- Failed Chain -->
33
- E, [2022-07-17T18:43:15.000000 #3784] ERROR -- BillingWorkflow:
34
- index=3 chain_id="018c2b95-b764-7615-a924-cc5b910ed1e5" type="Task" class="BillingWorkflow" state="interrupted" status="failed" caused_failure={index: 2, class: "CalculateTax", status: "failed"} threw_failure={index: 1, class: "ValidateCustomer", status: "failed"}
30
+ I, [2025-12-23T17:04:20.972539Z #20209] INFO -- cmdx: {index: 0, chain_id: "019b4c2b-3de9-71f7-bcc3-2a98836bcfd7", type: "Workflow", tags: [], class: "BillingWorkflow", dry_run: false, id: "019b4c2b-3de6-70b9-9c16-5be13b1a463c", state: "interrupted", status: "failed", outcome: "interrupted", metadata: {}, reason: "Validation failed", cause: #<CMDx::FailFault: Validation failed>, rolled_back: false, threw_failure: {index: 3, chain_id: "019b4c2b-3de9-71f7-bcc3-2a98836bcfd7", type: "Task", tags: [], class: "CalculateTax", id: "019b4c2b-3dec-70b3-969b-c5b7896e3b27", state: "interrupted", status: "failed", outcome: "failed", metadata: {error_code: "TAX_SERVICE_UNAVAILABLE"}, reason: "Validation failed", cause: #<CMDx::FailFault: Validation failed>, rolled_back: false}, caused_failure: {index: 3, chain_id: "019b4c2b-3de9-71f7-bcc3-2a98836bcfd7", type: "Task", tags: [], class: "CalculateTax", id: "019b4c2b-3dec-70b3-969b-c5b7896e3b27", state: "interrupted", status: "failed", outcome: "failed", metadata: {error_code: "TAX_SERVICE_UNAVAILABLE"}, reason: "Validation failed", cause: #<CMDx::FailFault: Validation failed>, rolled_back: false}}
35
31
  ```
36
32
 
37
33
  !!! tip
@@ -30,7 +30,7 @@ result.metadata #=> { error_code: "BUILD_TOOL.NOT_FOUND" }
30
30
 
31
31
  ## Lifecycle Information
32
32
 
33
- Check execution state and status with predicate methods:
33
+ Check execution state, status, and rollback with predicate methods:
34
34
 
35
35
  ```ruby
36
36
  result = BuildApplication.execute(version: "1.2.3")
@@ -48,6 +48,9 @@ result.skipped? #=> false (not skipped)
48
48
  # Outcome categorization
49
49
  result.good? #=> true (success or skipped)
50
50
  result.bad? #=> false (skipped or failed)
51
+
52
+ # Rollback Status
53
+ result.rolled_back? #=> true (execution was rolled back)
51
54
  ```
52
55
 
53
56
  ## Outcome Analysis
@@ -126,19 +129,20 @@ result = BuildApplication.execute(version: "1.2.3")
126
129
 
127
130
  # Status-based handlers
128
131
  result
129
- .handle_success { |result| notify_deployment_ready(result) }
130
- .handle_failed { |result| handle_build_failure(result) }
131
- .handle_skipped { |result| log_skip_reason(result) }
132
+ .on(:success) { |result| notify_deployment_ready(result) }
133
+ .on(:failed) { |result| handle_build_failure(result) }
134
+ .on(:skipped) { |result| log_skip_reason(result) }
132
135
 
133
136
  # State-based handlers
134
137
  result
135
- .handle_complete { |result| update_build_status(result) }
136
- .handle_interrupted { |result| cleanup_partial_artifacts(result) }
138
+ .on(:complete) { |result| update_build_status(result) }
139
+ .on(:interrupted) { |result| cleanup_partial_artifacts(result) }
140
+ .on(:executed) { |result| alert_operations_team(result) } #=> .on(:complete, :interrupted)
137
141
 
138
142
  # Outcome-based handlers
139
143
  result
140
- .handle_good { |result| increment_success_counter(result) }
141
- .handle_bad { |result| alert_operations_team(result) }
144
+ .on(:good) { |result| increment_success_counter(result) } #=> .on(:success, :skipped)
145
+ .on(:bad) { |result| alert_operations_team(result) } #=> .on(:failed, :skipped)
142
146
  ```
143
147
 
144
148
  ## Pattern Matching
@@ -53,14 +53,14 @@ result.executed? #=> true (complete OR interrupted)
53
53
 
54
54
  ## Handlers
55
55
 
56
- Handle lifecycle events with state-based handlers. Use `handle_executed` for cleanup that runs regardless of outcome:
56
+ Handle lifecycle events with state-based handlers. Use `on(:executed)` for cleanup that runs regardless of outcome:
57
57
 
58
58
  ```ruby
59
59
  result = ProcessVideoUpload.execute
60
60
 
61
61
  # Individual state handlers
62
62
  result
63
- .handle_complete { |result| send_upload_notification(result) }
64
- .handle_interrupted { |result| cleanup_temp_files(result) }
65
- .handle_executed { |result| log_upload_metrics(result) }
63
+ .on(:complete) { |result| send_upload_notification(result) }
64
+ .on(:interrupted) { |result| cleanup_temp_files(result) }
65
+ .on(:executed) { |result| log_upload_metrics(result) } #=> .on(:complete, :interrupted)
66
66
  ```
@@ -47,19 +47,19 @@ result.bad? #=> true if skipped OR failed (not success)
47
47
 
48
48
  ## Handlers
49
49
 
50
- Branch business logic with status-based handlers. Use `handle_good` and `handle_bad` for success/skip vs failed outcomes:
50
+ Branch business logic with status-based handlers. Use `on(:good)` and `on(:bad)` for success/skip vs failed outcomes:
51
51
 
52
52
  ```ruby
53
53
  result = ProcessNotification.execute
54
54
 
55
55
  # Individual status handlers
56
56
  result
57
- .handle_success { |result| mark_notification_sent(result) }
58
- .handle_skipped { |result| log_notification_skipped(result) }
59
- .handle_failed { |result| queue_retry_notification(result) }
57
+ .on(:success) { |result| mark_notification_sent(result) }
58
+ .on(:skipped) { |result| log_notification_skipped(result) }
59
+ .on(:failed){ |result| queue_retry_notification(result) }
60
60
 
61
61
  # Outcome-based handlers
62
62
  result
63
- .handle_good { |result| update_message_stats(result) }
64
- .handle_bad { |result| track_delivery_failure(result) }
63
+ .on(:good) { |result| update_message_stats(result) } #=> .on(:success, :skipped)
64
+ .on(:bad) { |result| track_delivery_failure(result) } #=> .on(:failed, :skipped)
65
65
  ```
@@ -147,7 +147,11 @@ end
147
147
 
148
148
  ## Useful Examples
149
149
 
150
+ - [Active Record Database Transaction](https://github.com/drexed/cmdx/blob/main/examples/active_record_database_transaction.md)
150
151
  - [Active Record Query Tagging](https://github.com/drexed/cmdx/blob/main/examples/active_record_query_tagging.md)
152
+ - [Flipper Feature Flags](https://github.com/drexed/cmdx/blob/main/examples/flipper_feature_flags.md)
151
153
  - [Paper Trail Whatdunnit](https://github.com/drexed/cmdx/blob/main/examples/paper_trail_whatdunnit.md)
154
+ - [Redis Idempotency](https://github.com/drexed/cmdx/blob/main/examples/redis_idempotency.md)
155
+ - [Sentry Error Tracking](https://github.com/drexed/cmdx/blob/main/examples/sentry_error_tracking.md)
152
156
  - [Sidekiq Async Execution](https://github.com/drexed/cmdx/blob/main/examples/sidekiq_async_execution.md)
153
157
  - [Stoplight Circuit Breaker](https://github.com/drexed/cmdx/blob/main/examples/stoplight_circuit_breaker.md)
@@ -0,0 +1,27 @@
1
+ # Active Record Query Tagging
2
+
3
+ Wrap task or workflow execution in a database transaction. This is essential for data integrity when multiple steps modify the database.
4
+
5
+ ### Setup
6
+
7
+ ```ruby
8
+ # lib/cmdx_database_transaction_middleware.rb
9
+ class CmdxDatabaseTransactionMiddleware
10
+ def self.call(task, **options, &)
11
+ ActiveRecord::Base.transaction(requires_new: true, &)
12
+ end
13
+ end
14
+ ```
15
+
16
+ ### Usage
17
+
18
+ ```ruby
19
+ class MyTask < CMDx::Task
20
+ register :middleware, CmdxDatabaseTransactionMiddleware
21
+
22
+ def work
23
+ # Do work...
24
+ end
25
+
26
+ end
27
+ ```