taski 0.8.3 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +39 -0
  3. data/README.md +65 -50
  4. data/docs/GUIDE.md +41 -56
  5. data/examples/README.md +10 -29
  6. data/examples/clean_demo.rb +25 -65
  7. data/examples/large_tree_demo.rb +356 -0
  8. data/examples/message_demo.rb +0 -1
  9. data/examples/progress_demo.rb +13 -24
  10. data/examples/reexecution_demo.rb +8 -44
  11. data/lib/taski/execution/execution_facade.rb +150 -0
  12. data/lib/taski/execution/executor.rb +156 -357
  13. data/lib/taski/execution/registry.rb +15 -19
  14. data/lib/taski/execution/scheduler.rb +161 -140
  15. data/lib/taski/execution/task_observer.rb +41 -0
  16. data/lib/taski/execution/task_output_router.rb +41 -58
  17. data/lib/taski/execution/task_wrapper.rb +123 -219
  18. data/lib/taski/execution/worker_pool.rb +238 -64
  19. data/lib/taski/logging.rb +105 -0
  20. data/lib/taski/progress/layout/base.rb +600 -0
  21. data/lib/taski/progress/layout/filters.rb +126 -0
  22. data/lib/taski/progress/layout/log.rb +27 -0
  23. data/lib/taski/progress/layout/simple.rb +166 -0
  24. data/lib/taski/progress/layout/tags.rb +76 -0
  25. data/lib/taski/progress/layout/theme_drop.rb +84 -0
  26. data/lib/taski/progress/layout/tree.rb +300 -0
  27. data/lib/taski/progress/theme/base.rb +224 -0
  28. data/lib/taski/progress/theme/compact.rb +58 -0
  29. data/lib/taski/progress/theme/default.rb +25 -0
  30. data/lib/taski/progress/theme/detail.rb +48 -0
  31. data/lib/taski/progress/theme/plain.rb +40 -0
  32. data/lib/taski/static_analysis/analyzer.rb +5 -17
  33. data/lib/taski/static_analysis/dependency_graph.rb +19 -1
  34. data/lib/taski/static_analysis/visitor.rb +1 -39
  35. data/lib/taski/task.rb +44 -58
  36. data/lib/taski/test_helper/errors.rb +1 -1
  37. data/lib/taski/test_helper.rb +21 -35
  38. data/lib/taski/version.rb +1 -1
  39. data/lib/taski.rb +60 -61
  40. data/sig/taski.rbs +194 -203
  41. metadata +31 -8
  42. data/examples/section_demo.rb +0 -195
  43. data/lib/taski/execution/base_progress_display.rb +0 -393
  44. data/lib/taski/execution/execution_context.rb +0 -390
  45. data/lib/taski/execution/plain_progress_display.rb +0 -76
  46. data/lib/taski/execution/simple_progress_display.rb +0 -247
  47. data/lib/taski/execution/tree_progress_display.rb +0 -643
  48. data/lib/taski/section.rb +0 -74
@@ -24,16 +24,14 @@ module Taski
24
24
  # @param task_class [Class] The task class
25
25
  # @param wrapper [TaskWrapper] The wrapper instance to register
26
26
  def register(task_class, wrapper)
27
- @tasks[task_class] = wrapper
27
+ @monitor.synchronize { @tasks[task_class] = wrapper }
28
28
  end
29
29
 
30
+ # Check if a task wrapper has been registered (created during run phase).
30
31
  # @param task_class [Class] The task class
31
- # @return [Object] The task instance
32
- # @raise [RuntimeError] If the task is not registered
33
- def get_task(task_class)
34
- @tasks.fetch(task_class) do
35
- raise "Task #{task_class} not registered"
36
- end
32
+ # @return [Boolean] true if a wrapper exists for this task
33
+ def registered?(task_class)
34
+ @monitor.synchronize { @tasks.key?(task_class) }
37
35
  end
38
36
 
