Class: TrakFlow::Graph::DependencyGraph

Inherits:
Object
  • Object
show all
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

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)
  collect_related_tasks(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)
  collect_related_tasks(task_id, :incoming, Models::Dependency::BLOCKING_TYPES)
end

#analyzeObject

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_tasksObject

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_tasksObject

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