chrono_forge 0.0.2 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1f2655b7f5ea191bd4c80e36b5e6e55439ad04f8570fb29d36ce0f6dc66ce687
4
- data.tar.gz: ce5509c4fc6ee82179c6f14be7bf887f41f09ec0f344ef976ba3ac4691305725
3
+ metadata.gz: 0303e4144f2e7546095c0828a02fa810138b3963603ee33810232cc4444a7ee8
4
+ data.tar.gz: d1805992395168a7873b34c7264518e0b6d740fcdef23dd5fce2ac2cf686bb3a
5
5
  SHA512:
6
- metadata.gz: baca6968fedbded14682da7a4fb68b3d2f5f3da4228671358461c77245920787d0672fd4886ad000693a06bb8e22438d4f311b66dabfdbbd917dfad5d003cea2
7
- data.tar.gz: 677a8ef2549511e7e03f18c45d34897ac0ceaaa25ea9a11300ecac711f552c92c074e56ea6453d21a1b58d0ba20aabdd859941bbb1e93eec8cf3b7e9a6c69b34
6
+ metadata.gz: 64d20d4a723c5d751c60415f1461e5e0e581a6401a084d4f6c83cc9c0813f7b7163c87dff916e4af628e7742291cecfc9786d6afc922fc046577713d2db94f13
7
+ data.tar.gz: 0bcb5a5c37f3fd0e241e0e3d60a358b4fd7d2700c1960a8148b5b12dca147ce8f0db07c7e435d314c7b6bd537e3830ad1f0fc131f33e2cee1bfbf0d60c4a7b63
data/README.md CHANGED
@@ -0,0 +1,242 @@
1
+ # ChronoForge
2
+
3
+ ChronoForge is a Ruby gem that provides a robust framework for building durable, distributed workflows in Ruby on Rails applications. It offers a reliable way to handle long-running processes, state management, and error recovery.
4
+
5
+ ## Features
6
+
7
+ - **Durable Execution**: Automatically tracks and recovers from failures during workflow execution
8
+ - **State Management**: Built-in workflow state tracking with support for custom contexts
9
+ - **Concurrency Control**: Advanced locking mechanisms to prevent concurrent execution of the same workflow
10
+ - **Error Handling**: Comprehensive error tracking and retry strategies
11
+ - **Execution Logging**: Detailed logging of workflow execution steps and errors
12
+ - **Wait States**: Support for time-based waits and condition-based waiting
13
+ - **Database-Backed**: All workflow state is persisted to the database for durability
14
+ - **ActiveJob Integration**: Seamlessly works with any ActiveJob backend
15
+
16
+ ## Installation
17
+
18
+ Add this line to your application's Gemfile:
19
+
20
+ ```ruby
21
+ gem 'chrono_forge'
22
+ ```
23
+
24
+ Then execute:
25
+
26
+ ```bash
27
+ $ bundle install
28
+ ```
29
+
30
+ Or install it directly:
31
+
32
+ ```bash
33
+ $ gem install chrono_forge
34
+ ```
35
+
36
+ After installation, run the generator to create the necessary database migrations:
37
+
38
+ ```bash
39
+ $ rails generate chrono_forge:install
40
+ $ rails db:migrate
41
+ ```
42
+
43
+ ## Usage
44
+
45
+ ### Basic Workflow Example
46
+
47
+ Here's a complete example of a durable order processing workflow:
48
+
49
+ ```ruby
50
+ class OrderProcessingWorkflow < ApplicationJob
51
+ include ChronoForge::Executor
52
+
53
+ def perform
54
+ # Context can be used to pass and store data between executions
55
+ context["order_id"] = SecureRandom.hex
56
+
57
+ # Wait until payment is confirmed
58
+ wait_until :payment_confirmed?
59
+
60
+ # Wait for potential fraud check
61
+ wait 1.minute, :fraud_check_delay
62
+
63
+ # Durably execute order processing
64
+ durably_execute :process_order
65
+
66
+ # Final steps
67
+ complete_order
68
+ end
69
+
70
+ private
71
+
72
+ def payment_confirmed?
73
+ PaymentService.confirmed?(context["order_id"])
74
+ end
75
+
76
+ def process_order
77
+ context["processed_at"] = Time.current.iso8601
78
+ OrderProcessor.process(context["order_id"])
79
+ end
80
+
81
+ def complete_order
82
+ context["completed_at"] = Time.current.iso8601
83
+ OrderCompletionService.complete(context["order_id"])
84
+ end
85
+ end
86
+ ```
87
+
88
+ ### Workflow Features
89
+
90
+ #### Durable Execution
91
+
92
+ The `durably_execute` method ensures operations are executed exactly once:
93
+
94
+ ```ruby
95
+ # Execute a method
96
+ durably_execute(:process_payment, max_attempts: 3)
97
+
98
+ # Or with a block
99
+ durably_execute -> (ctx) {
100
+ Payment.process(ctx[:payment_id])
101
+ }
102
+ ```
103
+
104
+ #### Wait States
105
+
106
+ ChronoForge supports both time-based and condition-based waits:
107
+
108
+ ```ruby
109
+ # Wait for a specific duration
110
+ wait 1.hour, :cooling_period
111
+
112
+ # Wait until a condition is met
113
+ wait_until :payment_processed,
114
+ timeout: 1.hour,
115
+ check_interval: 5.minutes
116
+ ```
117
+
118
+ #### Workflow Context
119
+
120
+ ChronoForge provides a persistent context that survives job restarts:
121
+
122
+ ```ruby
123
+ # Set context values
124
+ context[:user_name] = "John Doe"
125
+ context[:status] = "processing"
126
+
127
+ # Read context values
128
+ user_name = context[:user_name]
129
+ ```
130
+
131
+ ### Error Handling
132
+
133
+ ChronoForge automatically tracks errors and provides retry capabilities:
134
+
135
+ ```ruby
136
+ class MyWorkflow < ApplicationJob
137
+ include ChronoForge::Executor
138
+
139
+ private
140
+
141
+ def should_retry?(error, attempt_count)
142
+ case error
143
+ when NetworkError
144
+ attempt_count < 5
145
+ when ValidationError
146
+ false # Don't retry validation errors
147
+ else
148
+ attempt_count < 3
149
+ end
150
+ end
151
+ end
152
+ ```
153
+
154
+ ## Testing
155
+
156
+ ChronoForge is designed to be easily testable using [ChaoticJob](https://github.com/fractaledmind/chaotic_job), a testing framework that makes it simple to test complex job workflows. Here's how to set up your test environment:
157
+
158
+ 1. Add ChaoticJob to your Gemfile's test group:
159
+
160
+ ```ruby
161
+ group :test do
162
+ gem 'chaotic_job'
163
+ end
164
+ ```
165
+
166
+ 2. Set up your test helper:
167
+
168
+ ```ruby
169
+ # test_helper.rb
170
+ require 'chrono_forge'
171
+ require 'minitest/autorun'
172
+ require 'chaotic_job'
173
+ ```
174
+
175
+ Example test:
176
+
177
+ ```ruby
178
+ class WorkflowTest < ActiveJob::TestCase
179
+ include ChaoticJob::Helpers
180
+
181
+ def test_workflow_completion
182
+ # Enqueue the job
183
+ OrderProcessingWorkflow.perform_later("order_123")
184
+
185
+ # Perform all enqueued jobs
186
+ perform_all_jobs
187
+
188
+ # Assert workflow completed successfully
189
+ workflow = ChronoForge::Workflow.last
190
+ assert workflow.completed?
191
+
192
+ # Check workflow context
193
+ assert workflow.context["processed_at"].present?
194
+ assert workflow.context["completed_at"].present?
195
+ end
196
+ end
197
+ ```
198
+
199
+ ChaoticJob provides several helpful methods for testing workflows:
200
+
201
+ - `perform_all_jobs`: Processes all enqueued jobs, including those enqueued during job execution
202
+
203
+ For testing with specific job processing libraries like Sidekiq or Delayed Job, you can still use their respective testing modes, but ChaoticJob is recommended for testing ChronoForge workflows as it better handles the complexities of nested job scheduling and wait states.
204
+
205
+
206
+ ## Database Schema
207
+
208
+ ChronoForge creates three main tables:
209
+
210
+ 1. `chrono_forge_workflows`: Stores workflow state and context
211
+ 2. `chrono_forge_execution_logs`: Tracks individual execution steps
212
+ 3. `chrono_forge_error_logs`: Records detailed error information
213
+
214
+ ## Development
215
+
216
+ After checking out the repo, run:
217
+
218
+ ```bash
219
+ $ bin/setup # Install dependencies
220
+ $ bundle exec rake test # Run the tests
221
+ $ bin/appraise # Run the full suite of appraisals
222
+ $ bin/console # Start an interactive console
223
+ ```
224
+
225
+ The test suite uses SQLite by default and includes:
226
+ - Unit tests for core functionality
227
+ - Integration tests with ActiveJob
228
+ - Example workflow implementations
229
+
230
+ ## Contributing
231
+
232
+ 1. Fork the repository
233
+ 2. Create your feature branch (`git checkout -b feature/my-new-feature`)
234
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
235
+ 4. Push to the branch (`git push origin feature/my-new-feature`)
236
+ 5. Create a new Pull Request
237
+
238
+ Please include tests for any new features or bug fixes.
239
+
240
+ ## License
241
+
242
+ This gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/export.json CHANGED
@@ -1,102 +1,114 @@
1
1
  [
2
2
  {
3
3
  "path": "/Users/stefan/Documents/plutonium/chrono_forge/CHANGELOG.md",
4
- "contents": "## [Unreleased]\n\n## [0.2.0] - 2024-07-31\n\n- Initial release\n"
4
+ "contents": "## [Unreleased]\n\n## [0.1.0] - 2024-12-21\n\n- Initial release\n"
5
5
  },
6
6
  {
7
7
  "path": "/Users/stefan/Documents/plutonium/chrono_forge/README.md",
8
- "contents": "# Phlexi::Form \n\nPhlexi::Form is a flexible and powerful form builder for Ruby applications. It provides a more customizable and extensible way to create forms compared to traditional form helpers.\n\n[![Ruby](https://github.com/radioactive-labs/phlexi-form/actions/workflows/main.yml/badge.svg)](https://github.com/radioactive-labs/phlexi-form/actions/workflows/main.yml)\n\n## Features\n\n- Customizable form components (input, select, checkbox, radio button, etc.)\n- Automatic field type and attribute inference based on model attributes\n- Built-in support for validations and error handling\n- Flexible form structure with support for nested attributes\n- Works with Phlex or erb views\n- Extract input from parameters that match your form definition. No need to strong paramters.\n- Rails compatible form inputs\n\n\n## Installation\n\nAdd this line to your application's Gemfile:\n\n```ruby\ngem 'phlexi-form'\n```\n\nAnd then execute:\n\n```\n$ bundle install\n```\n\nOr install it yourself as:\n\n```\n$ gem install phlexi-form\n```\n\n## Usage\n\nThere are 2 ways to use Phlexi::Form:\n\n### Direct Usage\n\n```ruby\nPhlexi::Form(User.new) do\n field(:name) do |name|\n render name.label_tag\n render name.input_tag\n end\n\n field(:email) do |email|\n render email.label_tag\n render email.input_tag\n end\n\n nest_one(:address) do |address|\n address.field(:street) do |street|\n render street.label_tag\n render street.input_tag\n end\n\n address.field(:city) do |city|\n render city.label_tag\n render city.input_tag\n end\n end\n\n render submit_button\nend\n```\n\n> **Important**\n>\n> If you are rendering your form inline e.g. \n> ```ruby\n> render Phlexi::Form(User.new) {\n> render field(:name).label_tag\n> render field(:name).input_tag\n> }\n> ```\n>\n> Make sure you use `{...}` in defining your block instead of `do...end`\n> This might be fixed in a future version.\n\n### Inherit form\n\n```ruby\nclass UserForm < Phlexi::Form::Base\n def form_template\n field(:name) do |name|\n render name.label_tag\n render name.input_tag\n end\n\n field(:email) do |email|\n render email.label_tag\n render email.input_tag\n end\n\n nest_one(:address) do |address|\n address.field(:street) do |street|\n render street.label_tag\n render street.input_tag\n end\n\n address.field(:city) do |city|\n render city.label_tag\n render city.input_tag\n end\n end\n\n render submit_button\n end\nend\n\n\n# In your view or controller\nform = UserForm.new(User.new)\n\n# Render the form\nrender form\n\n# Extract params\nform.extract_input({\n name: \"Brad Pitt\",\n email: \"brad@pitt.com\",\n address: {\n street: \"Plumbago\",\n city: \"Accra\",\n }\n})\n```\n\n## Advanced Usage\n\n### Custom Components\n\nYou can create custom form components by inheriting from `Phlexi::Form::Components::Base`:\n\n```ruby\nclass CustomInput < Phlexi::Form::Components::Base\n def template\n div(class: \"custom-input\") do\n input(**attributes)\n span(class: \"custom-icon\")\n end\n end\nend\n\n# Usage in your form\nfield(:custom_field) do |field|\n render CustomInput.new(field)\nend\n```\n\n### Theming\n\nPhlexi::Form supports theming through a flexible theming system:\n\n```ruby\nclass ThemedForm < Phlexi::Form::Base\n class FieldBuilder < FieldBuilder\n private\n \n def default_theme\n {\n input: \"border rounded px-2 py-1\",\n label: \"font-bold text-gray-700\",\n # Add more theme options here\n }\n end\n end\nend\n```\n\n<!-- ## Configuration\n\nYou can configure Phlexi::Form globally by creating an initializer:\n\n```ruby\n# config/initializers/phlexi_form.rb\nPhlexi::Form.configure do |config|\n config.default_theme = {\n # Your default theme options\n }\n # Add more configuration options here\nend\n``` -->\n\n## Contributing\n\nBug reports and pull requests are welcome on GitHub at https://github.com/radioactive-labs/phlexi-form.\n\n## License\n\nThe gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).\n"
8
+ "contents": "# ChronoForge\n\nChronoForge is a Ruby gem that provides a robust framework for building durable, distributed workflows in Ruby on Rails applications. It offers a reliable way to handle long-running processes, state management, and error recovery.\n\n## Features\n\n- **Durable Execution**: Automatically tracks and recovers from failures during workflow execution\n- **State Management**: Built-in workflow state tracking with support for custom contexts\n- **Concurrency Control**: Advanced locking mechanisms to prevent concurrent execution of the same workflow\n- **Error Handling**: Comprehensive error tracking and retry strategies\n- **Execution Logging**: Detailed logging of workflow execution steps and errors\n- **Wait States**: Support for time-based waits and condition-based waiting\n- **Database-Backed**: All workflow state is persisted to the database for durability\n- **ActiveJob Integration**: Seamlessly works with any ActiveJob backend\n\n## Installation\n\nAdd this line to your application's Gemfile:\n\n```ruby\ngem 'chrono_forge'\n```\n\nThen execute:\n\n```bash\n$ bundle install\n```\n\nOr install it directly:\n\n```bash\n$ gem install chrono_forge\n```\n\nAfter installation, run the generator to create the necessary database migrations:\n\n```bash\n$ rails generate chrono_forge:install\n$ rails db:migrate\n```\n\n## Usage\n\n### Basic Workflow Example\n\nHere's a complete example of a durable order processing workflow:\n\n```ruby\nclass OrderProcessingWorkflow < ApplicationJob\n include ChronoForge::Executor\n\n def perform\n # Context can be used to pass and store data between executions\n context[\"order_id\"] = SecureRandom.hex\n\n # Wait until payment is confirmed\n wait_until :payment_confirmed?\n\n # Wait for potential fraud check\n wait 1.minute, :fraud_check_delay\n\n # Durably execute order processing\n durably_execute :process_order\n\n # Final steps\n complete_order\n end\n\n private\n\n def payment_confirmed?\n PaymentService.confirmed?(context[\"order_id\"])\n end\n\n def process_order\n context[\"processed_at\"] = Time.current.iso8601\n OrderProcessor.process(context[\"order_id\"])\n end\n\n def complete_order\n context[\"completed_at\"] = Time.current.iso8601\n OrderCompletionService.complete(context[\"order_id\"])\n end\nend\n```\n\n### Workflow Features\n\n#### Durable Execution\n\nThe `durably_execute` method ensures operations are executed exactly once:\n\n```ruby\n# Execute a method\ndurably_execute(:process_payment, max_attempts: 3)\n\n# Or with a block\ndurably_execute -> (ctx) {\n Payment.process(ctx[:payment_id])\n}\n```\n\n#### Wait States\n\nChronoForge supports both time-based and condition-based waits:\n\n```ruby\n# Wait for a specific duration\nwait 1.hour, :cooling_period\n\n# Wait until a condition is met\nwait_until :payment_processed, \n timeout: 1.hour,\n check_interval: 5.minutes\n```\n\n#### Workflow Context\n\nChronoForge provides a persistent context that survives job restarts:\n\n```ruby\n# Set context values\ncontext[:user_name] = \"John Doe\"\ncontext[:status] = \"processing\"\n\n# Read context values\nuser_name = context[:user_name]\n```\n\n### Error Handling\n\nChronoForge automatically tracks errors and provides retry capabilities:\n\n```ruby\nclass MyWorkflow < ApplicationJob\n include ChronoForge::Executor\n\n private\n\n def should_retry?(error, attempt_count)\n case error\n when NetworkError\n attempt_count < 5\n when ValidationError\n false # Don't retry validation errors\n else\n attempt_count < 3\n end\n end\nend\n```\n\n### Cleanup\n\nChronoForge includes built-in cleanup methods for managing old workflow data:\n\n```ruby\n# Clean up old workflows and logs\nChronoForge::Workflow.cleanup_old_logs(retention_period: 30.days)\nChronoForge::ExecutionLog.cleanup_old_logs(retention_period: 30.days)\nChronoForge::ErrorLog.cleanup_old_logs(retention_period: 30.days)\n```\n\n## Testing\n\nChronoForge is designed to be easily testable using [ChaoticJob](https://github.com/fractaledmind/chaotic_job), a testing framework that makes it simple to test complex job workflows. Here's how to set up your test environment:\n\n1. Add ChaoticJob to your Gemfile's test group:\n\n```ruby\ngroup :test do\n gem 'chaotic_job'\nend\n```\n\n2. Set up your test helper:\n\n```ruby\n# test_helper.rb\nrequire 'chrono_forge'\nrequire 'minitest/autorun'\nrequire 'chaotic_job'\n```\n\nExample test:\n\n```ruby\nclass WorkflowTest < Minitest::Test\n include ChaoticJob::Helpers\n\n def test_workflow_completion\n # Enqueue the job\n OrderProcessingWorkflow.perform_later(\"order_123\")\n \n # Perform all enqueued jobs\n perform_all_jobs\n \n # Assert workflow completed successfully\n workflow = ChronoForge::Workflow.last\n assert workflow.completed?\n \n # Check workflow context\n assert workflow.context[\"processed_at\"].present?\n assert workflow.context[\"completed_at\"].present?\n end\nend\n```\n\nChaoticJob provides several helpful methods for testing workflows:\n\n- `perform_all_jobs`: Processes all enqueued jobs, including those enqueued during job execution\n- `enqueued_jobs`: Returns the current number of jobs in the queue\n\nFor testing with specific job processing libraries like Sidekiq or Delayed Job, you can still use their respective testing modes, but ChaoticJob is recommended for testing ChronoForge workflows as it better handles the complexities of nested job scheduling and wait states.\n\n\n## Database Schema\n\nChronoForge creates three main tables:\n\n1. `chrono_forge_workflows`: Stores workflow state and context\n2. `chrono_forge_execution_logs`: Tracks individual execution steps\n3. `chrono_forge_error_logs`: Records detailed error information\n\n## Development\n\nAfter checking out the repo, run:\n\n```bash\n$ bin/setup # Install dependencies\n$ bundle exec rake test # Run the tests\n$ bin/appraise # Run the full suite of appraisals\n$ bin/console # Start an interactive console\n```\n\nThe test suite uses SQLite by default and includes:\n- Unit tests for core functionality\n- Integration tests with ActiveJob\n- Example workflow implementations\n\n## Contributing\n\n1. Fork the repository\n2. Create your feature branch (`git checkout -b feature/my-new-feature`)\n3. Commit your changes (`git commit -am 'Add some feature'`)\n4. Push to the branch (`git push origin feature/my-new-feature`)\n5. Create a new Pull Request\n\nPlease include tests for any new features or bug fixes.\n\n## License\n\nThis gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).\n"
9
+ },
10
+ {
11
+ "path": "/Users/stefan/Documents/plutonium/chrono_forge/chrono_forge.gemspec",
12
+ "contents": "# frozen_string_literal: true\n\nrequire_relative \"lib/chrono_forge/version\"\n\nGem::Specification.new do |spec|\n spec.name = \"chrono_forge\"\n spec.version = ChronoForge::VERSION\n spec.authors = [\"Stefan Froelich\"]\n spec.email = [\"sfroelich01@gmail.com\"]\n\n spec.summary = \"Base fields for the Phlexi libraries\"\n spec.description = \"Base fields for the Phlexi libraries\"\n spec.homepage = \"https://github.com/radioactive-labs/chrono_forge\"\n spec.license = \"MIT\"\n spec.required_ruby_version = \">= 3.2.2\"\n\n spec.metadata[\"allowed_push_host\"] = \"https://rubygems.org\"\n\n spec.metadata[\"homepage_uri\"] = spec.homepage\n spec.metadata[\"source_code_uri\"] = spec.homepage\n spec.metadata[\"changelog_uri\"] = spec.homepage\n\n # Specify which files should be added to the gem when it is released.\n # The `git ls-files -z` loads the files in the RubyGem that have been added into git.\n gemspec = File.basename(__FILE__)\n spec.files = IO.popen(%w[git ls-files -z], chdir: __dir__, err: IO::NULL) do |ls|\n ls.readlines(\"\\x0\", chomp: true).reject do |f|\n (f == gemspec) ||\n f.start_with?(*%w[bin/ test/ spec/ features/ .git .github appveyor Gemfile])\n end\n end\n spec.bindir = \"exe\"\n spec.executables = spec.files.grep(%r{\\Aexe/}) { |f| File.basename(f) }\n spec.require_paths = [\"lib\"]\n\n spec.add_dependency \"activerecord\"\n spec.add_dependency \"activejob\"\n spec.add_dependency \"zeitwerk\"\n\n spec.add_development_dependency \"rake\"\n spec.add_development_dependency \"minitest\"\n spec.add_development_dependency \"minitest-reporters\"\n spec.add_development_dependency \"standard\"\n # spec.add_development_dependency \"brakeman\"\n spec.add_development_dependency \"bundle-audit\"\n spec.add_development_dependency \"appraisal\"\n spec.add_development_dependency \"combustion\"\n spec.add_development_dependency \"chaotic_job\"\nend\n"
9
13
  },
10
14
  {
11
15
  "path": "/Users/stefan/Documents/plutonium/chrono_forge/export.rb",
12
- "contents": "require \"json\"\nrequire \"find\"\n\ndef export_files_to_json(directory, extensions, output_file, exceptions = [])\n # Convert extensions to lowercase for case-insensitive matching\n extensions = extensions.map(&:downcase)\n\n # Array to store file data\n files_data = []\n\n # Find all files in directory and subdirectories\n Find.find(directory) do |path|\n # Skip if not a file\n next unless File.file?(path)\n next if exceptions.any? { |exception| path.include?(exception) }\n\n # Check if file extension matches any in our list\n ext = File.extname(path).downcase[1..-1] # Remove the leading dot\n next unless extensions.include?(ext)\n\n puts path\n\n begin\n # Read file contents\n contents = File.read(path)\n\n # Add to our array\n files_data << {\n \"path\" => path,\n \"contents\" => contents\n }\n rescue => e\n puts \"Error reading file #{path}: #{e.message}\"\n end\n end\n\n # Write to JSON file\n File.write(output_file, JSON.pretty_generate(files_data))\n\n puts \"Successfully exported #{files_data.length} files to #{output_file}\"\nend\n\n# Example usage (uncomment and modify as needed):\ndirectory = \"/Users/stefan/Documents/plutonium/chrono_forge\"\nexceptions = [\"/.github/\", \"/.vscode/\", \"gemfiles\", \"pkg\", \"test\", \"node_modules\"]\nextensions = [\"rb\", \"md\", \"yml\", \"yaml\", \"gemspec\"]\noutput_file = \"export.json\"\nexport_files_to_json(directory, extensions, output_file, exceptions)\n"
16
+ "contents": "require \"json\"\nrequire \"find\"\n\ndef export_files_to_json(directory, extensions, output_file, exceptions = [])\n # Convert extensions to lowercase for case-insensitive matching\n extensions = extensions.map(&:downcase)\n\n # Array to store file data\n files_data = []\n\n # Find all files in directory and subdirectories\n Find.find(directory) do |path|\n # Skip if not a file\n next unless File.file?(path)\n next if exceptions.any? { |exception| path.include?(exception) }\n\n # Check if file extension matches any in our list\n ext = File.extname(path).downcase[1..-1] # Remove the leading dot\n next unless extensions.include?(ext)\n\n puts path\n\n begin\n # Read file contents\n contents = File.read(path)\n\n # Add to our array\n files_data << {\n \"path\" => path,\n \"contents\" => contents\n }\n rescue => e\n puts \"Error reading file #{path}: #{e.message}\"\n end\n end\n\n # Write to JSON file\n File.write(output_file, JSON.pretty_generate(files_data))\n\n puts \"Successfully exported #{files_data.length} files to #{output_file}\"\nend\n\n# Example usage (uncomment and modify as needed):\ndirectory = \"/Users/stefan/Documents/plutonium/chrono_forge\"\nexceptions = [\"/.github/\", \"/.vscode/\", \"gemfiles\", \"pkg\", \"node_modules\"]\nextensions = [\"rb\", \"md\", \"yml\", \"yaml\", \"gemspec\"]\noutput_file = \"export.json\"\nexport_files_to_json(directory, extensions, output_file, exceptions)\n"
13
17
  },
14
18
  {
15
- "path": "/Users/stefan/Documents/plutonium/chrono_forge/lib/chrono_forge/builder.rb",
16
- "contents": "# frozen_string_literal: true\n\nrequire \"phlex\"\n\nmodule Phlexi\n module Field\n # Builder class is responsible for building fields with various options and components.\n #\n # @attr_reader [Structure::DOM] dom The DOM structure for the field.\n # @attr_reader [Hash] options Options for the field.\n # @attr_reader [Object] object The object associated with the field.\n # @attr_reader [Hash] attributes Attributes for the field.\n # @attr_accessor [Object] value The value of the field.\n class Builder < Structure::Node\n include Phlex::Helpers\n include Options::Validators\n include Options::InferredTypes\n include Options::Multiple\n include Options::Labels\n include Options::Placeholders\n include Options::Descriptions\n include Options::Hints\n include Options::Associations\n include Options::Attachments\n\n class DOM < Structure::DOM; end\n\n class FieldCollection < Structure::FieldCollection; end\n\n attr_reader :dom, :options, :object, :value\n\n # Initializes a new FieldBuilder instance.\n #\n # @param key [Symbol, String] The key for the field.\n # @param parent [Structure::Namespace] The parent object.\n # @param object [Object, nil] The associated object.\n # @param value [Object] The initial value for the field.\n # @param options [Hash] Additional options for the field.\n def initialize(key, parent:, object: nil, value: NIL_VALUE, **options)\n super(key, parent: parent)\n\n @object = object\n @value = determine_initial_value(value)\n @options = options\n @dom = self.class::DOM.new(field: self)\n end\n\n # Creates a repeated field collection.\n #\n # @param range [#each] The collection of items to generate displays for.\n # @yield [block] The block to be executed for each item in the collection.\n # @return [FieldCollection] The field collection.\n def repeated(collection = nil, &)\n self.class::FieldCollection.new(field: self, collection:, &)\n end\n\n def has_value?\n attachment_reflection.present? ? value.attached? : (value.present? || value == false)\n end\n\n protected\n\n def determine_initial_value(value)\n return value unless value == NIL_VALUE\n\n determine_value_from_object\n end\n\n def determine_value_from_object\n ChronoForge::Support::Value.from(object, key)\n end\n end\n end\nend\n"
19
+ "path": "/Users/stefan/Documents/plutonium/chrono_forge/lib/chrono_forge/error_log.rb",
20
+ "contents": "# frozen_string_literal: true\n\n# == Schema Information\n#\n# Table name: chrono_forge_error_logs\n#\n# id :integer not null, primary key\n# backtrace :text\n# context :json\n# error_class :string\n# error_message :text\n# created_at :datetime not null\n# updated_at :datetime not null\n# workflow_id :integer not null\n#\n# Indexes\n#\n# index_chrono_forge_error_logs_on_workflow_id (workflow_id)\n#\n# Foreign Keys\n#\n# workflow_id (workflow_id => chrono_forge_workflows.id)\n#\n\nmodule ChronoForge\n class ErrorLog < ActiveRecord::Base\n self.table_name = \"chrono_forge_error_logs\"\n\n belongs_to :workflow\n\n # Cleanup method\n def self.cleanup_old_logs(retention_period: 30.days)\n where(\"created_at < ?\", retention_period.ago).delete_all\n end\n end\nend\n"
17
21
  },
18
22
  {
19
- "path": "/Users/stefan/Documents/plutonium/chrono_forge/lib/chrono_forge/components/base.rb",
20
- "contents": "# frozen_string_literal: true\n\nmodule Phlexi\n module Field\n module Components\n class Base < COMPONENT_BASE\n attr_reader :field, :attributes\n\n def initialize(field, **attributes)\n @field = field\n @attributes = attributes\n\n build_attributes\n build_component_class\n end\n\n protected\n\n def build_attributes\n attributes.fetch(:id) { attributes[:id] = \"#{field.dom.id}_#{component_name}\" }\n end\n\n def build_component_class\n return if attributes[:class] == false\n\n attributes[:class] = tokens(component_name, attributes[:class])\n end\n\n def component_name\n @component_name ||= self.class.name.demodulize.underscore\n end\n end\n end\n end\nend\n"
23
+ "path": "/Users/stefan/Documents/plutonium/chrono_forge/lib/chrono_forge/execution_log.rb",
24
+ "contents": "# frozen_string_literal: true\n\n# == Schema Information\n#\n# Table name: chrono_forge_execution_logs\n#\n# id :integer not null, primary key\n# attempts :integer default(0), not null\n# completed_at :datetime\n# error_class :string\n# error_message :text\n# last_executed_at :datetime\n# metadata :json\n# started_at :datetime\n# state :integer default(\"pending\"), not null\n# step_name :string not null\n# created_at :datetime not null\n# updated_at :datetime not null\n# workflow_id :integer not null\n#\n# Indexes\n#\n# idx_on_workflow_id_step_name_11bea8586e (workflow_id,step_name) UNIQUE\n# index_chrono_forge_execution_logs_on_workflow_id (workflow_id)\n#\n# Foreign Keys\n#\n# workflow_id (workflow_id => chrono_forge_workflows.id)\n#\nmodule ChronoForge\n class ExecutionLog < ActiveRecord::Base\n self.table_name = \"chrono_forge_execution_logs\"\n\n belongs_to :workflow\n\n enum :state, %i[\n pending\n completed\n failed\n ]\n\n # Cleanup method\n def self.cleanup_old_logs(retention_period: 30.days)\n where(\"created_at < ?\", retention_period.ago).delete_all\n end\n end\nend\n"
21
25
  },
22
26
  {
23
- "path": "/Users/stefan/Documents/plutonium/chrono_forge/lib/chrono_forge/options/associations.rb",
24
- "contents": "# frozen_string_literal: true\n\nmodule Phlexi\n module Field\n module Options\n module Associations\n def association_reflection\n @association_reflection ||= find_association_reflection\n end\n\n protected\n\n def find_association_reflection\n if object.class.respond_to?(:reflect_on_association)\n object.class.reflect_on_association(key)\n end\n end\n end\n end\n end\nend\n"
27
+ "path": "/Users/stefan/Documents/plutonium/chrono_forge/lib/chrono_forge/executor/context.rb",
28
+ "contents": "module ChronoForge\n module Executor\n class Context\n class ValidationError < Error; end\n\n ALLOWED_TYPES = [\n String,\n Integer,\n Float,\n TrueClass,\n FalseClass,\n NilClass,\n Hash,\n Array\n ]\n\n def initialize(workflow)\n @workflow = workflow\n @context = workflow.context || {}\n @dirty = false\n end\n\n def []=(key, value)\n # Type and size validation\n validate_value!(value)\n\n @context[key.to_s] =\n if value.is_a?(Hash) || value.is_a?(Array)\n deep_dup(value)\n else\n value\n end\n\n @dirty = true\n end\n\n def [](key)\n @context[key.to_s]\n end\n\n def save!\n return unless @dirty\n\n @workflow.update_column(:context, @context)\n @dirty = false\n end\n\n private\n\n def validate_value!(value)\n unless ALLOWED_TYPES.any? { |type| value.is_a?(type) }\n raise ValidationError, \"Unsupported context value type: #{value.inspect}\"\n end\n\n # Optional: Add size constraints\n if value.is_a?(String) && value.size > 64.kilobytes\n raise ValidationError, \"Context value too large\"\n end\n end\n\n def deep_dup(obj)\n JSON.parse(JSON.generate(obj))\n rescue\n obj.dup\n end\n end\n end\nend\n"
25
29
  },
26
30
  {
27
- "path": "/Users/stefan/Documents/plutonium/chrono_forge/lib/chrono_forge/options/attachments.rb",
28
- "contents": "# frozen_string_literal: true\n\nmodule Phlexi\n module Field\n module Options\n module Attachments\n def attachment_reflection\n @attachment_reflection ||= find_attachment_reflection\n end\n\n protected\n\n def find_attachment_reflection\n if object.class.respond_to?(:reflect_on_attachment)\n object.class.reflect_on_attachment(key)\n end\n end\n end\n end\n end\nend\n"
31
+ "path": "/Users/stefan/Documents/plutonium/chrono_forge/lib/chrono_forge/executor/execution_tracker.rb",
32
+ "contents": "module ChronoForge\n module Executor\n class ExecutionTracker\n def self.track_error(workflow, error)\n # Create a detailed error log\n ErrorLog.create!(\n workflow: workflow,\n error_class: error.class.name,\n error_message: error.message,\n backtrace: error.backtrace.join(\"\\n\"),\n context: workflow.context\n )\n end\n end\n end\nend\n"
29
33
  },
30
34
  {
31
- "path": "/Users/stefan/Documents/plutonium/chrono_forge/lib/chrono_forge/options/descriptions.rb",
32
- "contents": "# frozen_string_literal: true\n\nmodule Phlexi\n module Field\n module Options\n module Descriptions\n def description(description = nil)\n if description.nil?\n options[:description]\n else\n options[:description] = description\n self\n end\n end\n\n def has_description?\n description.present?\n end\n end\n end\n end\nend\n"
35
+ "path": "/Users/stefan/Documents/plutonium/chrono_forge/lib/chrono_forge/executor/lock_strategy.rb",
36
+ "contents": "module ChronoForge\n module Executor\n class LongRunningConcurrentExecutionError < Error; end\n\n class ConcurrentExecutionError < Error; end\n\n class LockStrategy\n def self.acquire_lock(job_id, workflow, max_duration:)\n ActiveRecord::Base.transaction do\n # Find the workflow with a lock, considering stale locks\n workflow = workflow.lock!\n\n # Check for active execution\n if workflow.locked_at && workflow.locked_at > max_duration.ago\n raise ConcurrentExecutionError, \"Job currently in progress\"\n end\n\n # Atomic update of lock status\n workflow.update_columns(\n locked_by: job_id,\n locked_at: Time.current,\n state: :running\n )\n\n workflow\n end\n end\n\n def self.release_lock(job_id, workflow)\n workflow = workflow.reload\n if workflow.locked_by != job_id\n raise LongRunningConcurrentExecutionError,\n \"#{self.class}(#{job_id}) executed longer than specified max_duration, \" \\\n \"allowing another instance(#{workflow.locked_by}) to acquire the lock.\"\n end\n\n columns = {locked_at: nil, locked_by: nil}\n columns[:state] = :idle if workflow.running?\n\n workflow.update_columns(columns)\n end\n end\n end\nend\n"
33
37
  },
34
38
  {
35
- "path": "/Users/stefan/Documents/plutonium/chrono_forge/lib/chrono_forge/options/hints.rb",
36
- "contents": "# frozen_string_literal: true\n\nmodule Phlexi\n module Field\n module Options\n module Hints\n def hint(hint = nil)\n if hint.nil?\n options[:hint]\n else\n options[:hint] = hint\n self\n end\n end\n\n def has_hint?\n hint.present?\n end\n end\n end\n end\nend\n"
39
+ "path": "/Users/stefan/Documents/plutonium/chrono_forge/lib/chrono_forge/executor/methods/durably_execute.rb",
40
+ "contents": "module ChronoForge\n module Executor\n module Methods\n module DurablyExecute\n def durably_execute(method, **options)\n # Create execution log\n step_name = \"durably_execute$#{method}\"\n execution_log = ExecutionLog.create_or_find_by!(\n workflow: @workflow,\n step_name: step_name\n ) do |log|\n log.started_at = Time.current\n end\n\n # Return if already completed\n return execution_log.metadata[\"result\"] if execution_log.completed?\n\n # Execute with error handling\n begin\n # Update execution log with attempt\n execution_log.update!(\n attempts: execution_log.attempts + 1,\n last_executed_at: Time.current\n )\n\n # Execute the method\n result = if method.is_a?(Symbol)\n send(method)\n else\n method.call(@context)\n end\n\n # Complete the execution\n execution_log.update!(\n state: :completed,\n completed_at: Time.current,\n metadata: {result: result}\n )\n result\n rescue HaltExecutionFlow\n raise\n rescue => e\n # Log the error\n Rails.logger.error { \"Error while durably executing #{method}: #{e.message}\" }\n self.class::ExecutionTracker.track_error(workflow, e)\n\n # Optional retry logic\n if execution_log.attempts < (options[:max_attempts] || 3)\n # Reschedule with exponential backoff\n backoff = (2**[execution_log.attempts || 1, 5].min).seconds\n\n self.class\n .set(wait: backoff)\n .perform_later(\n @workflow.key,\n retry_method: method\n )\n\n # Halt current execution\n halt_execution!\n else\n # Max attempts reached\n execution_log.update!(\n state: :failed,\n error_message: e.message,\n error_class: e.class.name\n )\n raise ExecutionFailedError, \"#{step_name} failed after maximum attempts\"\n end\n end\n end\n end\n end\n end\nend\n"
37
41
  },
38
42
  {
39
- "path": "/Users/stefan/Documents/plutonium/chrono_forge/lib/chrono_forge/options/inferred_types.rb",
40
- "contents": "# frozen_string_literal: true\n\nrequire \"bigdecimal\"\n\nmodule Phlexi\n module Field\n module Options\n module InferredTypes\n def inferred_field_component\n @inferred_component ||= infer_field_component\n end\n\n def inferred_field_type\n @inferred_field_type ||= infer_field_type\n end\n\n def inferred_string_field_type\n @inferred_string_field_type || infer_string_field_type\n end\n\n private\n\n def infer_field_component\n inferred_field_type\n end\n\n def infer_field_type\n # Check attachments first since they are implemented as associations\n return :attachment if attachment_reflection\n\n return :association if association_reflection\n\n if object.class.respond_to?(:defined_enums)\n return :enum if object.class.defined_enums.key?(key.to_s)\n end\n\n if object.class.respond_to?(:columns_hash)\n # ActiveRecord\n column = object.class.columns_hash[key.to_s]\n return column.type if column&.type\n end\n\n if object.class.respond_to?(:attribute_types)\n # ActiveModel::Attributes\n custom_type = object.class.attribute_types[key.to_s]\n return custom_type.type if custom_type&.type\n end\n\n # Fallback to inferring type from the value\n value = ChronoForge::Support::Value.from(object, key)\n return infer_field_type_from_value(value) unless value.nil?\n\n # Default to string if we can't determine the type\n :string\n end\n\n def infer_field_type_from_value(value)\n case value\n when Integer\n :integer\n when Float\n :float\n when BigDecimal\n :decimal\n when TrueClass, FalseClass\n :boolean\n when Date\n :date\n when Time, DateTime\n :datetime\n when Hash\n :json\n else\n :string\n end\n end\n\n def infer_string_field_type\n infer_string_field_type_from_key || infer_string_field_type_from_validations\n end\n\n def infer_string_field_type_from_validations\n return unless has_validators?\n\n if attribute_validators.find { |v| v.kind == :numericality }\n :number\n elsif attribute_validators.find { |v| v.kind == :format && v.options[:with] == URI::MailTo::EMAIL_REGEXP }\n :email\n end\n end\n\n def infer_string_field_type_from_key\n key = self.key.to_s.downcase\n return :password if is_password_field?(key)\n\n custom_mappings = {\n /url$|^link|^site/ => :url,\n /^email/ => :email,\n /^search/ => :search,\n /phone|tel(ephone)?/ => :phone,\n /^time/ => :time,\n /^date/ => :date,\n /^number|_count$|_amount$/ => :number,\n /^color|_color$/ => :color\n }\n\n custom_mappings.each do |pattern, type|\n return type if key.match?(pattern)\n end\n\n nil\n end\n\n def is_password_field?(key)\n exact_matches = [\"password\"]\n prefixes = [\"encrypted_\"]\n suffixes = [\"_password\", \"_digest\", \"_hash\", \"_token\"]\n\n exact_matches.include?(key) ||\n prefixes.any? { |prefix| key.start_with?(prefix) } ||\n suffixes.any? { |suffix| key.end_with?(suffix) }\n end\n end\n end\n end\nend\n"
43
+ "path": "/Users/stefan/Documents/plutonium/chrono_forge/lib/chrono_forge/executor/methods/wait.rb",
44
+ "contents": "module ChronoForge\n module Executor\n module Methods\n module Wait\n def wait(duration, name, **options)\n # Create execution log\n execution_log = ExecutionLog.create_or_find_by!(\n workflow: @workflow,\n step_name: \"wait$#{name}\"\n ) do |log|\n log.started_at = Time.current\n log.metadata = {\n wait_until: duration.from_now\n }\n end\n\n # Return if already completed\n return if execution_log.completed?\n\n # Check if wait period has passed\n if Time.current >= Time.parse(execution_log.metadata[\"wait_until\"])\n execution_log.update!(\n attempts: execution_log.attempts + 1,\n state: :completed,\n completed_at: Time.current,\n last_executed_at: Time.current\n )\n return\n end\n\n execution_log.update!(\n attempts: execution_log.attempts + 1,\n last_executed_at: Time.current\n )\n\n # Reschedule the job\n self.class\n .set(wait: duration)\n .perform_later(@workflow.key)\n\n # Halt current execution\n halt_execution!\n end\n end\n end\n end\nend\n"
41
45
  },
42
46
  {
43
- "path": "/Users/stefan/Documents/plutonium/chrono_forge/lib/chrono_forge/options/labels.rb",
44
- "contents": "# frozen_string_literal: true\n\nmodule Phlexi\n module Field\n module Options\n module Labels\n def label(label = nil)\n if label.nil?\n options[:label] = options.fetch(:label) { calculate_label }\n else\n options[:label] = label\n self\n end\n end\n\n private\n\n def calculate_label\n if object.class.respond_to?(:human_attribute_name)\n object.class.human_attribute_name(key.to_s, {base: object})\n else\n key.to_s.humanize\n end\n end\n end\n end\n end\nend\n"
47
+ "path": "/Users/stefan/Documents/plutonium/chrono_forge/lib/chrono_forge/executor/methods/wait_until.rb",
48
+ "contents": "module ChronoForge\n module Executor\n class WaitConditionNotMet < ExecutionFailedError; end\n\n module Methods\n module WaitUntil\n def wait_until(condition, **options)\n # Default timeout and check interval\n timeout = options[:timeout] || 10.seconds\n check_interval = options[:check_interval] || 1.second\n\n # Find or create execution log\n step_name = \"wait_until$#{condition}\"\n execution_log = ExecutionLog.create_or_find_by!(\n workflow: @workflow,\n step_name: step_name\n ) do |log|\n log.started_at = Time.current\n log.metadata = {\n timeout_at: timeout.from_now,\n check_interval: check_interval,\n condition: condition.to_s\n }\n end\n\n # Return if already completed\n if execution_log.completed?\n return execution_log.metadata[\"result\"]\n end\n\n # Evaluate condition\n begin\n execution_log.update!(\n attempts: execution_log.attempts + 1,\n last_executed_at: Time.current\n )\n\n condition_met = if condition.is_a?(Proc)\n condition.call(@context)\n elsif condition.is_a?(Symbol)\n send(condition)\n else\n raise ArgumentError, \"Unsupported condition type\"\n end\n rescue HaltExecutionFlow\n raise\n rescue => e\n # Log the error\n Rails.logger.error { \"Error evaluating condition #{condition}: #{e.message}\" }\n self.class::ExecutionTracker.track_error(workflow, e)\n\n # Optional retry logic\n if (options[:retry_on] || []).include?(e.class)\n # Reschedule with exponential backoff\n backoff = (2**[execution_log.attempts || 1, 5].min).seconds\n\n self.class\n .set(wait: backoff)\n .perform_later(\n @workflow.key\n )\n\n # Halt current execution\n halt_execution!\n else\n execution_log.update!(\n state: :failed,\n error_message: e.message,\n error_class: e.class.name\n )\n raise ExecutionFailedError, \"#{step_name} failed with an error: #{e.message}\"\n end\n end\n\n # Handle condition met\n if condition_met\n execution_log.update!(\n state: :completed,\n completed_at: Time.current,\n metadata: execution_log.metadata.merge(\"result\" => true)\n )\n return true\n end\n\n # Check for timeout\n metadata = execution_log.metadata\n if Time.current > metadata[\"timeout_at\"]\n execution_log.update!(\n state: :failed,\n metadata: metadata.merge(\"result\" => nil)\n )\n Rails.logger.warn { \"Timeout reached for condition #{condition}. Condition not met within the timeout period.\" }\n raise WaitConditionNotMet, \"Condition not met within timeout period\"\n end\n\n # Reschedule with delay\n self.class\n .set(wait: check_interval)\n .perform_later(\n @workflow.key,\n wait_condition: condition\n )\n\n # Halt current execution\n halt_execution!\n end\n end\n end\n end\nend\n"
45
49
  },
46
50
  {
47
- "path": "/Users/stefan/Documents/plutonium/chrono_forge/lib/chrono_forge/options/multiple.rb",
48
- "contents": "# frozen_string_literal: true\n\nmodule Phlexi\n module Field\n module Options\n module Multiple\n def multiple?\n options[:multiple] = options.fetch(:multiple) { calculate_multiple_field_value }\n end\n\n def multiple!(multiple = true)\n options[:multiple] = multiple\n self\n end\n\n private\n\n def calculate_multiple_field_value\n return true if attachment_reflection&.macro == :has_many_attached\n return true if %i[has_many has_and_belongs_to_many].include?(association_reflection&.macro)\n return true if multiple_field_array_attribute?\n\n check_multiple_field_from_validators\n end\n\n def multiple_field_array_attribute?\n return false unless object.class.respond_to?(:columns_hash)\n\n column = object.class.columns_hash[key.to_s]\n return false unless column\n\n case object.class.connection.adapter_name.downcase\n when \"postgresql\"\n column.array? || (column.type == :string && column.sql_type.include?(\"[]\"))\n end # || object.class.attribute_types[key.to_s].is_a?(ActiveRecord::Type::Serialized)\n rescue\n # Rails.logger.warn(\"Error checking multiple field array attribute: #{e.message}\")\n false\n end\n\n def check_multiple_field_from_validators\n inclusion_validator = find_validator(:inclusion)\n length_validator = find_validator(:length)\n\n return false unless inclusion_validator || length_validator\n\n check_multiple_field_inclusion_validator(inclusion_validator) ||\n check_multiple_field_length_validator(length_validator)\n end\n\n def check_multiple_field_inclusion_validator(validator)\n return false unless validator\n in_option = validator.options[:in] || validator.options[:within]\n return false unless in_option.is_a?(Array)\n\n validator.options[:multiple] == true || (multiple_field_array_attribute? && in_option.size > 1)\n end\n\n def check_multiple_field_length_validator(validator)\n return false unless validator\n validator.options[:maximum].to_i > 1 if validator.options[:maximum]\n end\n end\n end\n end\nend\n"
51
+ "path": "/Users/stefan/Documents/plutonium/chrono_forge/lib/chrono_forge/executor/methods.rb",
52
+ "contents": "module ChronoForge\n module Executor\n module Methods\n include Methods::Wait\n include Methods::WaitUntil\n include Methods::DurablyExecute\n end\n end\nend\n"
49
53
  },
50
54
  {
51
- "path": "/Users/stefan/Documents/plutonium/chrono_forge/lib/chrono_forge/options/placeholders.rb",
52
- "contents": "# frozen_string_literal: true\n\nmodule Phlexi\n module Field\n module Options\n module Placeholders\n def placeholder(placeholder = nil)\n if placeholder.nil?\n options[:placeholder]\n else\n\n options[:placeholder] = placeholder\n self\n end\n end\n end\n end\n end\nend\n"
55
+ "path": "/Users/stefan/Documents/plutonium/chrono_forge/lib/chrono_forge/executor/retry_strategy.rb",
56
+ "contents": "module ChronoForge\n module Executor\n class RetryStrategy\n BACKOFF_STRATEGY = [\n 1.second, # Initial retry\n 5.seconds, # Second retry\n 30.seconds, # Third retry\n 2.minutes, # Fourth retry\n 10.minutes # Final retry\n ]\n\n def self.schedule_retry(workflow, attempt: 0)\n wait_duration = BACKOFF_STRATEGY[attempt] || BACKOFF_STRATEGY.last\n\n # Schedule with exponential backoff\n workflow.job_klass.constantize\n .set(wait: wait_duration)\n .perform_later(\n workflow.key,\n attempt: attempt + 1\n )\n end\n\n def self.max_attempts\n BACKOFF_STRATEGY.length\n end\n end\n end\nend\n"
53
57
  },
54
58
  {
55
- "path": "/Users/stefan/Documents/plutonium/chrono_forge/lib/chrono_forge/options/validators.rb",
56
- "contents": "# frozen_string_literal: true\n\nmodule Phlexi\n module Field\n module Options\n module Validators\n private\n\n def has_validators?\n @has_validators ||= object.class.respond_to?(:validators_on)\n end\n\n def attribute_validators\n object.class.validators_on(key)\n end\n\n def association_reflection_validators\n association_reflection ? object.class.validators_on(association_reflection.name) : []\n end\n\n def valid_validator?(validator)\n !conditional_validators?(validator) && action_validator_match?(validator)\n end\n\n def conditional_validators?(validator)\n validator.options.include?(:if) || validator.options.include?(:unless)\n end\n\n def action_validator_match?(validator)\n return true unless validator.options.include?(:on)\n\n case validator.options[:on]\n when :save\n true\n when :create\n !object.persisted?\n when :update\n object.persisted?\n end\n end\n\n def find_validator(kind)\n attribute_validators.find { |v| v.kind == kind && valid_validator?(v) } if has_validators?\n end\n end\n end\n end\nend\n"
59
+ "path": "/Users/stefan/Documents/plutonium/chrono_forge/lib/chrono_forge/executor.rb",
60
+ "contents": "module ChronoForge\n module Executor\n class Error < StandardError; end\n\n class ExecutionFailedError < Error; end\n\n class ExecutionFlowControl < Error; end\n\n class HaltExecutionFlow < ExecutionFlowControl; end\n\n include Methods\n\n def perform(key, attempt: 0, **kwargs)\n # Prevent excessive retries\n if attempt >= self.class::RetryStrategy.max_attempts\n Rails.logger.error { \"Max attempts reached for job #{key}\" }\n return\n end\n\n # Find or create job with comprehensive tracking\n setup_workflow(key, kwargs)\n\n begin\n # Skip if workflow cannot be executed\n return unless workflow.executable?\n\n # Acquire lock with advanced concurrency protection\n self.class::LockStrategy.acquire_lock(job_id, workflow, max_duration: max_duration)\n\n # Execute core job logic\n super(**workflow.kwargs.symbolize_keys)\n\n # Mark as complete\n complete_workflow!\n rescue ExecutionFailedError => e\n Rails.logger.error { \"Execution step failed for #{key}\" }\n self.class::ExecutionTracker.track_error(workflow, e)\n workflow.stalled!\n nil\n rescue HaltExecutionFlow\n # Halt execution\n Rails.logger.debug { \"Execution halted for #{key}\" }\n nil\n rescue ConcurrentExecutionError\n # Graceful handling of concurrent execution\n Rails.logger.warn { \"Concurrent execution detected for job #{key}\" }\n nil\n rescue => e\n Rails.logger.error { \"An error occurred during execution of #{key}\" }\n self.class::ExecutionTracker.track_error(workflow, e)\n\n # Retry if applicable\n if should_retry?(e, attempt)\n self.class::RetryStrategy.schedule_retry(workflow, attempt: attempt)\n else\n workflow.failed!\n end\n ensure\n context.save!\n # Always release the lock\n self.class::LockStrategy.release_lock(job_id, workflow)\n end\n end\n\n private\n\n def complete_workflow!\n # Create an execution log for workflow completion\n execution_log = ExecutionLog.create_or_find_by!(\n workflow: workflow,\n step_name: \"$workflow_completion$\"\n ) do |log|\n log.started_at = Time.current\n log.metadata = {\n workflow_id: workflow.id\n }\n end\n\n begin\n execution_log.update!(\n attempts: execution_log.attempts + 1,\n last_executed_at: Time.current\n )\n\n workflow.completed_at = Time.current\n workflow.completed!\n\n # Mark execution log as completed\n execution_log.update!(\n state: :completed,\n completed_at: Time.current\n )\n\n # Return the execution log for tracking\n execution_log\n rescue => e\n # Log any completion errors\n execution_log.update!(\n state: :failed,\n error_message: e.message,\n error_class: e.class.name\n )\n raise\n end\n end\n\n def setup_workflow(key, kwargs)\n @workflow = find_workflow(key, kwargs)\n @context = Context.new(@workflow)\n end\n\n def find_workflow(key, kwargs)\n Workflow.create_or_find_by!(key: key) do |workflow|\n workflow.job_klass = self.class.to_s\n workflow.kwargs = kwargs\n workflow.started_at = Time.current\n end\n end\n\n def should_retry?(error, attempt_count)\n attempt_count < 3\n end\n\n def halt_execution!\n raise HaltExecutionFlow\n end\n\n def workflow\n @workflow\n end\n\n def context\n @context\n end\n\n def max_duration\n 10.minutes\n end\n end\nend\n"
57
61
  },
58
62
  {
59
- "path": "/Users/stefan/Documents/plutonium/chrono_forge/lib/chrono_forge/structure/dom.rb",
60
- "contents": "# frozen_string_literal: true\n\nmodule Phlexi\n module Field\n module Structure\n # Generates DOM IDs, names, etc. for a Field, Namespace, or Node based on\n # norms that were established by Rails. These can be used outside of or Rails in\n # other Ruby web frameworks since it has no dependencies on Rails.\n class DOM\n def initialize(field:)\n @field = field\n end\n\n # Converts the value of the field to a String, which is required to work\n # with Phlex. Assumes that `Object#to_s` emits a format suitable for display.\n def value\n @field.value.to_s\n end\n\n # Walks from the current node to the parent node, grabs the names, and separates\n # them with a `_` for a DOM ID.\n def id\n @id ||= begin\n root, *rest = lineage\n root_key = if root.respond_to?(:dom_id)\n root.dom_id\n else\n root.key\n end\n rest.map(&:key).unshift(root_key).join(\"_\")\n end\n end\n\n # The `name` attribute of a node, which is influenced by Rails.\n # All node names, except the parent node, are wrapped in a `[]` and collections\n # are left empty. For example, `user[addresses][][street]` would be created for a form with\n # data shaped like `{user: {addresses: [{street: \"Sesame Street\"}]}}`.\n def name\n @name ||= begin\n root, *names = keys\n names.map { |name| \"[#{name}]\" }.unshift(root).join\n end\n end\n\n # One-liner way of walking from the current node all the way up to the parent.\n def lineage\n @lineage ||= Enumerator.produce(@field, &:parent).take_while(&:itself).reverse\n end\n\n # Emit the id, name, and value in an HTML tag-ish that doesnt have an element.\n def inspect\n \"<#{self.class.name} id=#{id.inspect} name=#{name.inspect} value=#{value.inspect}/>\"\n end\n\n private\n\n def keys\n @keys ||= lineage.map do |node|\n # If the parent of a field is a field, the name should be nil.\n node.key unless node.parent.is_a? Builder\n end\n end\n end\n end\n end\nend\n"
63
+ "path": "/Users/stefan/Documents/plutonium/chrono_forge/lib/chrono_forge/version.rb",
64
+ "contents": "# frozen_string_literal: true\n\nmodule ChronoForge\n VERSION = \"0.1.0\"\nend\n"
61
65
  },
62
66
  {
63
- "path": "/Users/stefan/Documents/plutonium/chrono_forge/lib/chrono_forge/structure/field_collection.rb",
64
- "contents": "# frozen_string_literal: true\n\nmodule Phlexi\n module Field\n module Structure\n class FieldCollection\n include Enumerable\n\n class Builder\n include Phlex::Helpers\n\n attr_reader :key, :index\n\n def initialize(key, field, index)\n @key = key.to_s\n @field = field\n @index = index\n end\n\n def field(**)\n @field.class.new(key, **, parent: @field).tap do |field|\n yield field if block_given?\n end\n end\n end\n\n def initialize(field:, collection:, &)\n @field = field\n @collection = build_collection(collection)\n each(&) if block_given?\n end\n\n def each(&)\n @collection.each.with_index do |item, index|\n yield self.class::Builder.new(item, @field, index)\n end\n end\n\n private\n\n def build_collection(collection)\n collection\n end\n end\n end\n end\nend\n"
67
+ "path": "/Users/stefan/Documents/plutonium/chrono_forge/lib/chrono_forge/workflow.rb",
68
+ "contents": "# frozen_string_literal: true\n\n# == Schema Information\n#\n# Table name: chrono_forge_workflows\n#\n# id :integer not null, primary key\n# completed_at :datetime\n# context :json not null\n# job_klass :string not null\n# key :string not null\n# kwargs :json not null\n# locked_at :datetime\n# started_at :datetime\n# state :integer default(\"idle\"), not null\n# created_at :datetime not null\n# updated_at :datetime not null\n#\n# Indexes\n#\n# index_chrono_forge_workflows_on_key (key) UNIQUE\n#\nmodule ChronoForge\n class Workflow < ActiveRecord::Base\n self.table_name = \"chrono_forge_workflows\"\n\n has_many :execution_logs\n has_many :error_logs\n\n enum :state, %i[\n idle\n running\n completed\n failed\n stalled\n ]\n\n # Cleanup method\n def self.cleanup_old_logs(retention_period: 30.days)\n where(\"created_at < ?\", retention_period.ago).delete_all\n end\n\n # Serialization for metadata\n serialize :metadata, coder: JSON\n\n def executable?\n idle? || running?\n end\n end\nend\n"
65
69
  },
66
70
  {
67
- "path": "/Users/stefan/Documents/plutonium/chrono_forge/lib/chrono_forge/structure/namespace.rb",
68
- "contents": "# frozen_string_literal: true\n\nmodule Phlexi\n module Field\n module Structure\n # A Namespace maps an object to values, but doesn't actually have a value itself. For\n # example, a `User` object or ActiveRecord model could be passed into the `:user` namespace.\n #\n # To access single values on a Namespace, #field can be used.\n #\n # To access nested objects within a namespace, two methods are available:\n #\n # 1. #nest_one: Used for single nested objects, such as if a `User belongs_to :profile` in\n # ActiveRecord. This method returns another Namespace object.\n #\n # 2. #nest_many: Used for collections of nested objects, such as if a `User has_many :addresses` in\n # ActiveRecord. This method returns a NamespaceCollection object.\n class Namespace < Structure::Node\n include Enumerable\n\n class NamespaceCollection < Structure::NamespaceCollection; end\n\n attr_reader :builder_klass, :object\n\n def initialize(key, parent:, builder_klass:, object: nil, dom_id: nil)\n super(key, parent: parent)\n @builder_klass = builder_klass\n @object = object\n @dom_id = dom_id\n @children = {}\n yield self if block_given?\n end\n\n def field(key, template: false, **attributes)\n create_child(key, attributes.delete(:builder_klass) || builder_klass, object: object, template:, **attributes).tap do |field|\n yield field if block_given?\n end\n end\n\n # Creates a `Namespace` child instance with the parent set to the current instance, adds to\n # the `@children` Hash to ensure duplicate child namespaces aren't created, then calls the\n # method on the `@object` to get the child object to pass into that namespace.\n #\n # For example, if a `User#permission` returns a `Permission` object, we could map that to a\n # form like this:\n #\n # ```ruby\n # Phlexi::Form(User.new, as: :user) do\n # nest_one :profile do |profile|\n # render profile.field(:gender).input_tag\n # end\n # end\n # ```\n def nest_one(key, as: nil, object: nil, default: nil, template: false, &)\n object ||= object_value_for(key: key) || default\n key = as || key\n create_child(key, self.class, object:, template:, builder_klass:, &)\n end\n\n # Wraps an array of objects in Namespace classes. For example, if `User#addresses` returns\n # an enumerable or array of `Address` classes:\n #\n # ```ruby\n # Phlexi::Form(User.new) do\n # render field(:email).input_tag\n # render field(:name).input_tag\n # nest_many :addresses do |address|\n # render address.field(:street).input_tag\n # render address.field(:state).input_tag\n # render address.field(:zip).input_tag\n # end\n # end\n # ```\n # The object within the block is a `Namespace` object that maps each object within the enumerable\n # to another `Namespace` or `Field`.\n def nest_many(key, as: nil, collection: nil, default: nil, template: false, &)\n collection ||= Array(object_value_for(key: key)).presence || default\n key = as || key\n create_child(key, self.class::NamespaceCollection, collection:, template:, &)\n end\n\n # Iterates through the children of the current namespace, which could be `Namespace` or `Field`\n # objects.\n def each(&)\n @children.values.each(&)\n end\n\n def dom_id\n @dom_id ||= begin\n object_id = if object.nil?\n nil\n elsif (primary_key = ChronoForge.object_primary_key(object))\n primary_key&.to_s || :new\n end\n [key, object_id].compact.join(\"_\").underscore\n end\n end\n\n # Creates a root Namespace.\n def self.root(*, builder_klass:, **, &)\n new(*, parent: nil, builder_klass:, **, &)\n end\n\n protected\n\n def object_value_for(key:)\n ChronoForge::Support::Value.from(@object, key)\n end\n\n private\n\n # Checks if the child exists. If it does then it returns that. If it doesn't, it will\n # build the child.\n def create_child(key, child_class, template: false, **, &)\n if template\n child_class.new(key, parent: self, **, &)\n else\n @children.fetch(key) { @children[key] = child_class.new(key, parent: self, **, &) }\n end\n end\n end\n end\n end\nend\n"
71
+ "path": "/Users/stefan/Documents/plutonium/chrono_forge/lib/chrono_forge.rb",
72
+ "contents": "# frozen_string_literal: true\n\nrequire \"zeitwerk\"\nrequire \"active_record\"\nrequire \"active_job\"\n\nmodule Chronoforge\n Loader = Zeitwerk::Loader.for_gem.tap do |loader|\n loader.ignore(\"#{__dir__}/generators\")\n loader.setup\n end\n\n class Error < StandardError; end\nend\n"
69
73
  },
70
74
  {
71
- "path": "/Users/stefan/Documents/plutonium/chrono_forge/lib/chrono_forge/structure/namespace_collection.rb",
72
- "contents": "# frozen_string_literal: true\n\nmodule Phlexi\n module Field\n module Structure\n class NamespaceCollection < Node\n include Enumerable\n\n def initialize(key, parent:, collection: nil, &block)\n raise ArgumentError, \"block is required\" unless block.present?\n\n super(key, parent: parent)\n\n @collection = collection\n @block = block\n each(&block)\n end\n\n def object\n @collection\n end\n\n private\n\n def each(&)\n namespaces.each(&)\n end\n\n # Builds and memoizes namespaces for the collection.\n #\n # @return [Array<Namespace>] An array of namespace objects.\n def namespaces\n @namespaces ||= case @collection\n when Hash\n @collection.map do |key, object|\n build_namespace(key, object: object)\n end\n when Array\n @collection.map.with_index do |object, key|\n build_namespace(key, object: object)\n end\n end\n end\n\n def build_namespace(index, **)\n parent.class.new(index, parent: self, builder_klass: parent.builder_klass, **)\n end\n end\n end\n end\nend\n"
75
+ "path": "/Users/stefan/Documents/plutonium/chrono_forge/lib/generators/chrono_forge/install/install_generator.rb",
76
+ "contents": "# frozen_string_literal: true\n\nrequire \"rails/generators/active_record/migration\"\n\nmodule ChronoForge\n class InstallGenerator < Rails::Generators::Base\n include ::ActiveRecord::Generators::Migration\n\n source_root File.expand_path(\"templates\", __dir__)\n\n def start\n install_migrations\n rescue => err\n say \"#{err.class}: #{err}\\n#{err.backtrace.join(\"\\n\")}\", :red\n exit 1\n end\n\n private\n\n def install_migrations\n migration_template \"install_chrono_forge.rb\", \"install_chrono_forge.rb\"\n end\n end\nend\n"
73
77
  },
74
78
  {
75
- "path": "/Users/stefan/Documents/plutonium/chrono_forge/lib/chrono_forge/structure/node.rb",
76
- "contents": "# frozen_string_literal: true\n\nmodule Phlexi\n module Field\n module Structure\n # Superclass for Namespace and Field classes. Represents a node in the field tree structure.\n #\n # @attr_reader [Symbol] key The node's key\n # @attr_reader [Node, nil] parent The node's parent in the tree structure\n class Node\n attr_reader :key, :parent\n\n # Initializes a new Node instance.\n #\n # @param key [Symbol, String] The key for the node\n # @param parent [Node, nil] The parent node\n def initialize(key, parent:)\n @key = :\"#{key}\"\n @parent = parent\n end\n\n def inspect\n \"<#{self.class.name} key=#{key.inspect} object=#{object.inspect} parent=#{parent.inspect} />\"\n end\n end\n end\n end\nend\n"
79
+ "path": "/Users/stefan/Documents/plutonium/chrono_forge/lib/generators/chrono_forge/install/templates/install_chrono_forge.rb",
80
+ "contents": "# frozen_string_literal: true\n\nclass InstallChronoForge < ActiveRecord::Migration[7.1]\n def change\n create_table :chrono_forge_workflows do |t|\n t.string :key, null: false, index: {unique: true}\n t.string :job_klass, null: false\n\n if t.respond_to?(:jsonb)\n t.jsonb :kwargs, null: false, default: {}\n t.jsonb :options, null: false, default: {}\n t.jsonb :context, null: false, default: {}\n else\n t.json :kwargs, null: false, default: {}\n t.json :options, null: false, default: {}\n t.json :context, null: false, default: {}\n end\n\n t.integer :state, null: false, default: 0\n t.string :locked_by\n t.datetime :locked_at\n\n t.datetime :started_at\n t.datetime :completed_at\n\n t.timestamps\n end\n\n create_table :chrono_forge_execution_logs do |t|\n t.references :workflow, null: false, foreign_key: {to_table: :chrono_forge_workflows}\n t.string :step_name, null: false\n t.integer :attempts, null: false, default: 0\n t.datetime :started_at\n t.datetime :last_executed_at\n t.datetime :completed_at\n if t.respond_to?(:jsonb)\n t.jsonb :metadata\n else\n t.json :metadata\n end\n t.integer :state, null: false, default: 0\n t.string :error_class\n t.text :error_message\n\n t.timestamps\n t.index %i[workflow_id step_name], unique: true\n end\n\n create_table :chrono_forge_error_logs do |t|\n t.references :workflow, null: false, foreign_key: {to_table: :chrono_forge_workflows}\n t.string :error_class\n t.text :error_message\n t.text :backtrace\n if t.respond_to?(:jsonb)\n t.jsonb :context\n else\n t.json :context\n end\n\n t.timestamps\n end\n end\nend\n"
77
81
  },
78
82
  {
79
- "path": "/Users/stefan/Documents/plutonium/chrono_forge/lib/chrono_forge/support/value.rb",
80
- "contents": "module Phlexi\n module Field\n module Support\n module Value\n def self.from(object, key)\n return object[key] if object.is_a?(Hash)\n object.public_send(key) if object.respond_to?(key)\n end\n end\n end\n end\nend\n"
83
+ "path": "/Users/stefan/Documents/plutonium/chrono_forge/test/chrono_forge_test.rb",
84
+ "contents": "require \"test_helper\"\n\nclass ChronoForgeTest < Minitest::Test\n include ChaoticJob::Helpers\n\n def test_version\n assert ChronoForge::VERSION\n end\n\n def test_job_is_durable\n DurableJob.perform_later(:key)\n perform_all_jobs\n\n assert ChronoForge::Workflow.last.completed?\n end\nend\n"
81
85
  },
82
86
  {
83
- "path": "/Users/stefan/Documents/plutonium/chrono_forge/lib/chrono_forge/theme.rb",
84
- "contents": "require \"fiber/local\"\n\nmodule Phlexi\n module Field\n class Theme\n def self.inherited(subclass)\n super\n subclass.extend Fiber::Local\n end\n\n # Retrieves the theme hash\n #\n # This method returns a hash containing theme definitions for various display components.\n # If a theme has been explicitly set in the options, it returns that. Otherwise, it\n # initializes and returns a default theme.\n #\n # The theme hash defines CSS classes or references to other theme keys for different\n # components, allowing for a flexible and inheritance-based theming system.\n #\n # @return [Hash] A hash containing theme definitions for display components\n #\n # @example Accessing the theme\n # theme[:text]\n # # => \"text-gray-700 text-sm\"\n #\n # @example Theme inheritance\n # theme[:email] # Returns :text, indicating email inherits text's theme\n def self.theme\n raise NotImplementedError, \"#{self} must implement #self.theme\"\n end\n\n def theme\n @theme ||= self.class.theme.freeze\n end\n\n # Recursively resolves the theme for a given property, handling nested symbol references\n #\n # @param property [Symbol, String] The theme property to resolve\n # @param visited [Set] Set of already visited properties to prevent infinite recursion\n # @return [String, nil] The resolved theme value or nil if not found\n #\n # @example Basic usage\n # # Assuming the theme is: { text: \"text-gray-700\", email: :text }\n # themed(:text)\n # # => \"text-gray-700 text-sm\"\n #\n # @example Cascading themes\n # # Assuming the theme is: { text: \"text-gray-700\", email: :text }\n # resolve_theme(:email)\n # # => \"text-gray-700\"\n def resolve_theme(property, visited = Set.new)\n return nil if !property.present? || visited.include?(property)\n visited.add(property)\n\n result = theme[property]\n if result.is_a?(Symbol)\n resolve_theme(result, visited)\n else\n result\n end\n end\n end\n end\nend\n"
87
+ "path": "/Users/stefan/Documents/plutonium/chrono_forge/test/internal/app/jobs/durable_job.rb",
88
+ "contents": "class DurableJob < ActiveJob::Base\n prepend ChronoForge::Executor\n\n def perform\n # Context can be used to pass and store data between executions\n context[\"order_id\"] = SecureRandom.hex\n\n # Wait until payment is confirmed\n wait_until :payment_confirmed?\n\n # Wait for potential fraud check\n wait 1.seconds, :fraud_check_delay\n\n # Durably execute order processing\n durably_execute :process_order\n\n # Final steps\n complete_order\n end\n\n private\n\n def payment_confirmed?\n [true, false].sample\n end\n\n def process_order\n context[\"processed_at\"] = Time.current.iso8601\n end\n\n def complete_order\n context[\"completed_at\"] = Time.current.iso8601\n end\nend\n"
85
89
  },
86
90
  {
87
- "path": "/Users/stefan/Documents/plutonium/chrono_forge/lib/chrono_forge/version.rb",
88
- "contents": "# frozen_string_literal: true\n\nmodule Phlexi\n module Field\n VERSION = \"0.0.9\"\n end\nend\n"
91
+ "path": "/Users/stefan/Documents/plutonium/chrono_forge/test/internal/app/models/user.rb",
92
+ "contents": "class User < ActiveRecord::Base\n validates :name, presence: true\n validates :email, presence: true\nend\n"
89
93
  },
90
94
  {
91
- "path": "/Users/stefan/Documents/plutonium/chrono_forge/lib/chrono_forge.rb",
92
- "contents": "# frozen_string_literal: true\n\nrequire \"zeitwerk\"\nrequire \"phlex\"\nrequire \"active_support/core_ext/object/blank\"\n\nmodule Phlexi\n NIL_VALUE = :__i_phlexi_i__\n\n module Field\n Loader = Zeitwerk::Loader.new.tap do |loader|\n loader.tag = File.basename(__FILE__, \".rb\")\n loader.ignore(\"#{__dir__}/field/version.rb\")\n loader.inflector.inflect(\n \"chrono_forge\" => \"Phlexi\",\n \"phlexi\" => \"Phlexi\",\n \"dom\" => \"DOM\"\n )\n loader.push_dir(File.expand_path(\"..\", __dir__))\n loader.setup\n end\n\n COMPONENT_BASE = (defined?(::ApplicationComponent) ? ::ApplicationComponent : Phlex::HTML)\n\n class Error < StandardError; end\n\n def self.object_primary_key(object)\n if object.class.respond_to?(:primary_key)\n object.send(object.class.primary_key.to_sym)\n elsif object.respond_to?(:id)\n object.id\n end\n end\n end\nend\n"
95
+ "path": "/Users/stefan/Documents/plutonium/chrono_forge/test/internal/config/database.yml",
96
+ "contents": "test:\n adapter: sqlite3\n database: db/combustion_test.sqlite\n"
93
97
  },
94
98
  {
95
- "path": "/Users/stefan/Documents/plutonium/chrono_forge/lib/chrono_forge.rb",
96
- "contents": "# frozen_string_literal: true\n\nrequire_relative \"chrono_forge/version\"\nrequire_relative \"chrono_forge\"\n"
99
+ "path": "/Users/stefan/Documents/plutonium/chrono_forge/test/internal/config/storage.yml",
100
+ "contents": "test:\n service: Disk\n root: tmp/storage\n"
97
101
  },
98
102
  {
99
- "path": "/Users/stefan/Documents/plutonium/chrono_forge/chrono_forge.gemspec",
100
- "contents": "# frozen_string_literal: true\n\nrequire_relative \"lib/chrono_forge/version\"\n\nGem::Specification.new do |spec|\n spec.name = \"chrono_forge\"\n spec.version = ChronoForge::VERSION\n spec.authors = [\"Stefan Froelich\"]\n spec.email = [\"sfroelich01@gmail.com\"]\n\n spec.summary = \"Base fields for the Phlexi libraries\"\n spec.description = \"Base fields for the Phlexi libraries\"\n spec.homepage = \"https://github.com/radioactive-labs/chrono_forge\"\n spec.license = \"MIT\"\n spec.required_ruby_version = \">= 3.2.2\"\n\n spec.metadata[\"allowed_push_host\"] = \"https://rubygems.org\"\n\n spec.metadata[\"homepage_uri\"] = spec.homepage\n spec.metadata[\"source_code_uri\"] = spec.homepage\n spec.metadata[\"changelog_uri\"] = spec.homepage\n\n # Specify which files should be added to the gem when it is released.\n # The `git ls-files -z` loads the files in the RubyGem that have been added into git.\n gemspec = File.basename(__FILE__)\n spec.files = IO.popen(%w[git ls-files -z], chdir: __dir__, err: IO::NULL) do |ls|\n ls.readlines(\"\\x0\", chomp: true).reject do |f|\n (f == gemspec) ||\n f.start_with?(*%w[bin/ test/ spec/ features/ .git .github appveyor Gemfile])\n end\n end\n spec.bindir = \"exe\"\n spec.executables = spec.files.grep(%r{\\Aexe/}) { |f| File.basename(f) }\n spec.require_paths = [\"lib\"]\n\n spec.add_dependency \"phlex\", \"~> 1.11\"\n spec.add_dependency \"activesupport\"\n spec.add_dependency \"zeitwerk\"\n spec.add_dependency \"fiber-local\"\n\n spec.add_development_dependency \"rake\"\n spec.add_development_dependency \"minitest\"\n spec.add_development_dependency \"minitest-reporters\"\n spec.add_development_dependency \"standard\"\n # spec.add_development_dependency \"brakeman\"\n spec.add_development_dependency \"bundle-audit\"\n spec.add_development_dependency \"appraisal\"\n spec.add_development_dependency \"combustion\"\n spec.add_development_dependency \"phlex-testing-capybara\"\nend\n"
103
+ "path": "/Users/stefan/Documents/plutonium/chrono_forge/test/internal/db/migrate/20241217100623_install_chrono_forge.rb",
104
+ "contents": "# The template is our single source of truth.\nrequire File.expand_path(\"../../../../lib/generators/chrono_forge/install/templates/install_chrono_forge.rb\", __dir__)\n"
105
+ },
106
+ {
107
+ "path": "/Users/stefan/Documents/plutonium/chrono_forge/test/internal/db/schema.rb",
108
+ "contents": "# frozen_string_literal: true\n\nActiveRecord::Schema.define do\n create_table :users, force: true do |t|\n t.string :name\n t.string :email\n t.timestamps null: false\n end\nend\n"
109
+ },
110
+ {
111
+ "path": "/Users/stefan/Documents/plutonium/chrono_forge/test/test_helper.rb",
112
+ "contents": "require \"chrono_forge\"\n\nrequire \"minitest/autorun\"\nrequire \"minitest/reporters\"\nMinitest::Reporters.use!\n\nrequire \"combustion\"\nCombustion.path = \"test/internal\"\nCombustion.initialize! :active_record, :active_job\n\nrequire \"chaotic_job\"\n"
101
113
  }
102
114
  ]
data/export.rb CHANGED
@@ -42,7 +42,7 @@ end
42
42
 
43
43
  # Example usage (uncomment and modify as needed):
44
44
  directory = "/Users/stefan/Documents/plutonium/chrono_forge"
45
- exceptions = ["/.github/", "/.vscode/", "gemfiles", "pkg", "test", "node_modules"]
45
+ exceptions = ["/.github/", "/.vscode/", "gemfiles", "pkg", "node_modules"]
46
46
  extensions = ["rb", "md", "yml", "yaml", "gemspec"]
47
47
  output_file = "export.json"
48
48
  export_files_to_json(directory, extensions, output_file, exceptions)
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- chrono_forge (0.0.1)
4
+ chrono_forge (0.1.0)
5
5
  activejob
6
6
  activerecord
7
7
  zeitwerk
@@ -27,10 +27,5 @@ module ChronoForge
27
27
  self.table_name = "chrono_forge_error_logs"
28
28
 
29
29
  belongs_to :workflow
30
-
31
- # Cleanup method
32
- def self.cleanup_old_logs(retention_period: 30.days)
33
- where("created_at < ?", retention_period.ago).delete_all
34
- end
35
30
  end
36
31
  end
@@ -38,10 +38,5 @@ module ChronoForge
38
38
  completed
39
39
  failed
40
40
  ]