39
37
  # @param thread [Thread] The thread to register
@@ -77,19 +75,17 @@ module Taski
77
75
  end
78
76
  end
79
77
 
80
- # @param task_class [Class] The task class to run
81
- # @param exported_methods [Array<Symbol>] Methods to call to trigger execution
82
- # @return [Object] The result of the task execution
83
- def run(task_class, exported_methods)
84
- exported_methods.each do |method|
85
- task_class.public_send(method)
78
+ # Create or retrieve a TaskWrapper for the given task class.
79
+ # Encapsulates the standard wrapper creation pattern used by Executor and WorkerPool.
80
+ # @param task_class [Class] The task class
81
+ # @param execution_facade [ExecutionFacade] The execution facade
82
+ # @return [TaskWrapper] The wrapper instance
83
+ def create_wrapper(task_class, execution_facade:)
84
+ get_or_create(task_class) do
85
+ task_instance = task_class.allocate
86
+ task_instance.send(:initialize)
87
+ TaskWrapper.new(task_instance, registry: self, execution_facade: execution_facade)
86
88
  end
87
-
88
- wait_all
89
-
90
- # @type var wrapper: Taski::Execution::TaskWrapper
91
- wrapper = get_task(task_class)
92
- wrapper.result
93
89
  end
94
90
  end
95
91
  end
@@ -3,13 +3,19 @@
3
3
  module Taski
4
4
  module Execution
5
5
  # Scheduler manages task dependency state and determines execution order.
6
- # It tracks which tasks are pending, enqueued, or completed, and provides
7
- # methods to determine which tasks are ready to execute.
6
+ # Both run and clean phases use the same unified state set:
7
+ # :pending, :running, :completed, :failed, :skipped.
8
+ #
9
+ # == State Transitions
10
+ #
11
+ # Run phase: pending → running → completed | failed
12
+ # pending → skipped (when a dependency fails)
13
+ # Clean phase: pending → running → completed
8
14
  #
9
15
  # == Responsibilities
10
16
  #
11
- # - Build dependency graph from root task via static analysis
12
- # - Track task states: pending, enqueued, completed
17
+ # - Load pre-built dependency graph from Executor
18
+ # - Track task states: pending, running, completed
13
19
  # - Determine which tasks are ready to execute (all dependencies completed)
14
20
  # - Provide next_ready_tasks for the Executor's event loop
15
21
  # - Build reverse dependency graph for clean operations
@@ -19,19 +25,19 @@ module Taski
19
25
  # == API
20
26
  #
21
27
  # Run operations:
22
- # - {#build_dependency_graph} - Initialize dependency graph from root task
28
+ # - {#load_graph} - Load pre-built dependency graph
23
29
  # - {#next_ready_tasks} - Get tasks ready for execution
24
- # - {#mark_enqueued} - Mark task as sent to worker pool
30
+ # - {#mark_running} - Mark task as sent to worker pool
25
31
  # - {#mark_completed} - Mark task as finished
26
- # - {#completed?} - Check if task is completed
32
+ # - {#finished?} - Check if task is completed
27
33
  # - {#running_tasks?} - Check if any tasks are currently executing
28
34
  #
29
35
  # Clean operations:
30
36
  # - {#build_reverse_dependency_graph} - Build reverse graph for clean order
31
37
  # - {#next_ready_clean_tasks} - Get tasks ready for clean (reverse order)
32
- # - {#mark_clean_enqueued} - Mark task as sent for clean
38
+ # - {#mark_clean_running} - Mark task as sent for clean
33
39
  # - {#mark_clean_completed} - Mark task as clean finished
34
- # - {#clean_completed?} - Check if task clean is completed
40
+ # - {#clean_finished?} - Check if task clean is completed
35
41
  # - {#running_clean_tasks?} - Check if any clean tasks are currently executing
36
42
  #
37
43
  # == Thread Safety
