taski 0.4.1 → 0.5.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.
@@ -7,17 +7,36 @@ module Taski
7
7
  class Visitor < Prism::Visitor
8
8
  attr_reader :dependencies
9
9
 
10
- def initialize(target_task_class, target_method = :run)
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
+ # @param methods_to_analyze [Set<Symbol>] Set of method names to analyze (for following calls)
13
+ def initialize(target_task_class, target_method = :run, methods_to_analyze = nil)
11
14
  super()
12
15
  @target_task_class = target_task_class
13
16
  @target_method = target_method
14
17
  @dependencies = Set.new
15
18
  @in_target_method = false
16
19
  @current_namespace_path = []
20
+ # Methods to analyze: starts with just the target method, grows as we find calls
21
+ @methods_to_analyze = methods_to_analyze || Set.new([@target_method])
22
+ # Track which methods we've already analyzed to prevent infinite loops
23
+ @analyzed_methods = Set.new
24
+ # Collect method calls made within analyzed methods (for following)
25
+ @method_calls_to_follow = Set.new
26
+ # Store method definitions found in the class for later analysis
27
+ @class_method_defs = {}
28
+ # Track if we're in an impl call chain (for Section constant detection)
29
+ @in_impl_chain = false
17
30
  end
18
31
 
19
32
  def visit_class_node(node)
20
- within_namespace(extract_constant_name(node.constant_path)) { super }
33
+ within_namespace(extract_constant_name(node.constant_path)) do
34
+ if in_target_class?
35
+ # First pass: collect all method definitions in the target class
36
+ collect_method_definitions(node)
37
+ end
38
+ super
39
+ end
21
40
  end
22
41
 
23
42
  def visit_module_node(node)
@@ -25,32 +44,80 @@ module Taski
25
44
  end
26
45
 
27
46
  def visit_def_node(node)
28
- if node.name == @target_method && in_target_class?
47
+ if in_target_class? && should_analyze_method?(node.name)
48
+ @analyzed_methods.add(node.name)
29
49
  @in_target_method = true
50
+ @current_analyzing_method = node.name
51
+ # Start impl chain when entering impl method
52
+ @in_impl_chain = true if node.name == :impl && @target_method == :impl
30
53
  super
31
54
  @in_target_method = false
55
+ @current_analyzing_method = nil
32
56
  else
33
57
  super
34
58
  end
35
59
  end
36
60
 
37
61
  def visit_call_node(node)
38
- detect_task_dependency(node) if @in_target_method
62
+ if @in_target_method
63
+ detect_task_dependency(node)
64
+ detect_method_call_to_follow(node)
65
+ end
39
66
  super
40
67
  end
41
68
 
42
69
  def visit_constant_read_node(node)
43
- detect_return_constant(node) if @in_target_method && @target_method == :impl
70
+ # For Section.impl, detect constants as impl candidates (static dependencies)
71
+ detect_impl_candidate(node) if in_impl_method?
44
72
  super
45
73
  end
46
74
 
47
75
  def visit_constant_path_node(node)
48
- detect_return_constant(node) if @in_target_method && @target_method == :impl
76
+ # For Section.impl, detect constants as impl candidates (static dependencies)
77
+ detect_impl_candidate(node) if in_impl_method?
49
78
  super
50
79
  end
51
80
 
81
+ # After visiting, follow any method calls that need analysis
82
+ # @in_impl_chain is preserved because methods called from impl should
83
+ # also detect constants as impl candidates
84
+ def follow_method_calls
85
+ new_methods = @method_calls_to_follow - @analyzed_methods
86
+ return if new_methods.empty?
87
+
88
+ # Add new methods to analyze
89
+ @methods_to_analyze.merge(new_methods)
90
+ @method_calls_to_follow.clear
91
+
92
+ # Re-analyze the class methods
93
+ # Preserve impl chain context: methods called from impl should continue
94
+ # detecting constants as impl candidates
95
+ @class_method_defs.each do |method_name, method_node|
96
+ next unless new_methods.include?(method_name)
97
+
98
+ @analyzed_methods.add(method_name)
99
+ @in_target_method = true
100
+ @current_analyzing_method = method_name
101
+ visit(method_node)
102
+ @in_target_method = false
103
+ @current_analyzing_method = nil
104
+ end
105
+
106
+ # Recursively follow any new calls discovered
107
+ follow_method_calls
108
+ end
109
+
52
110
  private
