cmdx 1.7.5 → 1.9.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 (150) hide show
  1. checksums.yaml +4 -4
  2. data/.DS_Store +0 -0
  3. data/.cursor/prompts/docs.md +3 -3
  4. data/.cursor/prompts/llms.md +1 -3
  5. data/.cursor/prompts/rspec.md +1 -1
  6. data/.irbrc +14 -2
  7. data/CHANGELOG.md +62 -29
  8. data/LLM.md +203 -78
  9. data/README.md +23 -85
  10. data/docs/.DS_Store +0 -0
  11. data/docs/assets/favicon.ico +0 -0
  12. data/docs/assets/favicon.svg +1 -0
  13. data/docs/attributes/coercions.md +19 -29
  14. data/docs/attributes/defaults.md +3 -16
  15. data/docs/attributes/definitions.md +29 -39
  16. data/docs/attributes/naming.md +3 -13
  17. data/docs/attributes/transformations.md +63 -0
  18. data/docs/attributes/validations.md +23 -40
  19. data/docs/basics/chain.md +14 -23
  20. data/docs/basics/context.md +13 -22
  21. data/docs/basics/execution.md +8 -26
  22. data/docs/basics/setup.md +8 -19
  23. data/docs/callbacks.md +19 -32
  24. data/docs/deprecation.md +8 -25
  25. data/docs/getting_started.md +101 -77
  26. data/docs/index.md +120 -0
  27. data/docs/internationalization.md +6 -18
  28. data/docs/interruptions/exceptions.md +10 -16
  29. data/docs/interruptions/faults.md +8 -25
  30. data/docs/interruptions/halt.md +31 -25
  31. data/docs/logging.md +7 -17
  32. data/docs/middlewares.md +13 -29
  33. data/docs/outcomes/result.md +21 -38
  34. data/docs/outcomes/states.md +8 -22
  35. data/docs/outcomes/statuses.md +10 -21
  36. data/docs/stylesheets/extra.css +42 -0
  37. data/docs/tips_and_tricks.md +7 -46
  38. data/docs/workflows.md +23 -38
  39. data/examples/active_record_query_tagging.md +46 -0
  40. data/examples/paper_trail_whatdunnit.md +39 -0
  41. data/lib/cmdx/attribute.rb +9 -2
  42. data/lib/cmdx/attribute_value.rb +31 -10
  43. data/lib/cmdx/callback_registry.rb +12 -2
  44. data/lib/cmdx/coercions/hash.rb +6 -1
  45. data/lib/cmdx/configuration.rb +10 -2
  46. data/lib/cmdx/deprecator.rb +3 -3
  47. data/lib/cmdx/errors.rb +1 -1
  48. data/lib/cmdx/executor.rb +97 -9
  49. data/lib/cmdx/log_formatters/logstash.rb +4 -4
  50. data/lib/cmdx/pipeline.rb +4 -4
  51. data/lib/cmdx/railtie.rb +9 -0
  52. data/lib/cmdx/result.rb +10 -1
  53. data/lib/cmdx/task.rb +12 -7
  54. data/lib/cmdx/version.rb +1 -1
  55. data/lib/cmdx.rb +1 -0
  56. data/lib/generators/cmdx/templates/install.rb +9 -0
  57. data/lib/locales/af.yml +2 -2
  58. data/lib/locales/ar.yml +2 -2
  59. data/lib/locales/az.yml +2 -2
  60. data/lib/locales/be.yml +2 -2
  61. data/lib/locales/bg.yml +2 -2
  62. data/lib/locales/bn.yml +2 -2
  63. data/lib/locales/bs.yml +2 -2
  64. data/lib/locales/ca.yml +2 -2
  65. data/lib/locales/cnr.yml +2 -2
  66. data/lib/locales/cs.yml +2 -2
  67. data/lib/locales/cy.yml +2 -2
  68. data/lib/locales/da.yml +2 -2
  69. data/lib/locales/de.yml +2 -2
  70. data/lib/locales/dz.yml +2 -2
  71. data/lib/locales/el.yml +2 -2
  72. data/lib/locales/en.yml +2 -2
  73. data/lib/locales/eo.yml +2 -2
  74. data/lib/locales/es.yml +2 -2
  75. data/lib/locales/et.yml +2 -2
  76. data/lib/locales/eu.yml +2 -2
  77. data/lib/locales/fa.yml +2 -2
  78. data/lib/locales/fi.yml +2 -2
  79. data/lib/locales/fr.yml +2 -2
  80. data/lib/locales/fy.yml +2 -2
  81. data/lib/locales/gd.yml +2 -2
  82. data/lib/locales/gl.yml +2 -2
  83. data/lib/locales/he.yml +2 -2
  84. data/lib/locales/hi.yml +2 -2
  85. data/lib/locales/hr.yml +2 -2
  86. data/lib/locales/hu.yml +2 -2
  87. data/lib/locales/hy.yml +2 -2
  88. data/lib/locales/id.yml +2 -2
  89. data/lib/locales/is.yml +2 -2
  90. data/lib/locales/it.yml +2 -2
  91. data/lib/locales/ja.yml +2 -2
  92. data/lib/locales/ka.yml +2 -2
  93. data/lib/locales/kk.yml +2 -2
  94. data/lib/locales/km.yml +2 -2
  95. data/lib/locales/kn.yml +2 -2
  96. data/lib/locales/ko.yml +2 -2
  97. data/lib/locales/lb.yml +2 -2
  98. data/lib/locales/lo.yml +2 -2
  99. data/lib/locales/lt.yml +2 -2
  100. data/lib/locales/lv.yml +2 -2
  101. data/lib/locales/mg.yml +2 -2
  102. data/lib/locales/mk.yml +2 -2
  103. data/lib/locales/ml.yml +2 -2
  104. data/lib/locales/mn.yml +2 -2
  105. data/lib/locales/mr-IN.yml +2 -2
  106. data/lib/locales/ms.yml +2 -2
  107. data/lib/locales/nb.yml +2 -2
  108. data/lib/locales/ne.yml +2 -2
  109. data/lib/locales/nl.yml +2 -2
  110. data/lib/locales/nn.yml +2 -2
  111. data/lib/locales/oc.yml +2 -2
  112. data/lib/locales/or.yml +2 -2
  113. data/lib/locales/pa.yml +2 -2
  114. data/lib/locales/pl.yml +2 -2
  115. data/lib/locales/pt.yml +2 -2
  116. data/lib/locales/rm.yml +2 -2
  117. data/lib/locales/ro.yml +2 -2
  118. data/lib/locales/ru.yml +2 -2
  119. data/lib/locales/sc.yml +2 -2
  120. data/lib/locales/sk.yml +2 -2
  121. data/lib/locales/sl.yml +2 -2
  122. data/lib/locales/sq.yml +2 -2
  123. data/lib/locales/sr.yml +2 -2
  124. data/lib/locales/st.yml +2 -2
  125. data/lib/locales/sv.yml +2 -2
  126. data/lib/locales/sw.yml +2 -2
  127. data/lib/locales/ta.yml +2 -2
  128. data/lib/locales/te.yml +2 -2
  129. data/lib/locales/th.yml +2 -2
  130. data/lib/locales/tl.yml +2 -2
  131. data/lib/locales/tr.yml +2 -2
  132. data/lib/locales/tt.yml +2 -2
  133. data/lib/locales/ug.yml +2 -2
  134. data/lib/locales/uk.yml +2 -2
  135. data/lib/locales/ur.yml +2 -2
  136. data/lib/locales/uz.yml +2 -2
  137. data/lib/locales/vi.yml +2 -2
  138. data/lib/locales/wo.yml +2 -2
  139. data/lib/locales/zh-CN.yml +2 -2
  140. data/lib/locales/zh-HK.yml +2 -2
  141. data/lib/locales/zh-TW.yml +2 -2
  142. data/lib/locales/zh-YUE.yml +2 -2
  143. data/mkdocs.yml +122 -0
  144. data/src/cmdx-dark-logo.png +0 -0
  145. data/src/cmdx-favicon.svg +1 -0
  146. data/src/cmdx-light-logo.png +0 -0
  147. data/src/cmdx-logo.svg +1 -0
  148. metadata +15 -4
  149. data/lib/cmdx/freezer.rb +0 -51
  150. data/src/cmdx-logo.png +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '0238294eeb6dd614280c7c386832d6780ea4005879c1162e7cacc4f97c54d5fd'