@@ -40,59 +46,56 @@ module Taski
40
46
  # so no synchronization is needed. The Executor serializes all
41
47
  # access to the Scheduler through its event loop.
42
48
  class Scheduler
43
- # Task execution states
49
+ # Unified task execution states (used by both run and clean phases)
44
50
  STATE_PENDING = :pending
45
- STATE_ENQUEUED = :enqueued
51
+ STATE_RUNNING = :running
46
52
  STATE_COMPLETED = :completed
47
-
48
- # Clean execution states (independent from run states)
49
- CLEAN_STATE_PENDING = :clean_pending
50
- CLEAN_STATE_ENQUEUED = :clean_enqueued
51
- CLEAN_STATE_COMPLETED = :clean_completed
53
+ STATE_FAILED = :failed
54
+ STATE_SKIPPED = :skipped
52
55
 
53
56
  ##
54
57
  # Initializes internal data structures used to track normal and clean task execution.
55
- #
56
- # Sets up:
57
- # - @dependencies: map from task class to its dependency task classes.
58
- # - @task_states: map from task class to its normal execution state.
59
- # - @completed_tasks: set of task classes that have completed normal execution.
60
- # - @reverse_dependencies: map from task class to dependent task classes (used for clean ordering).
61
- # - @clean_task_states: map from task class to its clean execution state.
62
- # - @clean_completed_tasks: set of task classes that have completed clean execution.
63
58
  def initialize
64
59
  # Run execution state
65
60
  @dependencies = {}
66
61
  @task_states = {}
67
- @completed_tasks = Set.new
62
+ @finished_tasks = Set.new
63
+ @run_reverse_deps = {}
68
64
 
69
- # Clean execution state (independent tracking)
65
+ # Clean execution state (independent tracking, same state values)
70
66
  @reverse_dependencies = {}
71
67
  @clean_task_states = {}
72
- @clean_completed_tasks = Set.new
68
+ @clean_finished_tasks = Set.new
73
69
  end
74
70
 
75
- # Build dependency graph by traversing from root task.
76
- # Populates internal state with all tasks and their dependencies.
71
+ # Load dependency graph from a pre-built DependencyGraph.
72
+ # Populates internal state with all tasks and their dependencies via BFS from root.
77
73
  #
74
+ # @param dependency_graph [StaticAnalysis::DependencyGraph] Pre-built graph
78
75
  # @param root_task_class [Class] The root task class to start from
79
- def build_dependency_graph(root_task_class)
76
+ def load_graph(dependency_graph, root_task_class)
80
77
  # @type var queue: Array[singleton(Taski::Task)]
81
78
  queue = [root_task_class]
82
79
 
83
80
  while (task_class = queue.shift)
84
81
  next if @task_states.key?(task_class)
85
82
 
86
- deps = task_class.cached_dependencies
83
+ deps = dependency_graph.dependencies_for(task_class)
87
84
  @dependencies[task_class] = deps.dup
88
85
  @task_states[task_class] = STATE_PENDING
86
+ @run_reverse_deps[task_class] ||= Set.new
89
87
 
90
- deps.each { |dep| queue << dep }
88
+ deps.each do |dep|
89
+ @run_reverse_deps[dep] ||= Set.new
90
+ @run_reverse_deps[dep].add(task_class)
91
+ log_dependency_resolved(task_class, dep)
92
+ queue << dep
93
+ end
91
94
  end
92
95
  end
93
96
 
94
97
  # Get all tasks that are ready to execute.
95
- # A task is ready when all its dependencies are completed.
98
+ # A task is ready when it is pending and all its dependencies are completed.
96
99
  #
97
100
  # @return [Array<Class>] Array of task classes ready for execution
98
101
  def next_ready_tasks
@@ -105,11 +108,12 @@ module Taski
105
108
  ready
106
109
  end
107
110
 
108
- # Mark a task as enqueued for execution.
111
+ # Mark a task as running (sent to worker pool).
112
+ # Prevents the task from being selected again by next_ready_tasks.
109
113
  #