41
-
42
- # Cleanup method
43
- def self.cleanup_old_logs(retention_period: 30.days)
44
- where("created_at < ?", retention_period.ago).delete_all
45
- end
46
41
  end
47
42
  end
@@ -6,8 +6,8 @@ module ChronoForge
6
6
  module WaitUntil
7
7
  def wait_until(condition, **options)
8
8
  # Default timeout and check interval
9
- timeout = options[:timeout] || 10.seconds
10
- check_interval = options[:check_interval] || 1.second
9
+ timeout = options[:timeout] || 1.hour
10
+ check_interval = options[:check_interval] || 15.minutes
11
11
 
12
12
  # Find or create execution log
13
13
  step_name = "wait_until$#{condition}"
@@ -89,8 +89,8 @@ module ChronoForge
89
89
  state: :failed,
90
90
  metadata: metadata.merge("result" => nil)
91
91
  )
92
- Rails.logger.warn { "Timeout reached for condition #{condition}. Condition not met within the timeout period." }
93
- raise WaitConditionNotMet, "Condition not met within timeout period"
92
+ Rails.logger.warn { "Timeout reached for condition '#{condition}'." }
93
+ raise WaitConditionNotMet, "Condition '#{condition}' not met within timeout period"
94
94
  end
95
95
 
