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.
- checksums.yaml +4 -4
- data/.DS_Store +0 -0
- data/.cursor/prompts/docs.md +3 -3
- data/.cursor/prompts/llms.md +1 -3
- data/.cursor/prompts/rspec.md +1 -1
- data/.irbrc +14 -2
- data/CHANGELOG.md +62 -29
- data/LLM.md +203 -78
- data/README.md +23 -85
- data/docs/.DS_Store +0 -0
- data/docs/assets/favicon.ico +0 -0
- data/docs/assets/favicon.svg +1 -0
- data/docs/attributes/coercions.md +19 -29
- data/docs/attributes/defaults.md +3 -16
- data/docs/attributes/definitions.md +29 -39
- data/docs/attributes/naming.md +3 -13
- data/docs/attributes/transformations.md +63 -0
- data/docs/attributes/validations.md +23 -40
- data/docs/basics/chain.md +14 -23
- data/docs/basics/context.md +13 -22
- data/docs/basics/execution.md +8 -26
- data/docs/basics/setup.md +8 -19
- data/docs/callbacks.md +19 -32
- data/docs/deprecation.md +8 -25
- data/docs/getting_started.md +101 -77
- data/docs/index.md +120 -0
- data/docs/internationalization.md +6 -18
- data/docs/interruptions/exceptions.md +10 -16
- data/docs/interruptions/faults.md +8 -25
- data/docs/interruptions/halt.md +31 -25
- data/docs/logging.md +7 -17
- data/docs/middlewares.md +13 -29
- data/docs/outcomes/result.md +21 -38
- data/docs/outcomes/states.md +8 -22
- data/docs/outcomes/statuses.md +10 -21
- data/docs/stylesheets/extra.css +42 -0
- data/docs/tips_and_tricks.md +7 -46
- data/docs/workflows.md +23 -38
- data/examples/active_record_query_tagging.md +46 -0
- data/examples/paper_trail_whatdunnit.md +39 -0
- data/lib/cmdx/attribute.rb +9 -2
- data/lib/cmdx/attribute_value.rb +31 -10
- data/lib/cmdx/callback_registry.rb +12 -2
- data/lib/cmdx/coercions/hash.rb +6 -1
- data/lib/cmdx/configuration.rb +10 -2
- data/lib/cmdx/deprecator.rb +3 -3
- data/lib/cmdx/errors.rb +1 -1
- data/lib/cmdx/executor.rb +97 -9
- data/lib/cmdx/log_formatters/logstash.rb +4 -4
- data/lib/cmdx/pipeline.rb +4 -4
- data/lib/cmdx/railtie.rb +9 -0
- data/lib/cmdx/result.rb +10 -1
- data/lib/cmdx/task.rb +12 -7
- data/lib/cmdx/version.rb +1 -1
- data/lib/cmdx.rb +1 -0
- data/lib/generators/cmdx/templates/install.rb +9 -0
- data/lib/locales/af.yml +2 -2
- data/lib/locales/ar.yml +2 -2
- data/lib/locales/az.yml +2 -2
- data/lib/locales/be.yml +2 -2
- data/lib/locales/bg.yml +2 -2
- data/lib/locales/bn.yml +2 -2
- data/lib/locales/bs.yml +2 -2
- data/lib/locales/ca.yml +2 -2
- data/lib/locales/cnr.yml +2 -2
- data/lib/locales/cs.yml +2 -2
- data/lib/locales/cy.yml +2 -2
- data/lib/locales/da.yml +2 -2
- data/lib/locales/de.yml +2 -2
- data/lib/locales/dz.yml +2 -2
- data/lib/locales/el.yml +2 -2
- data/lib/locales/en.yml +2 -2
- data/lib/locales/eo.yml +2 -2
- data/lib/locales/es.yml +2 -2
- data/lib/locales/et.yml +2 -2
- data/lib/locales/eu.yml +2 -2
- data/lib/locales/fa.yml +2 -2
- data/lib/locales/fi.yml +2 -2
- data/lib/locales/fr.yml +2 -2
- data/lib/locales/fy.yml +2 -2
- data/lib/locales/gd.yml +2 -2
- data/lib/locales/gl.yml +2 -2
- data/lib/locales/he.yml +2 -2
- data/lib/locales/hi.yml +2 -2
- data/lib/locales/hr.yml +2 -2
- data/lib/locales/hu.yml +2 -2
- data/lib/locales/hy.yml +2 -2
- data/lib/locales/id.yml +2 -2
- data/lib/locales/is.yml +2 -2
- data/lib/locales/it.yml +2 -2
- data/lib/locales/ja.yml +2 -2
- data/lib/locales/ka.yml +2 -2
- data/lib/locales/kk.yml +2 -2
- data/lib/locales/km.yml +2 -2
- data/lib/locales/kn.yml +2 -2
- data/lib/locales/ko.yml +2 -2
- data/lib/locales/lb.yml +2 -2
- data/lib/locales/lo.yml +2 -2
- data/lib/locales/lt.yml +2 -2
- data/lib/locales/lv.yml +2 -2
- data/lib/locales/mg.yml +2 -2
- data/lib/locales/mk.yml +2 -2
- data/lib/locales/ml.yml +2 -2
- data/lib/locales/mn.yml +2 -2
- data/lib/locales/mr-IN.yml +2 -2
- data/lib/locales/ms.yml +2 -2
- data/lib/locales/nb.yml +2 -2
- data/lib/locales/ne.yml +2 -2
- data/lib/locales/nl.yml +2 -2
- data/lib/locales/nn.yml +2 -2
- data/lib/locales/oc.yml +2 -2
- data/lib/locales/or.yml +2 -2
- data/lib/locales/pa.yml +2 -2
- data/lib/locales/pl.yml +2 -2
- data/lib/locales/pt.yml +2 -2
- data/lib/locales/rm.yml +2 -2
- data/lib/locales/ro.yml +2 -2
- data/lib/locales/ru.yml +2 -2
- data/lib/locales/sc.yml +2 -2
- data/lib/locales/sk.yml +2 -2
- data/lib/locales/sl.yml +2 -2
- data/lib/locales/sq.yml +2 -2
- data/lib/locales/sr.yml +2 -2
- data/lib/locales/st.yml +2 -2
- data/lib/locales/sv.yml +2 -2
- data/lib/locales/sw.yml +2 -2
- data/lib/locales/ta.yml +2 -2
- data/lib/locales/te.yml +2 -2
- data/lib/locales/th.yml +2 -2
- data/lib/locales/tl.yml +2 -2
- data/lib/locales/tr.yml +2 -2
- data/lib/locales/tt.yml +2 -2
- data/lib/locales/ug.yml +2 -2
- data/lib/locales/uk.yml +2 -2
- data/lib/locales/ur.yml +2 -2
- data/lib/locales/uz.yml +2 -2
- data/lib/locales/vi.yml +2 -2
- data/lib/locales/wo.yml +2 -2
- data/lib/locales/zh-CN.yml +2 -2
- data/lib/locales/zh-HK.yml +2 -2
- data/lib/locales/zh-TW.yml +2 -2
- data/lib/locales/zh-YUE.yml +2 -2
- data/mkdocs.yml +122 -0
- data/src/cmdx-dark-logo.png +0 -0
- data/src/cmdx-favicon.svg +1 -0
- data/src/cmdx-light-logo.png +0 -0
- data/src/cmdx-logo.svg +1 -0
- metadata +15 -4
- data/lib/cmdx/freezer.rb +0 -51
- data/src/cmdx-logo.png +0 -0
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA256:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: 5ac9af727a0dd815dad8285e7e46d666e3f26f51eb03c3f1ec014c12e4af0e23
         | 