110
114
  # @param task_class [Class] The task class to mark
111
- def mark_enqueued(task_class)
112
- @task_states[task_class] = STATE_ENQUEUED
115
+ def mark_running(task_class)
116
+ @task_states[task_class] = STATE_RUNNING
113
117
  end
114
118
 
115
119
  # Mark a task as completed.
@@ -117,65 +121,101 @@ module Taski
117
121
  # @param task_class [Class] The task class to mark
118
122
  def mark_completed(task_class)
119
123
  @task_states[task_class] = STATE_COMPLETED
120
- @completed_tasks.add(task_class)
124
+ @finished_tasks.add(task_class)
125
+ end
126
+
127
+ # Mark a task as failed.
128
+ # Failed tasks are added to the finished set so dependents can proceed
129
+ # (they will be skipped by the Executor's skip_pending_dependents).
130
+ #
131
+ # @param task_class [Class] The task class to mark
132
+ def mark_failed(task_class)
133
+ @task_states[task_class] = STATE_FAILED
134
+ @finished_tasks.add(task_class)
121
135
  end
122
136
 
123
- # Check if a task is completed.
137
+ # Check if a task is in pending state.
124
138
  #
125
139
  # @param task_class [Class] The task class to check
126
- # @return [Boolean] true if the task is completed
127
- def completed?(task_class)
128
- @completed_tasks.include?(task_class)
140
+ # @return [Boolean] true if the task is pending
141
+ def pending?(task_class)
142
+ @task_states[task_class] == STATE_PENDING
129
143
  end
130
144
 
131
- # Check if there are any running (enqueued) tasks.
145
+ # Check if a task is finished (completed or failed).
132
146
  #
133
- ##
134
- # Indicates whether any tasks are currently enqueued for execution.
135
- # @return [Boolean] `true` if any task is enqueued, `false` otherwise.
147
+ # @param task_class [Class] The task class to check
148
+ # @return [Boolean] true if the task is finished
149
+ def finished?(task_class)
150
+ @finished_tasks.include?(task_class)
151
+ end
152
+
153
+ # Check if there are any running tasks.
154
+ #
155
+ # @return [Boolean] true if any task is running, false otherwise.
136
156
  def running_tasks?
137
- @task_states.values.any? { |state| state == STATE_ENQUEUED }
157
+ @task_states.values.any? { |state| state == STATE_RUNNING }
138
158
  end
139
159
 
140
- # ========================================
141
- # Runtime Dependency Merging
142
- # ========================================
160
+ # Get the total number of tasks in the dependency graph.
161
+ #
162
+ # @return [Integer] The number of tasks
163
+ def task_count
164
+ @task_states.size
165
+ end
166
+
167
+ # Get task classes that were never executed (remained in STATE_PENDING).
168
+ # These are tasks discovered by the static dependency graph
169
+ # (via load_graph) but not reached at runtime — e.g.,
170
+ # skipped due to conditional logic inside Task#run or because the
171
+ # root task completed before all statically-discovered tasks were needed.
172
+ #
173
+ # @return [Array<Class>] Array of task classes still pending
174
+ def never_started_task_classes
175
+ @task_states.select { |_, state| state == STATE_PENDING }.keys
176
+ end
177
+
178
+ # Mark a task as skipped (never executed). Only transitions from pending.
179
+ #
180
+ # @param task_class [Class] The task class to mark as skipped
181
+ # @return [Boolean] true if the state was changed
182
+ def mark_skipped(task_class)
183
+ return false unless @task_states[task_class] == STATE_PENDING
184
+ @task_states[task_class] = STATE_SKIPPED
185
+ true
186
+ end
143
187
 
144
- # Merge runtime dependencies into the dependency graph.
145
- # Used to incorporate dynamically selected dependencies (e.g., Section implementations)
146
- # that were determined during the run phase.
188
+ # Get the count of tasks in STATE_SKIPPED.
147
189
  #