96
96
  # Reschedule with delay
@@ -13,7 +13,7 @@ module ChronoForge
13
13
  wait_duration = BACKOFF_STRATEGY[attempt] || BACKOFF_STRATEGY.last
14
14
 
15
15
  # Schedule with exponential backoff
16
- workflow.job_klass.constantize
16
+ workflow.job_klass
17
17
  .set(wait: wait_duration)
18
18
  .perform_later(
19
19
  workflow.key,
@@ -10,7 +10,7 @@ module ChronoForge
10
10
 
11
11
  include Methods
12
12
 
13
- def perform(key, attempt: 0, **kwargs)
13
+ def perform(key, attempt: 0, options: {}, **kwargs)
14
14
  # Prevent excessive retries
15
15
  if attempt >= self.class::RetryStrategy.max_attempts
16
16
  Rails.logger.error { "Max attempts reached for job #{key}" }
@@ -18,7 +18,7 @@ module ChronoForge
18
18
  end
19
19
 
20
20
  # Find or create job with comprehensive tracking
21
- setup_workflow(key, kwargs)
21
+ setup_workflow(key, options, kwargs)
22
22
 
23
23
  begin
24
24
  # Skip if workflow cannot be executed
@@ -47,13 +47,13 @@ module ChronoForge
47
47
  nil
48
48
  rescue => e
49
49
  Rails.logger.error { "An error occurred during execution of #{key}" }
50
- self.class::ExecutionTracker.track_error(workflow, e)
50
+ error_log = self.class::ExecutionTracker.track_error(workflow, e)
51
51
 
52
52
  # Retry if applicable
53
53
  if should_retry?(e, attempt)
54
54
  self.class::RetryStrategy.schedule_retry(workflow, attempt: attempt)
55
55
  else
56
- workflow.failed!
56
+ fail_workflow! error_log
57
57
  end
58
58
  ensure
59
59
  context.save!
@@ -71,9 +71,6 @@ module ChronoForge
71
71
  step_name: "$workflow_completion$"
72
72
  ) do |log|