53
111
 
112
+ # Collect all method definitions in the target class for later analysis
113
+ def collect_method_definitions(class_node)
114
+ class_node.body&.body&.each do |node|
115
+ if node.is_a?(Prism::DefNode)
116
+ @class_method_defs[node.name] = node
117
+ end
118
+ end
119
+ end
120
+
54
121
  def within_namespace(name)
55
122
  @current_namespace_path.push(name)
56
123
  yield
@@ -66,18 +133,40 @@ module Taski
66
133
  node.slice
67
134
  end
68
135
 
69
- def detect_task_dependency(node)
70
- return unless node.receiver
136
+ def should_analyze_method?(method_name)
137
+ @methods_to_analyze.include?(method_name) && !@analyzed_methods.include?(method_name)
138
+ end
71
139
 
72
- constant_name = extract_receiver_constant(node.receiver)
73
- resolve_and_add_dependency(constant_name) if constant_name
140
+ def in_impl_method?
141
+ @in_target_method && @in_impl_chain
74
142
  end
75
143
 
76
- def detect_return_constant(node)
144
+ # Detect method calls that should be followed (calls to methods in the same class)
145
+ def detect_method_call_to_follow(node)
146
+ # Only follow calls without explicit receiver (self.method or just method)
147
+ return if node.receiver && !self_receiver?(node.receiver)
148
+
149
+ method_name = node.name
150
+ # Mark this method for later analysis if it's defined in the class
151
+ @method_calls_to_follow.add(method_name) if @class_method_defs.key?(method_name)
152
+ end
153
+
154
+ def self_receiver?(receiver)
155
+ receiver.is_a?(Prism::SelfNode)
156
+ end
157
+
158
+ def detect_impl_candidate(node)
77
159
  constant_name = node.slice
78
160
  resolve_and_add_dependency(constant_name)
79
161
  end
80
162
 
163
+ def detect_task_dependency(node)
164
+ return unless node.receiver
165
+
166
+ constant_name = extract_receiver_constant(node.receiver)
167
+ resolve_and_add_dependency(constant_name) if constant_name
168
+ end
169
+
81
170
  def extract_receiver_constant(receiver)
82
171
  case receiver
83
172
  when Prism::ConstantReadNode, Prism::ConstantPathNode
data/lib/taski/task.rb CHANGED
@@ -2,7 +2,6 @@
2
2
 
3
3
  require_relative "static_analysis/analyzer"
4
4
  require_relative "execution/registry"
5
- require_relative "execution/coordinator"
6
5
  require_relative "execution/task_wrapper"
7
6
 
8
7
  module Taski
@@ -24,11 +23,16 @@ module Taski
24
23
  # Each call creates a fresh TaskWrapper instance for re-execution support.
25
24
  # Use class methods (e.g., MyTask.result) for cached single execution.
26
25
  def new
