Class: TrakFlow::Graph::DependencyGraph
- Inherits:
-
Object
- Object
- TrakFlow::Graph::DependencyGraph
- Defined in:
- lib/trak_flow/graph/dependency_graph.rb
Overview
Dependency graph operations for visualizing and analyzing task relationships
Constant Summary collapse
- COLORS =
Graph visualization colors (dark theme compatible)
{ status: { closed: "#4a5568", tombstone: "#4a5568", in_progress: "#3182ce", blocked: "#e53e3e", deferred: "#d69e2e", pinned: "#805ad5" }, priority: { critical: "#e53e3e", high: "#ed8936", medium: "#48bb78", low: "#4299e1", backlog: "#a0aec0" }, edge: { blocks: "#e53e3e", parent_child: "#3182ce", related: "#a0aec0", discovered_from: "#805ad5" } }.freeze
Instance Method Summary collapse
-
#all_blocked(task_id) ⇒ Object
Find all tasks blocked by the given task (directly or transitively).
-
#all_blockers(task_id) ⇒ Object
Find all tasks that block the given task (directly or transitively).
-
#analyze ⇒ Object
Analyze the graph for potential problems.
-
#critical_path(root_task_id) ⇒ Object
Find the critical path - longest chain of blocking dependencies.
-
#dependency_tree(task_id, direction: :blocking, max_depth: 10) ⇒ Object
Build a tree representation of dependencies for a task.
-
#initialize(db) ⇒ DependencyGraph
constructor
A new instance of DependencyGraph.
-
#leaf_tasks ⇒ Object
Get all leaf tasks (tasks with no children/blocked tasks).
-
#root_tasks ⇒ Object
Get all root tasks (tasks with no parents/blockers).
-
#to_dot(task_ids: nil, include_closed: false) ⇒ Object
Generate a DOT representation for Graphviz.
-
#to_svg(task_ids: nil, include_closed: false) ⇒ Object
Generate SVG using Graphviz (if available).
Constructor Details
#initialize(db) ⇒ DependencyGraph
Returns a new instance of DependencyGraph.
32 33 34 |
# File 'lib/trak_flow/graph/dependency_graph.rb', line 32 def initialize(db) @db = db end |
Instance Method Details
#all_blocked(task_id) ⇒ Object
Find all tasks blocked by the given task (directly or transitively)
48 49 50 |
# File 'lib/trak_flow/graph/dependency_graph.rb', line 48 def all_blocked(task_id) (task_id, :outgoing, Models::Dependency::BLOCKING_TYPES) end |
#all_blockers(task_id) ⇒ Object
Find all tasks that block the given task (directly or transitively)
43 44 45 |
# File 'lib/trak_flow/graph/dependency_graph.rb', line 43 def all_blockers(task_id) (task_id, :incoming, Models::Dependency::BLOCKING_TYPES) end |
#analyze ⇒ Object
Analyze the graph for potential problems
134 135 136 137 138 139 140 141 142 143 |
# File 'lib/trak_flow/graph/dependency_graph.rb', line 134 def analyze { total_tasks: @db.all_task_ids.size, open_tasks: @db.list_tasks(status: "open").size, ready_tasks: @db.ready_tasks.size, blocked_tasks: @db.blocked_tasks.size, orphan_tasks: find_orphans.size, potential_cycles: find_potential_bottlenecks } end |
#critical_path(root_task_id) ⇒ Object
Find the critical path - longest chain of blocking dependencies
53 54 55 56 |
# File 'lib/trak_flow/graph/dependency_graph.rb', line 53 def critical_path(root_task_id) visited = {} find_longest_path(root_task_id, visited) end |
#dependency_tree(task_id, direction: :blocking, max_depth: 10) ⇒ Object
Build a tree representation of dependencies for a task
37 38 39 40 |
# File 'lib/trak_flow/graph/dependency_graph.rb', line 37 def dependency_tree(task_id, direction: :blocking, max_depth: 10) task = @db.find_task!(task_id) build_tree_node(task, direction, max_depth, Set.new) end |
#leaf_tasks ⇒ Object
Get all leaf tasks (tasks with no children/blocked tasks)
59 60 61 62 63 64 65 66 67 68 69 |
# File 'lib/trak_flow/graph/dependency_graph.rb', line 59 def leaf_tasks all_targets = Set.new @db.all_task_ids.each do |id| @db.find_dependencies(id, direction: :outgoing).each do |dep| all_targets << dep.target_id if dep.blocking? end end @db.list_tasks.reject { |task| all_targets.include?(task.id) } end |
#root_tasks ⇒ Object
Get all root tasks (tasks with no parents/blockers)
72 73 74 75 76 77 78 79 80 81 82 |
# File 'lib/trak_flow/graph/dependency_graph.rb', line 72 def root_tasks all_sources = Set.new @db.all_task_ids.each do |id| @db.find_dependencies(id, direction: :incoming).each do |dep| all_sources << dep.source_id if dep.blocking? end end @db.list_tasks.reject { |task| all_sources.include?(task.id) } end |
#to_dot(task_ids: nil, include_closed: false) ⇒ Object
Generate a DOT representation for Graphviz
85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 |
# File 'lib/trak_flow/graph/dependency_graph.rb', line 85 def to_dot(task_ids: nil, include_closed: false) task_ids ||= @db.all_task_ids tasks = task_ids.map { |id| @db.find_task(id) }.compact tasks = tasks.reject(&:closed?) unless include_closed lines = ["digraph dependencies {"] lines << ' rankdir=TB;' lines << ' node [shape=box, style=filled];' lines << "" tasks.each do |task| color = node_color(task) label = "#{task.id}\\n#{truncate(task.title, 30)}" lines << " \"#{task.id}\" [label=\"#{label}\", fillcolor=\"#{color}\"];" end lines << "" task_set = Set.new(tasks.map(&:id)) tasks.each do |task| @db.find_dependencies(task.id, direction: :outgoing).each do |dep| next unless task_set.include?(dep.target_id) style = edge_style(dep) lines << " \"#{dep.source_id}\" -> \"#{dep.target_id}\" [#{style}];" end end lines << "}" lines.join("\n") end |
#to_svg(task_ids: nil, include_closed: false) ⇒ Object
Generate SVG using Graphviz (if available)
119 120 121 122 123 124 125 126 127 128 129 130 131 |
# File 'lib/trak_flow/graph/dependency_graph.rb', line 119 def to_svg(task_ids: nil, include_closed: false) dot = to_dot(task_ids: task_ids, include_closed: include_closed) require "open3" stdout, stderr, status = Open3.capture3("dot", "-Tsvg", stdin_data: dot) unless status.success? raise Error, "Graphviz error: #{stderr}" end # Make background transparent for dark theme compatibility stdout.gsub(/fill="white"/, 'fill="none"') end |