73
73
  log.started_at = Time.current
74
- log.metadata = {
75
- workflow_id: workflow.id
76
- }
77
74
  end
78
75
 
79
76
  begin
@@ -104,14 +101,53 @@ module ChronoForge
104
101
  end
105
102
  end
106
103
 
107
- def setup_workflow(key, kwargs)
108
- @workflow = find_workflow(key, kwargs)
104
+ def fail_workflow!(error_log)
105
+ # Create an execution log for workflow failure
106
+ execution_log = ExecutionLog.create_or_find_by!(
107
+ workflow: workflow,
108
+ step_name: "$workflow_failure$"
109
+ ) do |log|
110
+ log.started_at = Time.current
111
+ log.metadata = {
112
+ error_log_id: error_log.id
113
+ }
114
+ end
115
+
116
+ begin
117
+ execution_log.update!(
118
+ attempts: execution_log.attempts + 1,
119
+ last_executed_at: Time.current
120
+ )
121
+
122
+ workflow.failed!
123
+
124
+ # Mark execution log as completed
125
+ execution_log.update!(
126
+ state: :completed,
127
+ completed_at: Time.current
128
+ )
129
+
130
+ # Return the execution log for tracking
131
+ execution_log
132
+ rescue => e
133
+ # Log any completion errors
134
+ execution_log.update!(
135
+ state: :failed,
136
+ error_message: e.message,
137
+ error_class: e.class.name
138
+ )
139
+ raise
140
+ end
141
+ end
142
+
143
+ def setup_workflow(key, options, kwargs)
144
+ @workflow = find_workflow(key, options, kwargs)
109
145
  @context = Context.new(@workflow)