4
- data.tar.gz: 50c49904dbc84ffa7451a585e443c061b71576498ad00504ee87ae2efff07945
3
+ metadata.gz: 5ac9af727a0dd815dad8285e7e46d666e3f26f51eb03c3f1ec014c12e4af0e23
4
+ data.tar.gz: 8279a9ebcf5339fb281cd685cb0484b005f72b2605749f1418baea282be1afdf
5
5
  SHA512:
6
- metadata.gz: 7bac1596b901e86412d241f81c2c5fa331d2bf05b63c0ef3ee3691dd832c30fa8a187e6d8d4788fe9f3d1760bb8f7b1da62c2701a9a86bac6d9a4b5116f3e490
7
- data.tar.gz: 8794c3049d206178d68a4e9446d001668ee8b3a7902569dd1f66d05fe10b67f9cbc59e5264286ebfb075681415f430a3d69c40f181fdbe6f541caf4ec7fb4e56
6
+ metadata.gz: f97ef5703cacbfa7c51e4e513e214322c330750066e2420727d1bdc6469dac092b4f9b601291c8b0a721cba6bca863d22d4189bf10a8aa4b0ec53683f6598ccc
7
+ data.tar.gz: 3a1019a2f2db8088ec70821d51c5d13a10554f788a7732a70a8c14830cd9804fee354447e61dae3ca86fcf4a8a664932bdff5465a78256845d224870e78951ce
data/.DS_Store CHANGED
Binary file
@@ -3,10 +3,10 @@ You are a senior Ruby developer with expert knowledge of CMDx and writing docume
3
3
  Update the active tab using the following guidelines:
4
4
 
5
5
  - Follow best practices and implementation
6
- - Use a consistent professional voice
6
+ - Use a consistent warm, friendly and professional voice
7
7
  - Examples should be concise, non-repetitive, and realistic
8
8
  - Update any pre-existing documentation to match stated rules
9
9
  - Examples should not cross boundaries or focus
10
- - Docs must cover both typical use cases, including invalid inputs and error conditions
11
- - Use GitHub flavored markdown, including alerts to emphasize critical information (https://github.com/orgs/community/discussions/16925)
10
+ - Docs must cover both typical use cases, including invalid and error conditions
11
+ - Use mkdocs Admonitions to emphasize critical information (https://squidfunk.github.io/mkdocs-material/reference/admonitions/)
12
12
  - Optimize for LLM's including coding and AI agents
@@ -4,9 +4,7 @@ Process the following instructions in the order given:
4
4
  2. Append all files within `docs/**/*.md` into @LLM.md
5
5
  2a. Use order outlined in the table of contents of @README.md
6
6
  2b. Process one file at a time faster performance and improved accuracy
7
- 2c. Remove the table of contents from the chunk
8
- 2c. Remove the navigations below `---` from the chunk
9
- 2d. Wrap the chunk the files GitHub url the top and a spacer at the bottom like so:
7
+ 2c. Wrap the chunk the files GitHub url the top and a spacer at the bottom like so:
10
8
  ```
11
9
 
12
10
  ---
@@ -18,7 +18,7 @@ Add tests for the active tab using the following guidelines:
18
18
  - Use clear and descriptive names for describe, context, and it blocks
19
19
  - Prefer the expect syntax for assertions to improve readability
20
20
  - Keep test code concise; avoid unnecessary complexity or duplication
21
- - Tests must cover both typical cases and edge cases, including invalid inputs and error conditions
21
+ - Tests must cover both typical cases and edge cases, including Invalid and error conditions
22
22
  - Consider all possible scenarios for each method or behavior and ensure they are tested
23
23
  - Do NOT include integration or real world examples
24
24
  - Verify all specs are passing
data/.irbrc CHANGED
@@ -2,5 +2,17 @@
2
2
 
3
3
  require "pp"
4
4
 
5
- # To reload the gem, you must exit and restart the IRB session
6
- require_relative "lib/cmdx" unless defined?(CMDx)
5
+ # rubocop:disable Style/MixinUsage
6
+ unless defined?(CMDx)
7
+ require_relative "lib/cmdx"
8
+
9
+ require_relative "spec/support/helpers/task_builders"
10
+ require_relative "spec/support/helpers/workflow_builders"
11
+ include CMDx::Testing::TaskBuilders
12
+ include CMDx::Testing::WorkflowBuilders
13
+ end
14
+ # rubocop:enable Style/MixinUsage
15
+
16
+ def reload!
17
+ exec("irb")
18
+ end
data/CHANGELOG.md CHANGED
@@ -6,6 +6,37 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
 
7
7
  ## [UNRELEASED]
8
8
 
9
+ ## [1.9.0] - 2025-10-21
10
+
11
+ ### Added
12
+ - Added `transform` option to attributes
13
+ - Added option to output failure backtraces
14
+ - Added exception handling for non-bang methods
15
+ - Added durability with automatic retries to execution
16
+ - Added `to_h` hash coercion support
17
+ - Added comprehensive MkDocs configuration with material theme
18
+
19
+ ### Changed
20
+ - Improved performance of task settings setup
21
+ - Improved error messages for raised exceptions
22
+ - Improved inheritance of parent settings
23
+ - Cleaned halt backtrace frames for better readability
24
+
25
+ ### Removed
26
+ - Removed `Freezer` module and moved logic into executor `freeze_execution!` method
27
+ - Removed task parameter from callback signature
28
+ - Removed task and workflow arguments from conditional checks
29
+ - Removed chain persistence after execution in specs
30
+
31
+ ## [1.8.0] - 2025-09-22
32
+
33
+ ### Changed
34
+ - Generalized locale values for fault `invalid` and `unspecified`
35
+ - Nested attribute error messages under `error` key within metadata
36
+ - Reordered logstash formatter keys for consistency
37
+ - Improved error message for already defined items
38
+ - Changed hash coercion for `nil` to return `{}`
39
+
9
40
  ## [1.7.5] - 2025-09-10
10
41
 
11
42
  ### Added
@@ -22,69 +53,71 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
22
53
 
23
54
  ## [1.7.3] - 2025-09-03
24
55
 
25
- ### Changes
26
- - Return generic validation reason
27
- - Move validation full message string to `:full_message` within metadata
56
+ ### Changed
57
+ - Changed validation reasons to use generic values
58
+ - Moved validation full message string to `:full_message` key within metadata
28
59
 
29
60
  ## [1.7.2] - 2025-09-03
30
61
 
31
- ### Changes
32
- - Correlation ID is set before continuing to further steps
62
+ ### Changed
63
+ - Changed correlation ID to be set before continuing to further steps
33
64
 
34
65
  ## [1.7.1] - 2025-08-26
35
66
 
36
67
  ### Added
37
- - Yield result if block given to `execute` and `execute!` methods
68
+ - Added result yielding when block is given to `execute` and `execute!` methods
38
69
 
39
70
  ## [1.7.0] - 2025-08-25
40
71
 
41
72
  ### Added
42
- - Workflow generator
73
+ - Added workflow generator
43
74
 
44
- ### Changes
45
- - Port over `cmdx-parallel` changes
46
- - Port over `cmdx-i18n` changes
75
+ ### Changed
76
+ - Ported `cmdx-parallel` changes into core
77
+ - Ported `cmdx-i18n` changes into core
47
78
 
48
79
  ## [1.6.2] - 2025-08-24
49
80
 
50
- ### Changes
51
- - Prefix railtie I18n with `::` to play nice with `CMDx::I18n`
52
- - Use `cmdx-rspec` for matchers support
81
+ ### Changed
82
+ - Prefixed railtie I18n with `::` for compatibility with `CMDx::I18n`
83
+ - Changed to use `cmdx-rspec` for matchers support
53
84
 
54
85
  ## [1.6.1] - 2025-08-23
55
86
 
56
- ### Changes
57
- - Log task results before freezing
58
- - Rename `execute_tasks_sequentially` to `execute_tasks_in_sequence`
87
+ ### Changed
88
+ - Changed task results to be logged before freezing
89
+ - Renamed `execute_tasks_sequentially` to `execute_tasks_in_sequence`
59
90
 
60
91
  ## [1.6.0] - 2025-08-22
61
92
 
62
- ### Changes
63
- - Rename `Worker` class to `Executor`
64
- - Move workflow `work` logic into `Pipeline`
65
- - Add workflow task `:breakpoints`
93
+ ### Added
94
+ - Added workflow task `:breakpoints` support
95
+
96
+ ### Changed
97
+ - Renamed `Worker` class to `Executor`
98
+ - Moved workflow `work` logic into `Pipeline`
66
99
 
67
100
  ## [1.5.2] - 2025-08-22
68
101
 
69
- ### Changes
70
- - Rename workflow `execution_groups` attribute to `pipeline`
102
+ ### Changed
103
+ - Renamed workflow `execution_groups` attribute to `pipeline`
71
104
 
72
105
  ## [1.5.1] - 2025-08-21
73
106
 
74
- ### Changes
75
- - Prefix locale I18n with `::` to play nice with `CMDx::I18n`
76
- - Safe navigate length and numeric validators
77
- - Update railtie file path points to correct directory
107
+ ### Changed
108
+ - Prefixed locale I18n with `::` for compatibility with `CMDx::I18n`
109
+ - Added safe navigation to length and numeric validators
110
+ - Updated railtie file path to point to correct directory
78
111
 
79
112
  ## [1.5.0] - 2025-08-21
80
113
 
81
- ### Changes
82
- - BREAKING - Revamp CMDx for clarity, transparency, and higher performance
114
+ ### Changed
115
+ - **BREAKING**: Revamped CMDx for improved clarity, transparency, and higher performance
83
116
 
84
117
  ## [1.1.2] - 2025-07-20
85
118
 
86
119
  ### Changed
87
- - All items between versions `0.1.0` and `1.1.2` should be reviewed within its own tag
120
+ - All changes between versions `0.1.0` and `1.1.2` should be reviewed within their respective git tags
88
121
 
89
122
  ## [0.1.0] - 2025-03-07
90
123
 
data/LLM.md CHANGED
@@ -87,6 +87,45 @@ CMDx.configure do |config|
87
87
  end
88
88
  ```
89
89
 
90
+ ### Backtraces
91
+
92
+ Enable backtraces to be logged on any non-fault exceptions for improved debugging context. Run them through a cleaner to remove unwanted stack trace noise.
93
+
94
+ > [!NOTE]
95
+ > The `backtrace_cleaner` is set to `Rails.backtrace_cleaner.clean` in a Rails env by default.
96
+
97
+ ```ruby
98
+ CMDx.configure do |config|
99
+ # Truthy
100
+ config.backtrace = true
101
+
102
+ # Via callable (must respond to `call(backtrace)`)
103
+ config.backtrace_cleaner = AdvanceCleaner.new
104
+
105
+ # Via proc or lambda
106
+ config.backtrace_cleaner = ->(backtrace) { backtrace[0..5] }
107
+ end
108
+ ```
109
+
110
+ ### Exception Handlers
111
+
112
+ Use exception handlers are called on non-fault standard error based exceptions.
113
+
114
+ > [!TIP]
115
+ > Use exception handlers to send errors to your APM of choice.
116
+
117
+ ```ruby
118
+ CMDx.configure do |config|
119
+ # Via callable (must respond to `call(task, exception)`)
120
+ config.exception_handler = NewRelicReporter
121
+
122
+ # Via proc or lambda
123
+ config.exception_handler = proc do |task, exception|
124
+ APMService.report(exception, extra_data: { task: task.name, id: task.id })
125
+ end
126
+ end
127
+ ```
128
+
90
129
  ### Logging
91
130
 
92
131
  ```ruby
@@ -213,6 +252,8 @@ class GenerateInvoice < CMDx::Task
213
252
  # Global configuration overrides
214
253
  task_breakpoints: ["failed"], # Breakpoint override
215
254
  workflow_breakpoints: [], # Breakpoint override
255
+ backtrace: true, # Toggle backtrace
256
+ backtrace_cleaner: ->(bt) { bt[0..5] }, # Backtrace cleaner
216
257
  logger: CustomLogger.new($stdout), # Custom logger
217
258
 
218
259
  # Task configuration settings
@@ -220,7 +261,10 @@ class GenerateInvoice < CMDx::Task
220
261
  log_level: :info, # Log level override
221
262
  log_formatter: CMDx::LogFormatters::Json.new # Log formatter override
222
263
  tags: ["billing", "financial"], # Logging tags
223
- deprecated: true # Task deprecations
264
+ deprecated: true, # Task deprecations
265
+ retries: 3, # Non-fault exception retries
266
+ retry_on: [External::ApiError], # List of exceptions to retry on
267
+ retry_jitter: 1 # Space between retry iteration, eg: current retry num + 1
224
268
  )
225
269
 
226
270
  def work
@@ -229,8 +273,8 @@ class GenerateInvoice < CMDx::Task
229
273
  end
230
274
  ```
231
275
 
232
- > [!TIP]
233
- > Use task-level settings for tasks that require special handling, such as financial reporting, external API integrations, or critical system operations.
276
+ > [!IMPORTANT]
277
+ > Retries reuse the same context when executing its work. By default all `StandardErrors` will be retried if no `retry_on` option is passed.
234
278
 
235
279
  ### Registrations
236
280
 
@@ -783,7 +827,7 @@ result = ProcessInventory.execute(inventory_id: 456)
783
827
  result.status #=> "skipped"
784
828
 
785
829
  # Without a reason
786
- result.reason #=> "No reason given"
830
+ result.reason #=> "Unspecified"
787
831
 
788
832
  # With a reason
789
833
  result.reason #=> "Warehouse closed"
@@ -818,7 +862,7 @@ result = ProcessRefund.execute(refund_id: 789)
818
862
  result.status #=> "failed"
819
863
 
820
864
  # Without a reason
821
- result.reason #=> "No reason given"
865
+ result.reason #=> "Unspecified"
822
866
 
823
867
  # With a reason
824
868
  result.reason #=> "Refund period has expired"
@@ -938,8 +982,28 @@ skip!("Paused")
938
982
  fail!("Unsupported")
939
983
 
940
984
  # Bad: Default, cannot determine reason
941
- skip! #=> "No reason given"
942
- fail! #=> "No reason given"
985
+ skip! #=> "Unspecified"
986
+ fail! #=> "Unspecified"
987
+ ```
988
+
989
+ ## Manual Errors
990
+
991
+ There are rare cases where you need to manually assign errors.
992
+
993
+ > [!IMPORTANT]
994
+ > Keep in mind you will still need to initiate a fault if a stoppage of work is required.
995
+
996
+ ```ruby
997
+ class ProcessRenewal < CMDx::Task
998
+ def work
999
+ if document.nonrenewable?
1000
+ errors.add(:document, "not renewable")
1001
+ fail!("document could not be renewed")
1002
+ else
1003
+ document.renew!
1004
+ end
1005
+ end
1006
+ end
943
1007
  ```
944
1008
 
945
1009
  ---
@@ -1150,6 +1214,9 @@ result.reason #=> "[ActiveRecord::NotFoundError] record not found"
1150
1214
  result.cause #=> <ActiveRecord::NotFoundError>
1151
1215
  ```
1152
1216
 
1217
+ > [!NOTE]
1218
+ > The `exception_handler` setting only works with non-bang execution as it catches all exceptions preventing them from reaching your apps global error handler.
1219
+
1153
1220
  ### Bang execution
1154
1221
 
1155
1222
  The `execute!` method allows unhandled exceptions to propagate, enabling standard Ruby exception handling while respecting CMDx fault configuration.
@@ -1301,19 +1368,19 @@ result = BuildApplication.execute(version: "1.2.3")
1301
1368
 
1302
1369
  # Status-based handlers
1303
1370
  result
1304
- .on_success { |result| notify_deployment_ready(result) }
1305
- .on_failed { |result| handle_build_failure(result) }
1306
- .on_skipped { |result| log_skip_reason(result) }
1371
+ .handle_success { |result| notify_deployment_ready(result) }
1372
+ .handle_failed { |result| handle_build_failure(result) }
1373
+ .handle_skipped { |result| log_skip_reason(result) }
1307
1374
 
1308
1375
  # State-based handlers
1309
1376
  result
1310
- .on_complete { |result| update_build_status(result) }
1311
- .on_interrupted { |result| cleanup_partial_artifacts(result) }
1377
+ .handle_complete { |result| update_build_status(result) }
1378
+ .handle_interrupted { |result| cleanup_partial_artifacts(result) }
1312
1379
 
1313
1380
  # Outcome-based handlers
1314
1381
  result
1315
- .on_good { |result| increment_success_counter(result) }
1316
- .on_bad { |result| alert_operations_team(result) }
1382
+ .handle_good { |result| increment_success_counter(result) }
1383
+ .handle_bad { |result| alert_operations_team(result) }
1317
1384
  ```
1318
1385
 
1319
1386
  ## Pattern Matching
@@ -1435,9 +1502,9 @@ result = ProcessVideoUpload.execute
1435
1502
 
1436
1503
  # Individual state handlers
1437
1504
  result
1438
- .on_complete { |result| send_upload_notification(result) }
1439
- .on_interrupted { |result| cleanup_temp_files(result) }
1440
- .on_executed { |result| log_upload_metrics(result) }
1505
+ .handle_complete { |result| send_upload_notification(result) }
1506
+ .handle_interrupted { |result| cleanup_temp_files(result) }
1507
+ .handle_executed { |result| log_upload_metrics(result) }
1441
1508
  ```
1442
1509
 
1443
1510
  ---
@@ -1500,14 +1567,14 @@ result = ProcessNotification.execute
1500
1567
 
1501
1568
  # Individual status handlers
1502
1569
  result
1503
- .on_success { |result| mark_notification_sent(result) }
1504
- .on_skipped { |result| log_notification_skipped(result) }
1505
- .on_failed { |result| queue_retry_notification(result) }
1570
+ .handle_success { |result| mark_notification_sent(result) }
1571
+ .handle_skipped { |result| log_notification_skipped(result) }
1572
+ .handle_failed { |result| queue_retry_notification(result) }
1506
1573
 
1507
1574
  # Outcome-based handlers
1508
1575
  result
1509
- .on_good { |result| update_message_stats(result) }
1510
- .on_bad { |result| track_delivery_failure(result) }
1576
+ .handle_good { |result| update_message_stats(result) }
1577
+ .handle_bad { |result| track_delivery_failure(result) }
1511
1578
  ```
1512
1579
 
1513
1580
  ---
@@ -1754,12 +1821,14 @@ result = ConfigureServer.execute(server_id: "srv-001")
1754
1821
 
1755
1822
  result.state #=> "interrupted"
1756
1823
  result.status #=> "failed"
1757
- result.reason #=> "Invalid inputs"
1824
+ result.reason #=> "Invalid"
1758
1825
  result.metadata #=> {
1759
- # full_message: "environment is required. network_config is required.",
1760
- # messages: {
1761
- # environment: ["is required"],
1762
- # network_config: ["is required"]
1826
+ # errors: {
1827
+ # full_message: "environment is required. network_config is required.",
1828
+ # messages: {
1829
+ # environment: ["is required"],
1830
+ # network_config: ["is required"]
1831
+ # }
1763
1832
  # }
1764
1833
  # }
1765
1834
 
@@ -1772,11 +1841,13 @@ result = ConfigureServer.execute(
1772
1841
 
1773
1842
  result.state #=> "interrupted"
1774
1843
  result.status #=> "failed"
1775
- result.reason #=> "Invalid inputs"
1844
+ result.reason #=> "Invalid"
1776
1845
  result.metadata #=> {
1777
- # full_message: "port is required.",
1778
- # messages: {
1779
- # port: ["is required"]
1846
+ # errors: {
1847
+ # full_message: "port is required.",
1848
+ # messages: {
1849
+ # port: ["is required"]
1850
+ # }
1780
1851
  # }
1781
1852
  # }
1782
1853
  ```
@@ -2000,12 +2071,14 @@ result = AnalyzePerformance.execute(
2000
2071
 
2001
2072
  result.state #=> "interrupted"
2002
2073
  result.status #=> "failed"
2003
- result.reason #=> "Invalid inputs"
2074
+ result.reason #=> "Invalid"
2004
2075
  result.metadata #=> {
2005
- # full_message: "iterations could not coerce into an integer. score could not coerce into one of: float, big_decimal.",
2006
- # messages: {
2007
- # iterations: ["could not coerce into an integer"],
2008
- # score: ["could not coerce into one of: float, big_decimal"]
2076
+ # errors: {
2077
+ # full_message: "iterations could not coerce into an integer. score could not coerce into one of: float, big_decimal.",
2078
+ # messages: {
2079
+ # iterations: ["could not coerce into an integer"],
2080
+ # score: ["could not coerce into one of: float, big_decimal"]
2081
+ # }
2009
2082
  # }
2010
2083
  # }
2011
2084
  ```
@@ -2294,14 +2367,16 @@ result = CreateProject.execute(
2294
2367
 
2295
2368
  result.state #=> "interrupted"
2296
2369
  result.status #=> "failed"
2297
- result.reason #=> "Invalid inputs"
2370
+ result.reason #=> "Invalid"
2298
2371
  result.metadata #=> {
2299
- # full_message: "project_name is too short (minimum is 3 characters). budget must be greater than 1000. priority is not included in the list. contact_email is invalid.",
2300
- # messages: {
2301
- # project_name: ["is too short (minimum is 3 characters)"],
2302
- # budget: ["must be greater than 1000"],
2303
- # priority: ["is not included in the list"],
2304
- # contact_email: ["is invalid"]
2372
+ # errors: {
2373
+ # full_message: "project_name is too short (minimum is 3 characters). budget must be greater than 1000. priority is not included in the list. contact_email is invalid.",
2374
+ # messages: {
2375
+ # project_name: ["is too short (minimum is 3 characters)"],
2376
+ # budget: ["must be greater than 1000"],
2377
+ # priority: ["is not included in the list"],
2378
+ # contact_email: ["is invalid"]
2379
+ # }
2305
2380
  # }
2306
2381
  # }
2307
2382
  ```
@@ -2391,6 +2466,80 @@ end
2391
2466
 
2392
2467
  ---
2393
2468
 
2469
+ url: https://github.com/drexed/cmdx/blob/main/docs/attributes/transformations.md
2470
+ ---
2471
+
2472
+ # Attributes - Transformations
2473
+
2474
+ Transformations allow you to modify attribute values after they are derived and coerced from their source but before any validations. This enables data normalization, formatting, and conditional processing within the attribute pipeline.
2475
+
2476
+ ## Declarations
2477
+
2478
+ ### Symbol References
2479
+
2480
+ Reference instance methods by symbol for dynamic value transformations:
2481
+
2482
+ ```ruby
2483
+ class ProcessAnalytics < CMDx::Task
2484
+ attribute :options, transform: :compact_blank
2485
+ end
2486
+ ```
2487
+
2488
+ ### Proc or Lambda
2489
+
2490
+ Use anonymous functions for dynamic value transformations:
2491
+
2492
+ ```ruby
2493
+ class CacheContent < CMDx::Task
2494
+ # Proc
2495
+ attribute :expire_hours, transform: proc { |v| v * 2 }
2496
+
2497
+ # Lambda
2498
+ attribute :compression, transform: ->(v) { v.to_s.upcase.strip[0..2] }
2499
+ end
2500
+ ```
2501
+
2502
+ ### Class or Module
2503
+
2504
+ Use any object that responds to `call` for reusable transformation logic:
2505
+
2506
+ ```ruby
2507
+ class EmailNormalizer
2508
+ def call(value)
2509
+ value.to_s.downcase.strip
2510
+ end
2511
+ end
2512
+
2513
+ class ProcessContacts < CMDx::Task
2514
+ # Class or Module
2515
+ attribute :email, transform: EmailNormalizer
2516
+
2517
+ # Instance
2518
+ attribute :email, transform: EmailNormalizer.new
2519
+ end
2520
+ ```
2521
+
2522
+ ## Validations
2523
+
2524
+ Transformed values are subject to the same validation rules as untransformed values, ensuring consistency and catching configuration errors early.
2525
+
2526
+ ```ruby
2527
+ class ScheduleBackup < CMDx::Task
2528
+ # Coercions
2529
+ attribute :retention_days, type: :integer, transform: proc { |v| v.clamp(1, 5) }
2530
+
2531
+ # Validations
2532
+ optional :frequency, transform: :downcase, inclusion: { in: %w[hourly daily weekly monthly] }
2533
+ end
2534
+ ```
2535
+
2536
+ ---
2537
+
2538
+ - **Prev:** [Attributes - Defaults](defaults.md)
2539
+ - **Next:** [Callbacks](../callbacks.md)
2540
+
2541
+ ---
2542
+
2394
2543
  url: https://github.com/drexed/cmdx/blob/main/docs/callbacks.md
2395
2544
  ---
2396
2545
 
@@ -2459,7 +2608,7 @@ Use anonymous functions for inline callback logic:
2459
2608
  ```ruby
2460
2609
  class ProcessBooking < CMDx::Task
2461
2610
  # Proc
2462
- on_interrupted proc { |task| ReservationSystem.pause! }
2611
+ on_interrupted proc { ReservationSystem.pause! }
2463
2612
 
2464
2613
  # Lambda
2465
2614
  on_complete -> { ReservationSystem.resume! }
@@ -2506,10 +2655,10 @@ class ProcessBooking < CMDx::Task
2506
2655
  before_execution :notify_guest, if: :messaging_enabled?, unless: :messaging_blocked?
2507
2656
 
2508
2657
  # Proc
2509
- on_failure :increment_failure, if: ->(task) { Rails.env.production? && task.class.name.include?("Legacy") }
2658
+ on_failure :increment_failure, if: -> { Rails.env.production? && self.class.name.include?("Legacy") }
2510
2659
 
2511
2660
  # Lambda
2512
- on_success :ping_housekeeping, if: proc { |task| task.context.rooms_need_cleaning? }
2661
+ on_success :ping_housekeeping, if: proc { context.rooms_need_cleaning? }
2513
2662
 
2514
2663
  # Class or Module
2515
2664
  on_complete :send_confirmation, unless: MessagingPermissionCheck
@@ -2524,7 +2673,7 @@ class ProcessBooking < CMDx::Task
2524
2673
  private
2525
2674
 
2526
2675
  def messaging_enabled?
2527
- context.guest.messaging_preference.present?
2676
+ context.guest.messaging_preference == true
2528
2677
  end
2529
2678
 
2530
2679
  def messaging_blocked?
@@ -3048,7 +3197,7 @@ result = ProcessOldData.execute
3048
3197
  result.successful? #=> true
3049
3198
 
3050
3199
  # Ruby warning appears in stderr:
3051
- # [ProcessOldData] DEPRECATED: migrate to replacement or discontinue use
3200
+ # [ProcessOldData] DEPRECATED: migrate to a replacement or discontinue use
3052
3201
  ```
3053
3202
 
3054
3203
  ## Declarations
@@ -3199,10 +3348,10 @@ class OnboardingWorkflow < CMDx::Task
3199
3348
  task SendWelcomeEmail, if: :email_configured?, unless: :email_disabled?
3200
3349
 
3201
3350
  # Proc
3202
- task SendWelcomeEmail, if: ->(workflow) { Rails.env.production? && workflow.class.name.include?("Premium") }
3351
+ task SendWelcomeEmail, if: -> { Rails.env.production? && self.class.name.include?("Premium") }
3203
3352
 
3204
3353
  # Lambda
3205
- task SendWelcomeEmail, if: proc { |workflow| workflow.context.features_enabled? }
3354
+ task SendWelcomeEmail, if: proc { context.features_enabled? }
3206
3355
 
3207
3356
  # Class or Module
3208
3357
  task SendWelcomeEmail, unless: ContentAccessCheck
@@ -3216,7 +3365,7 @@ class OnboardingWorkflow < CMDx::Task
3216
3365
  private
3217
3366
 
3218
3367
  def email_configured?
3219
- context.user.email_address.present?
3368
+ context.user.email_address == true
3220
3369
  end
3221
3370
 
3222
3371
  def email_disabled?
@@ -3508,33 +3657,9 @@ class ConfigureCompany < CMDx::Task
3508
3657
  end
3509
3658
  ```
3510
3659
 
3511
- ## ActiveRecord Query Tagging
3512
-
3513
- Automatically tag SQL queries for better debugging:
3514
-
3515
- ```ruby
3516
- # config/application.rb
3517
- config.active_record.query_log_tags_enabled = true
3518
- config.active_record.query_log_tags << :cmdx_task_class
3519
- config.active_record.query_log_tags << :cmdx_chain_id
3660
+ ## Advance Examples
3520
3661
 
3521
- # app/tasks/application_task.rb
3522
- class ApplicationTask < CMDx::Task
3523
- before_execution :set_execution_context
3524
-
3525
- private
3526
-
3527
- def set_execution_context
3528
- # NOTE: This could easily be made into a middleware
3529
- ActiveSupport::ExecutionContext.set(
3530
- cmdx_task_class: self.class.name,
3531
- cmdx_chain_id: chain.id
3532
- )
3533
- end
3534
- end
3535
-
3536
- # SQL queries will now include comments like:
3537
- # /*cmdx_task_class:ExportReportTask,cmdx_chain_id:018c2b95-b764-7615*/ SELECT * FROM reports WHERE id = 1
3538
- ```
3662
+ - [Active Record Query Tagging](https://github.com/drexed/cmdx/blob/main/examples/active_record_query_tagging.md)
3663
+ - [Paper Trail Whatdunnit](https://github.com/drexed/cmdx/blob/main/examples/paper_trail_whatdunnit.md)
3539
3664
 
3540
3665
  ---