| 4 | 
            +
              data.tar.gz: 8279a9ebcf5339fb281cd685cb0484b005f72b2605749f1418baea282be1afdf
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: f97ef5703cacbfa7c51e4e513e214322c330750066e2420727d1bdc6469dac092b4f9b601291c8b0a721cba6bca863d22d4189bf10a8aa4b0ec53683f6598ccc
         | 
| 7 | 
            +
              data.tar.gz: 3a1019a2f2db8088ec70821d51c5d13a10554f788a7732a70a8c14830cd9804fee354447e61dae3ca86fcf4a8a664932bdff5465a78256845d224870e78951ce
         | 
    
        data/.DS_Store
    CHANGED
    
    | Binary file | 
    
        data/.cursor/prompts/docs.md
    CHANGED
    
    | @@ -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  | 
| 11 | 
            -
            - Use  | 
| 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
         | 
    
        data/.cursor/prompts/llms.md
    CHANGED
    
    | @@ -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.  | 
| 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 | 
             
                  ---
         | 
    
        data/.cursor/prompts/rspec.md
    CHANGED
    
    | @@ -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  | 
| 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 | 
            -
            #  | 
| 6 | 
            -
             | 
| 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 | 
            -
            ###  | 
| 26 | 
            -
            -  | 
| 27 | 
            -
            -  | 