110
146
  end
111
147
 
112
- def find_workflow(key, kwargs)
113
- Workflow.create_or_find_by!(key: key) do |workflow|
114
- workflow.job_klass = self.class.to_s
148
+ def find_workflow(key, options, kwargs)
149
+ Workflow.create_or_find_by!(job_class: self.class.to_s, key: key) do |workflow|
150
+ workflow.options = options
115
151
  workflow.kwargs = kwargs
116
152
  workflow.started_at = Time.current
117
153
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ChronoForge
4
- VERSION = "0.0.2"
4
+ VERSION = "0.1.1"
5
5
  end
@@ -7,9 +7,10 @@
7
7
  # id :integer not null, primary key
8
8
  # completed_at :datetime
9
9
  # context :json not null
10
- # job_klass :string not null
10
+ # job_class :string not null
11
11
  # key :string not null
12
12
  # kwargs :json not null
13
+ # options :json not null
13
14
  # locked_at :datetime
14
15
  # started_at :datetime
15
16
  # state :integer default("idle"), not null
@@ -24,8 +25,8 @@ module ChronoForge
24
25
  class Workflow < ActiveRecord::Base
25
26
  self.table_name = "chrono_forge_workflows"
26
27
 
27
- has_many :execution_logs
28
- has_many :error_logs
28
+ has_many :execution_logs, -> { order(id: :asc) }
29
+ has_many :error_logs, -> { order(id: :asc) }
29
30
 