27
- Execution::TaskWrapper.new(
28
- super,
29
- registry: registry,
30
- coordinator: coordinator
26
+ fresh_registry = Execution::Registry.new
27
+ task_instance = allocate
28
+ task_instance.send(:initialize)
29
+ wrapper = Execution::TaskWrapper.new(
30
+ task_instance,
31
+ registry: fresh_registry
31
32
  )
33
+ # Pre-register to prevent Executor from creating a duplicate wrapper
34
+ fresh_registry.register(self, wrapper)
35
+ wrapper
32
36
  end
33
37
 
34
38
  def cached_dependencies
@@ -55,87 +59,19 @@ module Taski
55
59
  Taski.global_registry
56
60
  end
57
61
 
58
- def coordinator
59
- @coordinator ||= Execution::Coordinator.new(
60
- registry: registry,
61
- analyzer: StaticAnalysis::Analyzer
62
- )
63
- end
64
-
65
62
  def reset!
66
63
  registry.reset!
67
64
  Taski.reset_global_registry!
68
65
  Taski.reset_context!
69
- @coordinator = nil
70
66
  @circular_dependency_checked = false
71
67
  end
72
68
 
73
69
  def tree
74
- build_tree(self, "", Set.new, false)
70
+ Execution::TreeProgressDisplay.render_static_tree(self)
75
71
  end
76
72
 
77
73
  private
78
74
 
79
- # ANSI color codes
80
- COLORS = {
81
- reset: "\e[0m",
82
- task: "\e[32m", # green
83
- section: "\e[34m", # blue
84
- impl: "\e[33m", # yellow
85
- tree: "\e[90m", # gray
86
- name: "\e[1m" # bold
87
- }.freeze
88
-
89
- def build_tree(task_class, prefix, visited, is_impl)
90
- type_label = colored_type_label(task_class)
91
- impl_prefix = is_impl ? "#{COLORS[:impl]}[impl]#{COLORS[:reset]} " : ""
92
- name = "#{COLORS[:name]}#{task_class.name}#{COLORS[:reset]}"
93
- result = "#{impl_prefix}#{name} #{type_label}\n"
94
- return result if visited.include?(task_class)
95
-
96
- visited.add(task_class)
97
- dependencies = task_class.cached_dependencies.to_a
98
- is_section = section_class?(task_class)
99
-
100
- dependencies.each_with_index do |dep, index|
101
- is_last = (index == dependencies.size - 1)
102
- result += format_dependency_branch(dep, prefix, is_last, visited, is_section)
103
- end
104
-
105
- result
106
- end
107
-
108
- def format_dependency_branch(dep, prefix, is_last, visited, is_impl)
109
- connector, extension = tree_connector_chars(is_last)
110
- dep_tree = build_tree(dep, "#{prefix}#{extension}", visited, is_impl)
111
-
112
- result = "#{prefix}#{COLORS[:tree]}#{connector}#{COLORS[:reset]}"
113
- lines = dep_tree.lines
114
- result += lines.first
115
- lines.drop(1).each { |line| result += line }
116
- result
117
- end
118
-
119
- def tree_connector_chars(is_last)
120
- if is_last
121
- ["└── ", " "]
122
- else
123
- ["├── ", "│ "]
124
- end
125
- end
126
-
127
- def colored_type_label(klass)
128
- if section_class?(klass)
129
- "#{COLORS[:section]}(Section)#{COLORS[:reset]}"
130
- else
131
- "#{COLORS[:task]}(Task)#{COLORS[:reset]}"
132
- end
133
- end
134
-
135
- def section_class?(klass)
136
- defined?(Taski::Section) && klass < Taski::Section
137
- end
138
-
139
75
  # Use allocate + initialize instead of new to avoid infinite loop
140
76
  # since new is overridden to return TaskWrapper
141
77
  def cached_wrapper
@@ -144,8 +80,7 @@ module Taski
144
80
  task_instance.send(:initialize)
145
81
  Execution::TaskWrapper.new(
146
82
  task_instance,
147
- registry: registry,
148
- coordinator: coordinator
83
+ registry: registry
149
84
  )
150
85
  end
151
86
  end
data/lib/taski/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Taski
4
- VERSION = "0.4.1"
4
+ VERSION = "0.5.0"
5
5
  end
data/lib/taski.rb CHANGED
@@ -5,9 +5,9 @@ require_relative "taski/static_analysis/analyzer"
5
5
  require_relative "taski/static_analysis/visitor"
6
6
  require_relative "taski/static_analysis/dependency_graph"
7
7
  require_relative "taski/execution/registry"
8
- require_relative "taski/execution/coordinator"
9
8
  require_relative "taski/execution/task_wrapper"
10
- require_relative "taski/execution/parallel_progress_display"
9
+ require_relative "taski/execution/executor"
10
+ require_relative "taski/execution/tree_progress_display"
11
11
  require_relative "taski/context"
12
12
  require_relative "taski/task"
13
13
  require_relative "taski/section"
@@ -59,13 +59,17 @@ module Taski
59
59
  @global_registry = nil
60
60
  end
61
61
 
62
+ # Progress display is enabled by default (tree-style).
63
+ # Environment variables:
64
+ # - TASKI_PROGRESS_DISABLE=1: Disable progress display entirely
65
+ # - TASKI_FORCE_PROGRESS=1: Force enable even without TTY (for testing)
62
66
  def self.progress_display
63
- return nil unless progress_enabled?
64
- @progress_display ||= Execution::ParallelProgressDisplay.new
67
+ return nil if progress_disabled?
68
+ @progress_display ||= Execution::TreeProgressDisplay.new
65
69
  end
66
70
 
67
- def self.progress_enabled?
68
- ENV["TASKI_PROGRESS"] == "1" || ENV["TASKI_FORCE_PROGRESS"] == "1"
71
+ def self.progress_disabled?
72
+ ENV["TASKI_PROGRESS_DISABLE"] == "1"
69
73
  end
70
74
 
71
75
  def self.reset_progress_display!
data/sig/taski.rbs CHANGED
@@ -2,7 +2,8 @@ module Taski
2
2
  VERSION: String
3
3
 
4
4
  self.@global_registry: Execution::Registry?
5
- self.@progress_display: Execution::ParallelProgressDisplay?
5
+ self.@progress_display: Execution::TreeProgressDisplay?
6
+ self.@context: Context?
6
7
 
7
8
  # Type alias for Task class (singleton type with class methods)
8
9
  # This represents a Class that inherits from Taski::Task
@@ -11,9 +12,12 @@ module Taski
11
12
  # Module-level methods
12
13
  def self.global_registry: () -> Execution::Registry
13
14
  def self.reset_global_registry!: () -> void
14
- def self.progress_display: () -> Execution::ParallelProgressDisplay?
15
- def self.progress_enabled?: () -> bool
15
+ def self.progress_display: () -> Execution::TreeProgressDisplay?
16
+ def self.progress_disabled?: () -> bool
16
17
  def self.reset_progress_display!: () -> void
18
+ def self.context: () -> Hash[Symbol, untyped]
19
+ def self.start_context: (options: Hash[Symbol, untyped], root_task: Class) -> void
20
+ def self.reset_context!: () -> void
17
21
 
18
22
  # Custom exceptions
19
23
  class TaskAbortException < StandardError
@@ -29,23 +33,25 @@ module Taski
29
33
 
30
34
  # Context class for execution context management
31
35
  class Context
32
- self.@monitor: Monitor
33
- self.@working_directory: String?
34
- self.@started_at: Time?
35
- self.@root_task: Class?
36
-
37
- def self.working_directory: () -> String
38
- def self.started_at: () -> Time
39
- def self.root_task: () -> Class?
40
- def self.set_root_task: (Class task) -> void
41
- def self.reset!: () -> void
36
+ @options: Hash[Symbol | String, untyped]
37
+ @root_task: Class
38
+ @started_at: Time
39
+ @working_directory: String
40
+
41
+ attr_reader started_at: Time
42
+ attr_reader working_directory: String
43
+ attr_reader root_task: Class
44
+
45
+ def initialize: (options: Hash[Symbol | String, untyped], root_task: Class) -> void
46
+ def []: (Symbol | String key) -> untyped
47
+ def fetch: (Symbol | String key, ?untyped default) ?{ () -> untyped } -> untyped
48
+ def key?: (Symbol | String key) -> bool
42
49
  end
43
50
 
44
51
  # Main Task class
45
52
  class Task
46
53
  self.@exported_methods: Array[Symbol]
47
54
  self.@dependencies_cache: Set[task_class]?
48
- self.@coordinator: Execution::Coordinator?
49
55
  self.@circular_dependency_checked: bool
50
56
 
51
57
  # Class methods
@@ -54,10 +60,9 @@ module Taski
54
60
  def self.new: () -> Execution::TaskWrapper
55
61
  def self.cached_dependencies: () -> Set[task_class]
56
62
  def self.clear_dependency_cache: () -> void
57
- def self.run: () -> untyped
58
- def self.clean: () -> void
63
+ def self.run: (?context: Hash[Symbol, untyped]) -> untyped
64
+ def self.clean: (?context: Hash[Symbol, untyped]) -> void
59
65
  def self.registry: () -> Execution::Registry
60
- def self.coordinator: () -> Execution::Coordinator
61
66
  def self.reset!: () -> void
62
67
  def self.tree: () -> String
63
68
 
@@ -69,9 +74,6 @@ module Taski
69
74
  private
70
75
 
71
76
  # Private class methods
72
- def self.build_tree: (task_class task_class, String prefix, Set[task_class] visited) -> String
73
- def self.format_dependency_branch: (task_class dep, String prefix, bool is_last, Set[task_class] visited) -> String
74
- def self.tree_connector_chars: (bool is_last) -> [String, String]
75
77
  def self.cached_wrapper: () -> Execution::TaskWrapper
76
78
  def self.define_instance_reader: (Symbol method_name) -> void
77
79
  def self.define_class_accessor: (Symbol method_name) -> void
@@ -119,7 +121,8 @@ module Taski
119
121
 
120
122
  def initialize: () -> void
121
123
  def get_or_create: (Class task_class) { () -> TaskWrapper } -> TaskWrapper
122
- def get_task: (Class task_class) -> TaskWrapper
124
+ def get_task: (Class task_class) -> TaskWrapper?
125
+ def register: (Class task_class, TaskWrapper wrapper) -> void
123
126
  def register_thread: (Thread thread) -> void
124
127
  def wait_all: () -> void
125
128
  def reset!: () -> void
@@ -128,31 +131,52 @@ module Taski
128
131
  def run: (Class task_class, Array[Symbol] exported_methods) -> untyped
129
132
  end
130
133
 
131
- # Coordinator class for dependency management
132
- class Coordinator
133
- @registry: Registry
134
- @analyzer: singleton(StaticAnalysis::Analyzer)
134
+ # Executor class for parallel task execution (Producer-Consumer pattern)
135
+ class Executor
136
+ STATE_PENDING: Symbol
137
+ STATE_ENQUEUED: Symbol
138
+ STATE_COMPLETED: Symbol
135
139
 
136
- def initialize: (registry: Registry, analyzer: singleton(StaticAnalysis::Analyzer)) -> void
137
- def start_dependencies: (task_class task_class) -> void
138
- def start_clean_dependencies: (task_class task_class) -> void
139
- def get_dependencies: (task_class task_class) -> Set[task_class]
140
+ @registry: Registry
141
+ @worker_count: Integer
142
+ @execution_queue: Thread::Queue
143
+ @completion_queue: Thread::Queue
144
+ @workers: Array[Thread]
145
+ @dependencies: Hash[Class, Set[Class]]
146
+ @task_states: Hash[Class, Symbol]
147
+ @completed_tasks: Set[Class]
148
+
149
+ def self.execute: (Class root_task_class, registry: Registry) -> void
150
+ def initialize: (registry: Registry, ?worker_count: Integer?) -> void
151
+ def execute: (Class root_task_class) -> void
140
152
 
141
153
  private
142
154
 
143
- def start_thread_with: () { () -> void } -> void
144
- def start_dependency_execution: (task_class dep_class) -> void
145
- def start_dependency_clean: (task_class dep_class) -> void
155
+ def default_worker_count: () -> Integer
156
+ def build_dependency_graph: (Class root_task_class) -> void
157
+ def enqueue_ready_tasks: () -> void
158
+ def ready_to_execute?: (Class task_class) -> bool
159
+ def enqueue_task: (Class task_class) -> void
160
+ def get_or_create_wrapper: (Class task_class) -> TaskWrapper
161
+ def start_workers: () -> void
162
+ def worker_loop: () -> void
163
+ def execute_task: (Class task_class, TaskWrapper wrapper) -> void
164
+ def run_main_loop: (Class root_task_class) -> void
165
+ def no_running_tasks?: () -> bool
166
+ def handle_completion: (Hash[Symbol, untyped] event) -> void
167
+ def shutdown_workers: () -> void
168
+ def debug_log: (String message) -> void
146
169
  end
147
170
 
148
171
  # TaskWrapper class for task execution
149
172
  class TaskWrapper
150
173
  attr_reader task: Task
151
174
  attr_reader result: untyped
175
+ attr_reader error: StandardError?
176
+ attr_reader timing: TaskTiming?
152
177
 
153
178
  @task: Task
154
179
  @registry: Registry
155
- @coordinator: Coordinator
156
180
  @result: untyped
157
181
  @clean_result: untyped
158
182
  @error: StandardError?
@@ -167,38 +191,38 @@ module Taski
167
191
  STATE_RUNNING: Symbol
168
192
  STATE_COMPLETED: Symbol
169
193
 
170
- def initialize: (Task task, registry: Registry, coordinator: Coordinator) -> void
194
+ def initialize: (Task task, registry: Registry) -> void
195
+ def state: () -> Symbol
196
+ def pending?: () -> bool
197
+ def completed?: () -> bool
171
198
  def run: () -> untyped
172
- def clean: () -> void
199
+ def clean: () -> untyped
173
200
  def get_exported_value: (Symbol method_name) -> untyped
174
- def start_thread_with: () { () -> void } -> void
175
- def execute_with_state_pattern: (state_getter: ^() -> Symbol, starter: ^() -> void, waiter: ^() -> void, ?pre_start_check: (^() -> void)?) -> void
176
- def execute_task_if_needed: () -> void
177
- def start_async_execution: () -> void
178
- def execute_task: () -> void
179
- def wait_for_dependencies: () -> void
180
- def execute_clean_if_needed: () -> void
181
- def start_async_clean: () -> void
182
- def execute_clean: () -> void
183
- def wait_for_clean_dependencies: () -> void
184
- def mark_completed: () -> void
185
- def mark_clean_completed: () -> void
186
- def wait_for_completion: () -> untyped
201
+ def mark_running: () -> bool
202
+ def mark_completed: (untyped result) -> void
203
+ def mark_failed: (StandardError error) -> void
204
+ def mark_clean_completed: (untyped result) -> void
205
+ def wait_for_completion: () -> void
187
206
  def wait_for_clean_completion: () -> void
188
- def debug_log: (String message) -> void
189
- def log_start: () -> void
190
- def log_completion: () -> void
191
- def log_clean_start: () -> void
192
- def log_clean_completion: () -> void
193
- def register_with_progress_display: () -> void
194
- def update_progress: (Symbol state, ?duration: Numeric?, ?error: Exception?) -> void
195
207
  def method_missing: (Symbol name, *untyped args) ?{ (*untyped) -> untyped } -> untyped
196
208
  def respond_to_missing?: (Symbol name, ?bool include_private) -> bool
209
+
210
+ private
211
+
212
+ def trigger_execution_and_wait: () -> void
213
+ def trigger_clean_and_wait: () -> void
214
+ def execute_clean: () -> void
215
+ def wait_for_clean_dependencies: () -> void
216
+ def check_abort!: () -> void
217
+ def update_progress: (Symbol state, ?duration: Numeric?, ?error: Exception?) -> void
218
+ def debug_log: (String message) -> void
197
219
  end
198
220
 
199
- # ParallelProgressDisplay class for progress visualization
200
- class ParallelProgressDisplay
221
+ # TreeProgressDisplay class for tree-based progress visualization
222
+ class TreeProgressDisplay
201
223
  SPINNER_FRAMES: Array[String]
224
+ COLORS: Hash[Symbol, String]
225
+ ICONS: Hash[Symbol, String]
202
226
 
203
227
  @output: IO
204
228
  @tasks: Hash[Class, TaskProgress]
@@ -206,6 +230,30 @@ module Taski
206
230
  @spinner_index: Integer
207
231
  @renderer_thread: Thread?
208
232
  @running: bool
233
+ @nest_level: Integer
234
+ @root_task_class: Class?
235
+ @tree_structure: Hash[Symbol, untyped]?
236
+ @section_impl_map: Hash[Class, Class]
237
+ @last_line_count: Integer
238
+
239
+ # Shared class methods
240
+ def self.section_class?: (Class klass) -> bool
241
+ def self.nested_class?: (Class child_class, Class parent_class) -> bool
242
+ def self.render_static_tree: (Class root_task_class) -> String
243
+
244
+ # Static tree renderer (internal class)
245
+ class StaticTreeRenderer
246
+ @task_index_map: Hash[Class, Integer]
247
+
248
+ def render: (Class root_task_class) -> String
249
+
250
+ private
251
+
252
+ def build_tree: (Class task_class, String prefix, bool is_impl, Set[Class] ancestors) -> String
253
+ def render_dependency_branch: (Class dep, String prefix, bool is_last, bool is_impl, Set[Class] ancestors) -> String
254
+ def get_task_number: (Class task_class) -> String
255
+ def colored_type_label: (Class klass) -> String
256
+ end
209
257
 
210
258
  class TaskProgress
211
259
  attr_accessor state: Symbol
@@ -213,28 +261,39 @@ module Taski
213
261
  attr_accessor end_time: Time?
214
262
  attr_accessor error: Exception?
215
263
  attr_accessor duration: Numeric?
264
+ attr_accessor is_impl_candidate: bool
216
265
 
217
266
  def initialize: () -> void
218
267
  end
219
268
 
220
269
  def initialize: (?output: IO) -> void
270
+ def set_root_task: (Class root_task_class) -> void
271
+ def register_section_impl: (Class section_class, Class impl_class) -> void
221
272
  def register_task: (Class task_class) -> void
222
273
  def task_registered?: (Class task_class) -> bool
223
274
  def update_task: (Class task_class, state: Symbol, ?duration: Numeric?, ?error: Exception?) -> void
224
275
  def task_state: (Class task_class) -> Symbol?
225
- def render: () -> void
226
276
  def start: () -> void
227
277
  def stop: () -> void
228
278
 
229
279
  private
230
280
 
231
- def collect_task_lines: () -> Array[String]
281
+ def build_tree_structure: () -> void
282
+ def build_tree_node: (Class task_class, Set[Class] ancestors) -> Hash[Symbol, untyped]?
283
+ def register_tasks_from_tree: (Hash[Symbol, untyped] node) -> void
232
284
  def render_live: () -> void
233
285
  def render_final: () -> void
234
- def format_task_line: (Class task_class, TaskProgress progress) -> String
235
- def task_icon: (Symbol state) -> String
286
+ def build_tree_display: () -> Array[String]
287
+ def build_root_tree_lines: (Hash[Symbol, untyped] node, String prefix, Array[String] lines) -> void
288
+ def render_children: (Hash[Symbol, untyped] node, String prefix, Array[String] lines, Class parent_task_class, bool ancestor_selected) -> void
289
+ def format_tree_line: (Class task_class, TaskProgress? progress, bool is_impl, bool is_selected) -> String
290
+ def format_unknown_task: (Class task_class, ?bool is_selected) -> String
291
+ def task_status_icon: (Symbol state, bool is_selected) -> String
236
292
  def spinner_char: () -> String
293
+ def type_label_for: (Class task_class, ?bool is_selected) -> String
237
294
  def task_details: (TaskProgress progress) -> String
295
+ def section_class?: (Class klass) -> bool
296
+ def nested_class?: (Class child_class, Class parent_class) -> bool
238
297
  end
239
298
  end
240
299
 
@@ -246,19 +305,21 @@ module Taski
246
305
 
247
306
  private
248
307
 
249
- def self.extract_run_method_location: (Class task_class) -> [String, Integer]?
308
+ def self.target_method_for: (Class task_class) -> Symbol
309
+ def self.extract_method_location: (Class task_class, Symbol method_name) -> [String, Integer]?
250
310
  end
251
311
 
252
312
  # Visitor class for AST traversal
253
313
  class Visitor < Prism::Visitor
254
314
  @target_task_class: Class
315
+ @target_method: Symbol
255
316
  @dependencies: Set[task_class]
256
- @in_target_run_method: bool
317
+ @in_target_method: bool
257
318
  @current_namespace_path: Array[String?]
258
319
 
259
320
  attr_reader dependencies: Set[task_class]
260
321
 
261
- def initialize: (Class task_class) -> void
322
+ def initialize: (Class task_class, Symbol target_method) -> void
262
323
  def visit_class_node: (Prism::ClassNode node) -> untyped
263
324
  def visit_module_node: (Prism::ModuleNode node) -> untyped
264
325
  def visit_def_node: (Prism::DefNode node) -> untyped
@@ -268,8 +329,10 @@ module Taski
268
329
 
269
330
  def within_namespace: (String? name) { () -> void } -> void
270
331
  def in_target_class?: () -> bool
332
+ def in_impl_method?: () -> bool
271
333
  def extract_constant_name: (untyped node) -> String?
272
334
  def detect_task_dependency: (Prism::CallNode node) -> void
335
+ def detect_impl_candidate: (untyped node) -> void
273
336
  def extract_receiver_constant: (untyped receiver) -> String?
274
337
  def resolve_and_add_dependency: (String constant_name) -> void
275
338
  def resolve_constant: (String name) -> task_class?