148
- # This method also updates reverse dependencies if they exist (for clean operations).
149
- # This method is idempotent - calling it multiple times with the same data is safe.
190
+ # @return [Integer] Number of explicitly skipped tasks
191
+ def skipped_count
192
+ @task_states.count { |_, state| state == STATE_SKIPPED }
193
+ end
194
+
195
+ # Find all pending tasks that transitively depend on the given task.
196
+ # Traverses the reverse dependency graph (run phase) using BFS.
197
+ # Only returns tasks in STATE_PENDING; running/completed tasks are
198
+ # traversed through but not included in results.
150
199
  #
151
- # @param runtime_deps [Hash{Class => Set<Class>}] Runtime dependencies from ExecutionContext
152
- def merge_runtime_dependencies(runtime_deps)
153
- runtime_deps.each do |from_class, to_classes|
154
- # Ensure the from_class exists in the graph
155
- @dependencies[from_class] ||= Set.new
156
- @task_states[from_class] ||= STATE_PENDING
157
-
158
- to_classes.each do |to_class|
159
- # Add the dependency relationship
160
- @dependencies[from_class].add(to_class)
161
-
162
- # Add the to_class to the graph if not present
163
- unless @dependencies.key?(to_class)
164
- @dependencies[to_class] = to_class.cached_dependencies
165
- @task_states[to_class] = STATE_PENDING
166
- end
167
-
168
- # Update reverse dependencies if they exist (for clean operations)
169
- # If A depends on B (from_class→to_class), then B→[A] in reverse graph
170
- if @reverse_dependencies.any?
171
- @reverse_dependencies[to_class] ||= Set.new
172
- @reverse_dependencies[to_class].add(from_class)
173
-
174
- # Ensure to_class has clean state initialized
175
- @clean_task_states[to_class] ||= CLEAN_STATE_PENDING
176
- end
200
+ # @param task_class [Class] The task to find dependents of
201
+ # @return [Array<Class>] Pending transitive dependents
202
+ def pending_dependents_of(task_class)
203
+ result = []
204
+ queue = [task_class]
205
+ visited = Set.new([task_class])
206
+
207
+ while (tc = queue.shift)
208
+ dependents = @run_reverse_deps[tc] || Set.new
209
+ dependents.each do |dep|
210
+ next if visited.include?(dep)
211
+ visited.add(dep)
212
+
213
+ result << dep if @task_states[dep] == STATE_PENDING
214
+ queue << dep
177
215
  end
178
216
  end
217
+
218
+ result
179
219
  end
180
220
 
181
221
  # ========================================
@@ -186,28 +226,18 @@ module Taski
186
226
  # Clean operations run in reverse order: if A depends on B, then B must
187
227
  # be cleaned after A (so A→[B] in reverse graph means B depends on A's clean).
188
228
  #
189
- # Also initializes clean states for all tasks to CLEAN_STATE_PENDING.
190
- #
191
- ##
192
- # Builds the reverse dependency graph and initializes per-task clean execution state starting from the given root task.
193
- #
194
- # Ensures the forward dependency graph is present, clears prior clean-state data, initializes each discovered task with
195
- # an empty reverse-dependency set and `CLEAN_STATE_PENDING`, and populates reverse mappings so a task's clean run
196
- # depends on the clean completion of tasks that depend on it.
197
- # @param [Class] root_task_class The root task class from which to discover tasks and their dependencies.
198
- def build_reverse_dependency_graph(root_task_class)
199
- # First, ensure we have the forward dependency graph
200
- build_dependency_graph(root_task_class) if @dependencies.empty?
201
-
229
+ # Requires load_graph to have been called first to populate @dependencies.
230
+ # Also initializes clean states for all tasks to STATE_PENDING.
231
+ def build_reverse_dependency_graph
202
232
  # Clear previous clean state