30
31
  enum :state, %i[
31
32
  idle
@@ -35,16 +36,15 @@ module ChronoForge
35
36
  stalled
36
37
  ]
37
38
 
38
- # Cleanup method
39
- def self.cleanup_old_logs(retention_period: 30.days)
40
- where("created_at < ?", retention_period.ago).delete_all
41
- end
42
-
43
39
  # Serialization for metadata
44
40
  serialize :metadata, coder: JSON
45
41
 
46
42
  def executable?
47
43
  idle? || running?
48
44
  end
45
+
46
+ def job_klass
47
+ job_class.constantize
48
+ end
49
49
  end
50
50
  end
data/lib/chrono_forge.rb CHANGED
@@ -4,7 +4,7 @@ require "zeitwerk"
4
4
  require "active_record"
5
5
  require "active_job"
6
6
 
7
- module Chronoforge
7
+ module ChronoForge
8
8
  Loader = Zeitwerk::Loader.for_gem.tap do |loader|
9
9
  loader.ignore("#{__dir__}/generators")
10
10
  loader.setup
@@ -3,8 +3,8 @@
3
3
  class InstallChronoForge < ActiveRecord::Migration[7.1]
4
4
  def change
5
5
  create_table :chrono_forge_workflows do |t|
6
- t.string :key, null: false, index: {unique: true}
7
- t.string :job_klass, null: false
6
+ t.string :key, null: false, index: true
7
+ t.string :job_class, null: false
8
8
 
