taski 0.4.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +49 -110
  3. data/examples/README.md +8 -5
  4. data/examples/context_demo.rb +22 -16
  5. data/lib/taski/context.rb +36 -38
  6. data/lib/taski/section.rb +7 -0
  7. data/lib/taski/static_analysis/analyzer.rb +27 -6
  8. data/lib/taski/static_analysis/visitor.rb +30 -6
  9. data/lib/taski/task.rb +60 -16
  10. data/lib/taski/version.rb +1 -1
  11. data/lib/taski.rb +23 -0
  12. metadata +1 -62
  13. data/.gem_rbs_collection/ast/2.4/.rbs_meta.yaml +0 -9
  14. data/.gem_rbs_collection/ast/2.4/ast.rbs +0 -73
  15. data/.gem_rbs_collection/minitest/5.25/.rbs_meta.yaml +0 -9
  16. data/.gem_rbs_collection/minitest/5.25/minitest/abstract_reporter.rbs +0 -52
  17. data/.gem_rbs_collection/minitest/5.25/minitest/assertion.rbs +0 -17
  18. data/.gem_rbs_collection/minitest/5.25/minitest/assertions.rbs +0 -590
  19. data/.gem_rbs_collection/minitest/5.25/minitest/backtrace_filter.rbs +0 -23
  20. data/.gem_rbs_collection/minitest/5.25/minitest/bench_spec.rbs +0 -102
  21. data/.gem_rbs_collection/minitest/5.25/minitest/benchmark.rbs +0 -259
  22. data/.gem_rbs_collection/minitest/5.25/minitest/composite_reporter.rbs +0 -25
  23. data/.gem_rbs_collection/minitest/5.25/minitest/compress.rbs +0 -13
  24. data/.gem_rbs_collection/minitest/5.25/minitest/error_on_warning.rbs +0 -3
  25. data/.gem_rbs_collection/minitest/5.25/minitest/expectation.rbs +0 -2
  26. data/.gem_rbs_collection/minitest/5.25/minitest/expectations.rbs +0 -21
  27. data/.gem_rbs_collection/minitest/5.25/minitest/guard.rbs +0 -64
  28. data/.gem_rbs_collection/minitest/5.25/minitest/mock.rbs +0 -64
  29. data/.gem_rbs_collection/minitest/5.25/minitest/parallel/executor.rbs +0 -46
  30. data/.gem_rbs_collection/minitest/5.25/minitest/parallel/test/class_methods.rbs +0 -5
  31. data/.gem_rbs_collection/minitest/5.25/minitest/parallel/test.rbs +0 -3
  32. data/.gem_rbs_collection/minitest/5.25/minitest/parallel.rbs +0 -2
  33. data/.gem_rbs_collection/minitest/5.25/minitest/pride_io.rbs +0 -62
  34. data/.gem_rbs_collection/minitest/5.25/minitest/pride_lol.rbs +0 -19
  35. data/.gem_rbs_collection/minitest/5.25/minitest/progress_reporter.rbs +0 -11
  36. data/.gem_rbs_collection/minitest/5.25/minitest/reportable.rbs +0 -53
  37. data/.gem_rbs_collection/minitest/5.25/minitest/reporter.rbs +0 -5
  38. data/.gem_rbs_collection/minitest/5.25/minitest/result.rbs +0 -28
  39. data/.gem_rbs_collection/minitest/5.25/minitest/runnable.rbs +0 -163
  40. data/.gem_rbs_collection/minitest/5.25/minitest/skip.rbs +0 -6
  41. data/.gem_rbs_collection/minitest/5.25/minitest/spec/dsl/instance_methods.rbs +0 -48
  42. data/.gem_rbs_collection/minitest/5.25/minitest/spec/dsl.rbs +0 -129
  43. data/.gem_rbs_collection/minitest/5.25/minitest/spec.rbs +0 -11
  44. data/.gem_rbs_collection/minitest/5.25/minitest/statistics_reporter.rbs +0 -81
  45. data/.gem_rbs_collection/minitest/5.25/minitest/summary_reporter.rbs +0 -18
  46. data/.gem_rbs_collection/minitest/5.25/minitest/test/lifecycle_hooks.rbs +0 -92
  47. data/.gem_rbs_collection/minitest/5.25/minitest/test.rbs +0 -69
  48. data/.gem_rbs_collection/minitest/5.25/minitest/unexpected_error.rbs +0 -12
  49. data/.gem_rbs_collection/minitest/5.25/minitest/unexpected_warning.rbs +0 -6
  50. data/.gem_rbs_collection/minitest/5.25/minitest/unit/test_case.rbs +0 -3
  51. data/.gem_rbs_collection/minitest/5.25/minitest/unit.rbs +0 -4
  52. data/.gem_rbs_collection/minitest/5.25/minitest.rbs +0 -115
  53. data/.gem_rbs_collection/parallel/1.20/.rbs_meta.yaml +0 -9
  54. data/.gem_rbs_collection/parallel/1.20/parallel.rbs +0 -86
  55. data/.gem_rbs_collection/parser/3.2/.rbs_meta.yaml +0 -9
  56. data/.gem_rbs_collection/parser/3.2/manifest.yaml +0 -7
  57. data/.gem_rbs_collection/parser/3.2/parser.rbs +0 -193
  58. data/.gem_rbs_collection/parser/3.2/polyfill.rbs +0 -4
  59. data/.gem_rbs_collection/rainbow/3.0/.rbs_meta.yaml +0 -9
  60. data/.gem_rbs_collection/rainbow/3.0/global.rbs +0 -7
  61. data/.gem_rbs_collection/rainbow/3.0/presenter.rbs +0 -209
  62. data/.gem_rbs_collection/rainbow/3.0/rainbow.rbs +0 -5
  63. data/.gem_rbs_collection/rake/13.0/.rbs_meta.yaml +0 -9
  64. data/.gem_rbs_collection/rake/13.0/manifest.yaml +0 -2
  65. data/.gem_rbs_collection/rake/13.0/rake.rbs +0 -39
  66. data/.gem_rbs_collection/regexp_parser/2.8/.rbs_meta.yaml +0 -9
  67. data/.gem_rbs_collection/regexp_parser/2.8/regexp_parser.rbs +0 -17
  68. data/.gem_rbs_collection/rubocop/1.57/.rbs_meta.yaml +0 -9
  69. data/.gem_rbs_collection/rubocop/1.57/rubocop.rbs +0 -129
  70. data/.gem_rbs_collection/rubocop-ast/1.30/.rbs_meta.yaml +0 -9
  71. data/.gem_rbs_collection/rubocop-ast/1.30/rubocop-ast.rbs +0 -771
  72. data/.gem_rbs_collection/simplecov/0.22/.rbs_meta.yaml +0 -9
  73. data/.gem_rbs_collection/simplecov/0.22/simplecov.rbs +0 -54
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 17f3cd1627f0b49f60b2173fcfef71ce6bbc60b6280d1d3b35cf9edb5e89ea9c
4
- data.tar.gz: 25e99fdf4d4fbdc48be47a65764efd0884a71355db16c7d674b796f178bdf900
3
+ metadata.gz: 537249ee49e056c8841cc1c4d4061eccf7be201b3690a197af8d5a5b65939173
4
+ data.tar.gz: f012517341ea31b2e36f4a87e51ab199cb24d70d7ba10519653a2d290c78dcaf
5
5
  SHA512:
6
- metadata.gz: 19697472cbdff99b1a4a5315be18d274637c1806b225550d3a4acb541771a618a6fd1e14156599335174e12f3dd27d7f714bd0e7c87e0c76d525ca0a3375e361
7
- data.tar.gz: 20bff2f125897ca23dab9a1d78831c14e45fca5f40f2fed29bb6c1114a7d040569b2e46994614049963dcca0cb3831d8cee91ebc3dc7892a7def59e1a965198a
6
+ metadata.gz: e15871fa35cf862eae794b823ab760adcd1ca1156cb23b0d27d705e8c5fb0ddfa3f2dd17859f7d238a6f0e9dbf558bedf5148330ffbf239a7291f3c99b037e07
7
+ data.tar.gz: d1ca33867403c337c2d1638acae3a2fd47ca77a707e7df2b6076d8c22c8d289ceae73ba6e4f1f30d413c784c30f999d60648a270c3b49a398e3144d85fd63837
data/README.md CHANGED
@@ -4,15 +4,11 @@
4
4
  [![Codecov](https://codecov.io/gh/ahogappa/taski/branch/master/graph/badge.svg)](https://codecov.io/gh/ahogappa/taski)
5
5
  [![Gem Version](https://badge.fury.io/rb/taski.svg)](https://badge.fury.io/rb/taski)
6
6
 
7
- > **🚧 Development Status:** Taski is currently under active development. Not yet recommended for production use.
8
-
9
- **Taski** is a Ruby framework for building task dependency graphs with automatic resolution and **parallel execution**.
7
+ **Taski** is a Ruby framework for building task dependency graphs with automatic resolution and parallel execution.
10
8
 
11
9
  > **Name Origin**: "Taski" comes from the Japanese word "襷" (tasuki), a sash used in relay races. Just like how runners pass the sash to the next teammate, tasks in Taski pass dependencies to one another in a continuous chain.
12
10
 
13
- ## Why Taski?
14
-
15
- Build complex workflows by defining tasks and their dependencies - Taski automatically resolves execution order and executes them in parallel.
11
+ ## Features
16
12
 
17
13
  - **Automatic Dependency Resolution**: Dependencies detected via static analysis
18
14
  - **Parallel Execution**: Independent tasks run concurrently for maximum performance
@@ -20,18 +16,16 @@ Build complex workflows by defining tasks and their dependencies - Taski automat
20
16
  - **Real-time Progress**: Visual feedback with parallel task progress display
21
17
  - **Thread-Safe**: Built on Monitor-based synchronization for reliable concurrent execution
22
18
 
23
- ## 🚀 Quick Start
19
+ ## Quick Start
24
20
 
25
21
  ```ruby
26
22
  require 'taski'
27
23
 
28
- # Define tasks with dependencies
29
24
  class DatabaseSetup < Taski::Task
30
25
  exports :connection_string
31
26
 
32
27
  def run
33
28
  @connection_string = "postgresql://localhost/myapp"
34
- puts "Database configured"
35
29
  end
36
30
  end
37
31
 
@@ -40,31 +34,32 @@ class CacheSetup < Taski::Task
40
34
 
41
35
  def run
42
36
  @cache_url = "redis://localhost:6379"
43
- puts "Cache configured"
44
37
  end
45
38
  end
46
39
 
47
40
  class APIServer < Taski::Task
48
41
  def run
49
42
  # Dependencies execute automatically and in parallel
50
- puts "Starting API with #{DatabaseSetup.connection_string}"
51
- puts "Using cache at #{CacheSetup.cache_url}"
43
+ puts "DB: #{DatabaseSetup.connection_string}"
44
+ puts "Cache: #{CacheSetup.cache_url}"
52
45
  end
53
46
  end
54
47
 
55
- # Run any task - dependencies resolve and execute in parallel
56
48
  APIServer.run
57
- # => Database configured
58
- # => Cache configured
59
- # => Starting API with postgresql://localhost/myapp
60
- # => Using cache at redis://localhost:6379
49
+ ```
50
+
51
+ ## Installation
52
+
53
+ ```ruby
54
+ gem 'taski'
61
55
  ```
62
56
 
63
57
  ## Core Concepts
64
58
 
65
- Taski provides two complementary APIs:
59
+ ### Exports - Value Sharing Between Tasks
60
+
61
+ Share computed values between tasks:
66
62
 
67
- ### 1. Exports API - Value Sharing Between Tasks
68
63
  ```ruby
69
64
  class Config < Taski::Task
70
65
  exports :app_name, :port
@@ -77,26 +72,27 @@ end
77
72
 
78
73
  class Server < Taski::Task
79
74
  def run
80
- # Automatically depends on Config task
81
75
  puts "Starting #{Config.app_name} on port #{Config.port}"
82
76
  end
83
77
  end
84
78
  ```
85
79
 
86
- ### 2. Section API - Runtime Implementation Selection
80
+ ### Section - Runtime Implementation Selection
81
+
82
+ Switch implementations based on environment:
83
+
87
84
  ```ruby
88
85
  class DatabaseSection < Taski::Section
89
86
  interfaces :host, :port
90
87
 
91
- # Nested classes automatically inherit interfaces - no exports needed
92
- class ProductionDB < Taski::Task
88
+ class Production < Taski::Task
93
89
  def run
94
90
  @host = "prod.example.com"
95
91
  @port = 5432
96
92
  end
97
93
  end
98
94
 
99
- class LocalDB < Taski::Task
95
+ class Development < Taski::Task
100
96
  def run
101
97
  @host = "localhost"
102
98
  @port = 5432
@@ -104,97 +100,67 @@ class DatabaseSection < Taski::Section
104
100
  end
105
101
 
106
102
  def impl
107
- # Select implementation at runtime
108
- ENV['RAILS_ENV'] == 'production' ? ProductionDB : LocalDB
103
+ ENV['RAILS_ENV'] == 'production' ? Production : Development
109
104
  end
110
105
  end
111
106
 
112
107
  class App < Taski::Task
113
108
  def run
114
- # Access through Section - implementation selected at runtime
115
109
  puts "Connecting to #{DatabaseSection.host}:#{DatabaseSection.port}"
116
110
  end
117
111
  end
118
-
119
- App.run
120
- # In development (RAILS_ENV != 'production'):
121
- # => Connecting to localhost:5432
122
-
123
- # In production (RAILS_ENV == 'production'):
124
- # => Connecting to prod.example.com:5432
125
112
  ```
126
113
 
127
- **When to use each:**
128
- - **Exports**: Share computed values or side effects between tasks
129
- - **Section**: Switch implementations based on environment or conditions
114
+ > **Note**: Nested implementation classes automatically inherit Section's `interfaces` as `exports`.
130
115
 
131
- > **Note**: When implementation classes are nested inside a Section, they automatically inherit the Section's `interfaces` as `exports`. External implementations must declare `exports` explicitly.
116
+ ## Advanced Usage
132
117
 
133
- ### 3. Context - Runtime Information Access
118
+ ### Context - Runtime Information and Options
134
119
 
135
- Access execution context information from any task:
120
+ Pass custom options and access execution context from any task:
136
121
 
137
122
  ```ruby
138
123
  class DeployTask < Taski::Task
139
124
  def run
140
- puts "Working directory: #{Taski::Context.working_directory}"
141
- puts "Started at: #{Taski::Context.started_at}"
142
- puts "Root task: #{Taski::Context.root_task}"
125
+ # User-defined options
126
+ env = Taski.context[:env]
127
+ debug = Taski.context.fetch(:debug, false)
128
+
129
+ # Runtime information
130
+ puts "Working directory: #{Taski.context.working_directory}"
131
+ puts "Started at: #{Taski.context.started_at}"
132
+ puts "Root task: #{Taski.context.root_task}"
133
+ puts "Deploying to: #{env}"
143
134
  end
144
135
  end
145
- ```
146
-
147
- **Available context:**
148
- - `working_directory`: Directory where execution started
149
- - `started_at`: Time when execution began
150
- - `root_task`: The first task class that was called
151
136
 
152
- > **Note**: Context is not included in dependency analysis - it provides runtime information only.
153
-
154
- > **Important**: Tasks must be defined in source files, not dynamically with `Class.new`. Static analysis requires actual source files to detect dependencies.
137
+ # Pass options when running
138
+ DeployTask.run(context: { env: "production", debug: true })
139
+ ```
155
140
 
156
- ### 4. Re-execution with `new`
141
+ Context API:
142
+ - `Taski.context[:key]` - Get option value (nil if not set)
143
+ - `Taski.context.fetch(:key, default)` - Get with default value
144
+ - `Taski.context.key?(:key)` - Check if option exists
145
+ - `Taski.context.working_directory` - Execution directory
146
+ - `Taski.context.started_at` - Execution start time
147
+ - `Taski.context.root_task` - First task class called
157
148
 
158
- By default, task results are cached. Use `new` to create a fresh instance for re-execution:
149
+ ### Re-execution
159
150
 
160
151
  ```ruby
161
- class RandomTask < Taski::Task
162
- exports :value
163
-
164
- def run
165
- @value = rand(1000)
166
- end
167
- end
168
-
169
- # Cached execution (same result)
152
+ # Cached execution (default)
170
153
  RandomTask.value # => 42
171
154
  RandomTask.value # => 42 (cached)
172
155
 
173
- # Fresh execution with new
156
+ # Fresh execution
174
157
  RandomTask.new.run # => 123 (new instance)
175
- RandomTask.new.run # => 456 (another new instance)
176
158
 
177
159
  # Reset all caches
178
160
  RandomTask.reset!
179
- RandomTask.value # => 789 (fresh after reset)
180
161
  ```
181
162
 
182
- **When to use:**
183
- - `TaskClass.run` / `TaskClass.value`: Normal execution with caching (recommended for dependency graphs)
184
- - `TaskClass.new.run`: Re-execute only this task (dependencies still use cache)
185
- - `TaskClass.reset!`: Clear all caches and re-execute everything
186
-
187
- ## Key Features
188
-
189
- - **Parallel Execution**: Independent tasks run concurrently using threads
190
- - **Static Analysis**: Dependencies detected automatically via Prism AST parsing
191
- - **Thread-Safe**: Monitor-based synchronization ensures safe concurrent access
192
- - **Progress Display**: Real-time visual feedback with spinner animations and timing
193
- - **Tree Visualization**: See your dependency graph structure
194
- - **Graceful Abort**: Stop execution cleanly without starting new tasks (Ctrl+C)
195
- - **Runtime Context**: Access execution information from any task
196
-
197
- ### Parallel Progress Display
163
+ ### Progress Display
198
164
 
199
165
  Enable real-time progress visualization:
200
166
 
@@ -202,14 +168,11 @@ Enable real-time progress visualization:
202
168
  TASKI_FORCE_PROGRESS=1 ruby your_script.rb
203
169
  ```
204
170
 
205
- Output example:
206
171
  ```
207
172
  ⠋ DatabaseSetup (running)
208
173
  ⠙ CacheSetup (running)
209
174
  ✅ DatabaseSetup (123.4ms)
210
175
  ✅ CacheSetup (98.2ms)
211
- ⠸ WebServer (running)
212
- ✅ WebServer (45.1ms)
213
176
  ```
214
177
 
215
178
  ### Tree Visualization
@@ -222,33 +185,13 @@ puts WebServer.tree
222
185
  # => └── Cache
223
186
  ```
224
187
 
225
- ## 📦 Installation
226
-
227
- ```ruby
228
- gem 'taski'
229
- ```
230
-
231
- ```bash
232
- bundle install
233
- ```
234
-
235
- ## 🧪 Testing
188
+ ## Development
236
189
 
237
190
  ```bash
238
191
  rake test # Run all tests
239
192
  rake standard # Check code style
240
193
  ```
241
194
 
242
- ## 📚 Learn More
243
-
244
- - **[Examples](examples/)**: Practical examples from basic to advanced patterns
245
- - **[Tests](test/)**: Comprehensive test suite showing real-world usage
246
- - **[Source Code](lib/taski/)**: Clean, well-documented implementation
247
- - `lib/taski/task.rb` - Core Task implementation with exports API
248
- - `lib/taski/section.rb` - Section API for runtime selection
249
- - `lib/taski/execution/` - Parallel execution engine
250
- - `lib/taski/static_analysis/` - Prism-based dependency analyzer
251
-
252
195
  ## Support
253
196
 
254
197
  Bug reports and pull requests welcome at https://github.com/ahogappa/taski.
@@ -256,7 +199,3 @@ Bug reports and pull requests welcome at https://github.com/ahogappa/taski.
256
199
  ## License
257
200
 
258
201
  MIT License
259
-
260
- ---
261
-
262
- **Taski** - Parallel task execution with automatic dependency resolution. 🚀
data/examples/README.md CHANGED
@@ -34,18 +34,21 @@ ruby examples/section_demo.rb
34
34
 
35
35
  ---
36
36
 
37
- ### 3. context_demo.rb - Runtime Context
37
+ ### 3. context_demo.rb - Runtime Context and Options
38
38
 
39
- Access execution context information from any task.
39
+ Access execution context and pass custom options to tasks.
40
40
 
41
41
  ```bash
42
42
  ruby examples/context_demo.rb
43
43
  ```
44
44
 
45
45
  **Covers:**
46
- - `Taski::Context.working_directory`
47
- - `Taski::Context.started_at`
48
- - `Taski::Context.root_task`
46
+ - User-defined options via `run(context: {...})`
47
+ - `Taski.context[:key]` for option access
48
+ - `Taski.context.fetch(:key, default)` for defaults
49
+ - `Taski.context.working_directory`
50
+ - `Taski.context.started_at`
51
+ - `Taski.context.root_task`
49
52
 
50
53
  ---
51
54
 
@@ -7,6 +7,7 @@
7
7
  # - working_directory: Where execution started
8
8
  # - started_at: When execution began
9
9
  # - root_task: The first task class that was called
10
+ # - User-defined options: Custom values passed via run(context: {...})
10
11
  #
11
12
  # Run: ruby examples/context_demo.rb
12
13
 
@@ -21,13 +22,15 @@ class SetupTask < Taski::Task
21
22
 
22
23
  def run
23
24
  puts "Setup running..."
24
- puts " Working directory: #{Taski::Context.working_directory}"
25
- puts " Started at: #{Taski::Context.started_at}"
26
- puts " Root task: #{Taski::Context.root_task}"
25
+ puts " Working directory: #{Taski.context.working_directory}"
26
+ puts " Started at: #{Taski.context.started_at}"
27
+ puts " Root task: #{Taski.context.root_task}"
28
+ puts " Environment: #{Taski.context[:env]}"
27
29
 
28
30
  @setup_info = {
29
- directory: Taski::Context.working_directory,
30
- timestamp: Taski::Context.started_at
31
+ directory: Taski.context.working_directory,
32
+ timestamp: Taski.context.started_at,
33
+ env: Taski.context[:env]
31
34
  }
32
35
  end
33
36
  end
@@ -38,11 +41,12 @@ class FileProcessor < Taski::Task
38
41
 
39
42
  def run
40
43
  # Use context to determine output location
41
- base_dir = Taski::Context.working_directory
42
- @output_path = File.join(base_dir, "tmp", "output.txt")
44
+ base_dir = Taski.context.working_directory
45
+ env = Taski.context.fetch(:env, "development")
46
+ @output_path = File.join(base_dir, "tmp", env, "output.txt")
43
47
 
44
48
  puts "FileProcessor: Would write to #{@output_path}"
45
- puts " (relative to working directory)"
49
+ puts " (relative to working directory, env: #{env})"
46
50
  end
47
51
  end
48
52
 
@@ -51,11 +55,12 @@ class TimingTask < Taski::Task
51
55
  exports :duration_info
52
56
 
53
57
  def run
54
- start_time = Taski::Context.started_at
58
+ start_time = Taski.context.started_at
55
59
  current_time = Time.now
56
60
  elapsed = current_time - start_time
57
61
 
58
62
  puts "TimingTask: #{elapsed.round(3)}s since execution started"
63
+ puts " Debug mode: #{Taski.context.fetch(:debug, false)}"
59
64
 
60
65
  @duration_info = {
61
66
  started: start_time,
@@ -71,7 +76,8 @@ class MainTask < Taski::Task
71
76
 
72
77
  def run
73
78
  puts "\nMainTask executing..."
74
- puts " Root task is: #{Taski::Context.root_task}"
79
+ puts " Root task is: #{Taski.context.root_task}"
80
+ puts " Environment: #{Taski.context[:env]}"
75
81
 
76
82
  # Access dependencies
77
83
  setup = SetupTask.setup_info
@@ -82,7 +88,7 @@ class MainTask < Taski::Task
82
88
  setup: setup,
83
89
  output_path: output,
84
90
  timing: timing,
85
- root_task: Taski::Context.root_task.to_s
91
+ root_task: Taski.context.root_task.to_s
86
92
  }
87
93
 
88
94
  puts "\nExecution Summary:"
@@ -92,15 +98,15 @@ class MainTask < Taski::Task
92
98
  end
93
99
  end
94
100
 
95
- puts "\n1. Running MainTask (context will show MainTask as root)"
101
+ puts "\n1. Running MainTask with context options"
96
102
  puts "-" * 40
97
- MainTask.run
103
+ MainTask.run(context: {env: "production", debug: true})
98
104
 
99
105
  puts "\n" + "=" * 40
100
- puts "\n2. Running SetupTask directly (context will show SetupTask as root)"
106
+ puts "\n2. Running SetupTask directly with different context"
101
107
  puts "-" * 40
102
108
  SetupTask.reset!
103
- SetupTask.run
109
+ SetupTask.run(context: {env: "staging"})
104
110
 
105
111
  puts "\n" + "=" * 40
106
112
  puts "\n3. Dependency Tree"
@@ -109,4 +115,4 @@ puts MainTask.tree
109
115
 
110
116
  puts "\n" + "=" * 40
111
117
  puts "Context API demonstration complete!"
112
- puts "Note: Context provides runtime information without affecting dependency analysis."
118
+ puts "Note: Context provides runtime information and user options without affecting dependency analysis."
data/lib/taski/context.rb CHANGED
@@ -3,50 +3,48 @@
3
3
  require "monitor"
4
4
 
5
5
  module Taski
6
- # Runtime context accessible from any task (not included in dependency analysis).
6
+ # Runtime context accessible from any task.
7
+ # Holds user-defined options and execution metadata.
8
+ # Context is immutable after creation - options cannot be modified during task execution.
7
9
  class Context
8
- @monitor = Monitor.new
10
+ attr_reader :started_at, :working_directory, :root_task
9
11
 
10
- class << self
11
- # @return [String] The working directory path
12
- def working_directory
13
- @monitor.synchronize do
14
- @working_directory ||= Dir.pwd
15
- end
16
- end
17
-
18
- # @return [Time] The start time
19
- def started_at
20
- @monitor.synchronize do
21
- @started_at ||= Time.now
22
- end
23
- end
12
+ # @param options [Hash] User-defined options (immutable after creation)
13
+ # @param root_task [Class] The root task class that initiated execution
14
+ def initialize(options:, root_task:)
15
+ @options = options.dup.freeze
16
+ @root_task = root_task
17
+ @started_at = Time.now
18
+ @working_directory = Dir.pwd
19
+ end
24
20
 
25
- # @return [Class, nil] The root task class or nil if not set
26
- def root_task
27
- @monitor.synchronize do
28
- @root_task
29
- end
30
- end
21
+ # Get a user-defined option value
22
+ # @param key [Symbol, String] The option key
23
+ # @return [Object, nil] The option value or nil if not set
24
+ def [](key)
25
+ @options[key]
26
+ end
31
27
 
32
- # Called internally when a task is first invoked. Only the first call has effect.
33
- # @param task_class [Class] The task class to set as root
34
- def set_root_task(task_class)
35
- @monitor.synchronize do
36
- return if @root_task
37
- @root_task = task_class
38
- @started_at ||= Time.now
39
- @working_directory ||= Dir.pwd
40
- end
28
+ # Get a user-defined option value with a default
29
+ # @param key [Symbol, String] The option key
30
+ # @param default [Object] Default value if key is not present
31
+ # @yield Block to compute default value if key is not present
32
+ # @return [Object] The option value or default
33
+ def fetch(key, default = nil, &block)
34
+ if @options.key?(key)
35
+ @options[key]
36
+ elsif block
37
+ block.call
38
+ else
39
+ default
41
40
  end
41
+ end
42
42
 
43
- def reset!
44
- @monitor.synchronize do
45
- @working_directory = nil
46
- @started_at = nil
47
- @root_task = nil
48
- end
49
- end
43
+ # Check if a user-defined option key exists
44
+ # @param key [Symbol, String] The option key
45
+ # @return [Boolean] true if the key exists
46
+ def key?(key)
47
+ @options.key?(key)
50
48
  end
51
49
  end
52
50
  end
data/lib/taski/section.rb CHANGED
@@ -9,6 +9,13 @@ module Taski
9
9
  def interfaces(*interface_methods)
10
10
  exports(*interface_methods)
11
11
  end
12
+
13
+ # Section does not have static dependencies for execution.
14
+ # The impl method is called at runtime to determine the actual implementation.
15
+ # Static dependencies (impl candidates) are only used for tree display and circular detection.
16
+ def cached_dependencies
17
+ Set.new
18
+ end
12
19
  end
13
20
 
14
21
  def run
@@ -6,29 +6,50 @@ require_relative "visitor"
6
6
  module Taski
7
7
  module StaticAnalysis
8
8
  class Analyzer
9
+ # Analyzes a task class and returns its static dependencies.
10
+ # For Task: dependencies detected from run method (SomeTask.method calls)
11
+ # For Section: impl candidates detected from impl method (constants returned)
12
+ #
13
+ # Static dependencies are used for:
14
+ # - Tree display visualization
15
+ # - Circular dependency detection
16
+ # - Task execution (for Task only; Section resolves impl at runtime)
17
+ #
9
18
  # @param task_class [Class] The task class to analyze
10
- # @return [Set<Class>] Set of task classes that are dependencies
19
+ # @return [Set<Class>] Set of task classes that are static dependencies
11
20
  def self.analyze(task_class)
12
- source_location = extract_run_method_location(task_class)
21
+ target_method = target_method_for(task_class)
22
+ source_location = extract_method_location(task_class, target_method)
13
23
  return Set.new unless source_location
14
24
 
15
25
  file_path, _line_number = source_location
16
26
  parse_result = Prism.parse_file(file_path)
17
27
 
18
- visitor = Visitor.new(task_class)
28
+ visitor = Visitor.new(task_class, target_method)
19
29
  visitor.visit(parse_result.value)
20
30
  visitor.dependencies
21
31
  end
22
32
 
23
33
  # @param task_class [Class] The task class
34
+ # @return [Symbol] The method name to analyze (:run for Task, :impl for Section)
35
+ def self.target_method_for(task_class)
36
+ if defined?(Taski::Section) && task_class < Taski::Section
37
+ :impl
38
+ else
39
+ :run
40
+ end
41
+ end
42
+
43
+ # @param task_class [Class] The task class
44
+ # @param method_name [Symbol] The method name to extract location for
24
45
  # @return [Array<String, Integer>, nil] File path and line number, or nil
25
- def self.extract_run_method_location(task_class)
26
- task_class.instance_method(:run).source_location
46
+ def self.extract_method_location(task_class, method_name)
47
+ task_class.instance_method(method_name).source_location
27
48
  rescue NameError
28
49
  nil
29
50
  end
30
51
 
31
- private_class_method :extract_run_method_location
52
+ private_class_method :target_method_for, :extract_method_location
32
53
  end
33
54
  end
34
55
  end
@@ -7,11 +7,14 @@ module Taski
7
7
  class Visitor < Prism::Visitor
8
8
  attr_reader :dependencies
9
9
 
10
- def initialize(target_task_class)
10
+ # @param target_task_class [Class] The task class to analyze
11
+ # @param target_method [Symbol] The method name to analyze (:run or :impl)
12
+ def initialize(target_task_class, target_method = :run)
11
13
  super()
12
14
  @target_task_class = target_task_class
15
+ @target_method = target_method
13
16
  @dependencies = Set.new
14
- @in_target_run_method = false
17
+ @in_target_method = false
15
18
  @current_namespace_path = []
16
19
  end
17
20
 
@@ -24,17 +27,29 @@ module Taski
24
27
  end
25
28
 
26
29
  def visit_def_node(node)
27
- if node.name == :run && in_target_class?
28
- @in_target_run_method = true
30
+ if node.name == @target_method && in_target_class?
31
+ @in_target_method = true
29
32
  super
30
- @in_target_run_method = false
33
+ @in_target_method = false
31
34
  else
32
35
  super
33
36
  end
34
37
  end
35
38
 
36
39
  def visit_call_node(node)
37
- detect_task_dependency(node) if @in_target_run_method
40
+ detect_task_dependency(node) if @in_target_method
41
+ super
42
+ end
43
+
44
+ def visit_constant_read_node(node)
45
+ # For Section.impl, detect constants as impl candidates (static dependencies)
46
+ detect_impl_candidate(node) if in_impl_method?
47
+ super
48
+ end
49
+
50
+ def visit_constant_path_node(node)
51
+ # For Section.impl, detect constants as impl candidates (static dependencies)
52
+ detect_impl_candidate(node) if in_impl_method?
38
53
  super
39
54
  end
40
55
 
@@ -55,6 +70,15 @@ module Taski
55
70
  node.slice
56
71
  end
57
72
 
73
+ def in_impl_method?
74
+ @in_target_method && @target_method == :impl
75
+ end
76
+
77
+ def detect_impl_candidate(node)
78
+ constant_name = node.slice
79
+ resolve_and_add_dependency(constant_name)
80
+ end
81
+
58
82
  def detect_task_dependency(node)
59
83
  return unless node.receiver
60
84