| 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 | 
            -
            ###  | 
| 32 | 
            -
            -  | 
| 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 | 
            -
            -  | 
| 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 | 
            -
            -  | 
| 73 | 
            +
            - Added workflow generator
         | 
| 43 74 |  | 
| 44 | 
            -
            ###  | 
| 45 | 
            -
            -  | 
| 46 | 
            -
            -  | 
| 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 | 
            -
            ###  | 
| 51 | 
            -
            -  | 
| 52 | 
            -
            -  | 
| 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 | 
            -
            ###  | 
| 57 | 
            -
            -  | 
| 58 | 
            -
            -  | 
| 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 | 
            -
            ###  | 
| 63 | 
            -
            -  | 
| 64 | 
            -
             | 
| 65 | 
            -
             | 
| 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 | 
            -
            ###  | 
| 70 | 
            -
            -  | 
| 102 | 
            +
            ### Changed
         | 
| 103 | 
            +
            - Renamed workflow `execution_groups` attribute to `pipeline`
         | 
| 71 104 |  | 
| 72 105 | 
             
            ## [1.5.1] - 2025-08-21
         | 
| 73 106 |  | 
| 74 | 
            -
            ###  | 
| 75 | 
            -
            -  | 
| 76 | 
            -
            -  | 
| 77 | 
            -
            -  | 
| 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 | 
            -
            ###  | 
| 82 | 
            -
            - BREAKING  | 
| 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  | 
| 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 | 
| 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 | 
            -
            > [! | 
| 233 | 
            -
            >  | 
| 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 #=> " | 
| 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 #=> " | 
| 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! #=> " | 
| 942 | 
            -
            fail! #=> " | 
| 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 | 
            -
              . | 
| 1305 | 
            -
              . | 
| 1306 | 
            -
              . | 
| 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 | 
            -
              . | 
| 1311 | 
            -
              . | 
| 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 | 
            -
              . | 
| 1316 | 
            -
              . | 
| 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 | 
            -
              . | 
| 1439 | 
            -
              . | 
| 1440 | 
            -
              . | 
| 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 | 
            -
              . | 
| 1504 | 
            -
              . | 
| 1505 | 
            -
              . | 
| 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 | 
            -
              . | 
| 1510 | 
            -
              . | 
| 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 | 
| 1824 | 
            +
            result.reason   #=> "Invalid"
         | 
| 1758 1825 | 
             
            result.metadata #=> {
         | 
| 1759 | 
            -
                            #      | 
| 1760 | 
            -
                            # | 
| 1761 | 
            -
                            #        | 
| 1762 | 
            -
                            # | 
| 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 | 
| 1844 | 
            +
            result.reason   #=> "Invalid"
         | 
| 1776 1845 | 
             
            result.metadata #=> {
         | 
| 1777 | 
            -
                            #      | 
| 1778 | 
            -
                            # | 
| 1779 | 
            -
                            #        | 
| 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 | 
| 2074 | 
            +
            result.reason   #=> "Invalid"
         | 
| 2004 2075 | 
             
            result.metadata #=> {
         | 
| 2005 | 
            -
                            #      | 
| 2006 | 
            -
                            # | 
| 2007 | 
            -
                            #        | 
| 2008 | 
            -
                            # | 
| 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 | 
| 2370 | 
            +
            result.reason   #=> "Invalid"
         | 
| 2298 2371 | 
             
            result.metadata #=> {
         | 
| 2299 | 
            -
                            #      | 
| 2300 | 
            -
                            # | 
| 2301 | 
            -
                            #        | 
| 2302 | 
            -
                            # | 
| 2303 | 
            -
                            # | 
| 2304 | 
            -
                            # | 
| 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 {  | 
| 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: -> | 
| 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 {  | 
| 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 | 
| 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: -> | 
| 3351 | 
            +
              task SendWelcomeEmail, if: -> { Rails.env.production? && self.class.name.include?("Premium") }
         | 
| 3203 3352 |  | 
| 3204 3353 | 
             
              # Lambda
         | 
| 3205 | 
            -
              task SendWelcomeEmail, if: proc {  | 
| 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 | 
| 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 | 
            -
            ##  | 
| 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 | 
            -
             | 
| 3522 | 
            -
             | 
| 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 | 
             
            ---
         |