9
9
  if t.respond_to?(:jsonb)
10
10
  t.jsonb :kwargs, null: false, default: {}
@@ -24,6 +24,7 @@ class InstallChronoForge < ActiveRecord::Migration[7.1]
24
24
  t.datetime :completed_at
25
25
 
26
26
  t.timestamps
27
+ t.index %i[job_class key], unique: true
27
28
  end
28
29
 
29
30
  create_table :chrono_forge_execution_logs do |t|
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: chrono_forge
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stefan Froelich
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-12-21 00:00:00.000000000 Z
11
+ date: 2025-04-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -164,7 +164,10 @@ dependencies:
164
164
  - - ">="
165
165
  - !ruby/object:Gem::Version
166
166
  version: '0'
167
- description: Base fields for the Phlexi libraries
167
+ description: ChronoForge provides a robust framework for building durable, distributed
168
+ workflows in Ruby on Rails applications. It offers reliable state management, error
169
+ recovery, and workflow orchestration through features like durable execution, wait
170
+ states, and comprehensive error tracking.
168
171
  email:
169
172
  - sfroelich01@gmail.com
170
173
  executables: []
@@ -226,5 +229,5 @@ requirements: []
226
229
  rubygems_version: 3.4.10
227
230
  signing_key:
228
231
  specification_version: 4
229
- summary: Base fields for the Phlexi libraries
232
+ summary: A durable workflow engine for Ruby on Rails built on ActiveJob
230
233
  test_files: []