203
233
  @reverse_dependencies.clear
204
234
  @clean_task_states.clear
205
- @clean_completed_tasks.clear
235
+ @clean_finished_tasks.clear
206
236
 
207
237
  # Initialize all tasks with empty reverse dependency sets
208
238
  @dependencies.each_key do |task_class|
209
239
  @reverse_dependencies[task_class] = Set.new
210
- @clean_task_states[task_class] = CLEAN_STATE_PENDING
240
+ @clean_task_states[task_class] = STATE_PENDING
211
241
  end
212
242
 
213
243
  # Build reverse mappings: if A depends on B, then B→[A] in reverse graph
@@ -220,88 +250,79 @@ module Taski
220
250
  end
221
251
 
222
252
  # Get all tasks that are ready to clean.
223
- # A task is ready to clean when all its reverse dependencies (dependents)
224
- # have completed their clean operation.
253
+ # A task is ready to clean when it is pending and all its reverse
254
+ # dependencies (dependents) have completed their clean operation.
225
255
  #
226
- ##
227
- # Lists task classes that are ready for clean execution.
228
- # A task is considered ready when its clean state is `CLEAN_STATE_PENDING` and all of its reverse dependencies have completed their clean execution.
229
256
  # @return [Array<Class>] Array of task classes ready for clean execution.
230
257
  def next_ready_clean_tasks
231
258
  ready = []
232
259
  @clean_task_states.each_key do |task_class|
233
- next unless @clean_task_states[task_class] == CLEAN_STATE_PENDING
260
+ next unless @clean_task_states[task_class] == STATE_PENDING
234
261
  next unless ready_to_clean?(task_class)
235
262
  ready << task_class
236
263
  end
237
264
  ready
238
265
  end
239
266
 
240
- # Mark a task as enqueued for clean execution.
267
+ # Mark a task as running for clean execution.
241
268
  #
242
- ##
243
- # Marks the given task class as enqueued for clean execution.
244
- # @param [Class] task_class The task class to mark as enqueued for clean execution.
245
- def mark_clean_enqueued(task_class)
246
- @clean_task_states[task_class] = CLEAN_STATE_ENQUEUED
269
+ # @param task_class [Class] The task class to mark as running for clean.
270
+ def mark_clean_running(task_class)
271
+ @clean_task_states[task_class] = STATE_RUNNING
247
272
  end
248
273
 
249
274
  # Mark a task as clean completed.
250
275
  #
251
- ##
252
- # Marks the clean execution of the given task class as completed and records it.
253
- # @param [Class] task_class - The task class to mark as clean completed.
276
+ # @param task_class [Class] The task class to mark as clean completed.
254
277
  def mark_clean_completed(task_class)
255
- @clean_task_states[task_class] = CLEAN_STATE_COMPLETED
256
- @clean_completed_tasks.add(task_class)
278
+ @clean_task_states[task_class] = STATE_COMPLETED
279
+ @clean_finished_tasks.add(task_class)
280
+ end
281
+
282
+ # Mark a task as clean failed.
283
+ # Adds to @clean_finished_tasks so dependents are not blocked.
284
+ #
285
+ # @param task_class [Class] The task class to mark as clean failed.
286
+ def mark_clean_failed(task_class)
287
+ @clean_task_states[task_class] = STATE_FAILED
288
+ @clean_finished_tasks.add(task_class)
257
289
  end
258
290
 
259
291
  # Check if a task's clean is completed.
260
292
  #
261
- # @param task_class [Class] The task class to check
262
- ##
263
- # Checks whether a task class has completed its clean execution.
264
- # @param [Class] task_class - The task class to check.
265
- # @return [Boolean] `true` if the task's clean is completed, `false` otherwise.
266
- def clean_completed?(task_class)
267
- @clean_completed_tasks.include?(task_class)
293
+ # @param task_class [Class] The task class to check.
294
+ # @return [Boolean] true if the task's clean is completed, false otherwise.
295
+ def clean_finished?(task_class)
296
+ @clean_finished_tasks.include?(task_class)
268
297
  end
269
298
 
270
- # Check if there are any running (enqueued) clean tasks.
299
+ # Check if there are any running clean tasks.
271
300
  #
272
- ##
273
- # Indicates whether any clean tasks are currently enqueued for execution.
274
- # @return [Boolean] `true` if at least one clean task is enqueued, `false` otherwise.
301
+ # @return [Boolean] true if at least one clean task is running, false otherwise.
275
302
  def running_clean_tasks?
276
- @clean_task_states.values.any? { |state| state == CLEAN_STATE_ENQUEUED }
303
+ @clean_task_states.values.any? { |state| state == STATE_RUNNING }
277
304
  end
278
305
 
279
306
  private
280
307
 
281
308
  # Check if a task is ready to execute (all dependencies completed).
282
- #
283
- # @param task_class [Class] The task class to check
284
- ##
285
- # Determines whether a task's dependencies have all completed.
286
- # @param [Class] task_class - The task class to check.
287
- # @return [Boolean] `true` if every dependency of `task_class` is in the set of completed tasks, `false` otherwise.
288
309
  def ready_to_execute?(task_class)
289
310
  task_deps = @dependencies[task_class] || Set.new
290
- task_deps.subset?(@completed_tasks)
311
+ task_deps.subset?(@finished_tasks)
291
312
  end
292
313
 
293
314
  # Check if a task is ready to clean (all reverse dependencies completed).
294
- # Reverse dependencies are tasks that depend on this task, which must
295
- # be cleaned before this task can be cleaned.
296
- #
297
- # @param task_class [Class] The task class to check
298
- ##
299
- # Determines whether a task is ready for clean execution.
300
- # @param [Class] task_class - The task class to check.
301
- # @return [Boolean] `true` if all tasks that depend on `task_class` have completed their clean execution, `false` otherwise.
302
315
  def ready_to_clean?(task_class)
303
316
  reverse_deps = @reverse_dependencies[task_class] || Set.new
304
- reverse_deps.subset?(@clean_completed_tasks)
317
+ reverse_deps.subset?(@clean_finished_tasks)
318
+ end
319
+
320
+ def log_dependency_resolved(from_task, to_task)
321
+ Taski::Logging.debug(
322
+ Taski::Logging::Events::DEPENDENCY_RESOLVED,
323
+ from_task: from_task.name,
324
+ to_task: to_task.name
325
+ )
305
326
  end
306
327
  end
307
328
  end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Taski
4
+ module Execution
5
+ # Base class for observers of the execution lifecycle.
6
+ # Subclasses override only the events they care about.
7
+ #
8
+ # All events are defined as no-op methods. The +context+ accessor
9
+ # is auto-injected by ExecutionFacade#add_observer.
10
+ #
11
+ # == Event Methods
12
+ #
13
+ # - on_ready — facade is configured, observer can pull from context
14
+ # - on_start — execution is about to begin
15
+ # - on_stop — execution has finished
16
+ # - on_task_updated(task_class, previous_state:, current_state:, phase:, timestamp:)
17
+ # - on_group_started(task_class, group_name, phase:, timestamp:)
18
+ # - on_group_completed(task_class, group_name, phase:, timestamp:)
19
+ class TaskObserver
20
+ attr_accessor :context
21
+
22
+ def on_ready
23
+ end
24
+
25
+ def on_start
26
+ end
27
+
28
+ def on_stop
29
+ end
30
+
31
+ def on_task_updated(task_class, previous_state:, current_state:, phase:, timestamp:)
32
+ end
33
+
34
+ def on_group_started(task_class, group_name, phase:, timestamp:)
35
+ end
36
+
37
+ def on_group_completed(task_class, group_name, phase:, timestamp:)
38
+ end
39
+ end
40
+ end
41
+ end