Module: Brainiac::Plugins::Basecamp::Epic

Defined in:
lib/brainiac/plugins/basecamp/epic.rb

Overview

Parses Basecamp todolist-based epics.

Epic structure (Option C):

Todolist: "Epic: Build Auth System"
Todo: "#1234 — Set up auth models"
  Description: <a href="https://app.fizzy.do/org/cards/1234">Fizzy #1234</a>
               [depends:none] or [depends:1234,1235]
Todo: "#1235 — Add API endpoints"
  Description: ...

Each todo in the list = one work item linked to a Fizzy card. Dependencies are declared in the todo description or title.

Defined Under Namespace

Classes: Task

Class Method Summary collapse

Class Method Details

.build_todo_description(fizzy_card:, fizzy_account_id:, depends_on: [], agent: nil) ⇒ String

Generate a rich text HTML description for a todo linked to a Fizzy card.

Parameters:

  • fizzy_card (Integer)

    Fizzy card number

  • fizzy_account_id (String)

    Fizzy account ID (for URL)

  • depends_on (Array<Integer>) (defaults to: [])

    Card numbers this task depends on

  • agent (String, nil) (defaults to: nil)

    Agent name assigned to this task

Returns:

  • (String)

    HTML description



109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
# File 'lib/brainiac/plugins/basecamp/epic.rb', line 109

def build_todo_description(fizzy_card:, fizzy_account_id:, depends_on: [], agent: nil)
  lines = []
  lines << "<div>"
  lines << "<strong>Fizzy:</strong> <a href=\"https://app.fizzy.do/#{}/cards/#{fizzy_card}\">##{fizzy_card}</a><br>"

  if depends_on.any?
    dep_links = depends_on.map { |d| "<a href=\"https://app.fizzy.do/#{}/cards/#{d}\">##{d}</a>" }
    lines << "<strong>Depends on:</strong> #{dep_links.join(', ')}<br>"
  else
    lines << "<strong>Depends on:</strong> none<br>"
  end

  lines << "<strong>Agent:</strong> #{agent}<br>" if agent
  lines << "</div>"
  lines.join("\n")
end

.dependency_graph(tasks) ⇒ Hash

Build a full dependency graph from tasks.

Parameters:

  • tasks (Array<Task>)

    All tasks

Returns:

  • (Hash)

    Graph structure for visualization/debugging



75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
# File 'lib/brainiac/plugins/basecamp/epic.rb', line 75

def dependency_graph(tasks)
  completed_cards = tasks.select { |t| t.status == :complete }.map(&:fizzy_card).compact

  {
    total: tasks.size,
    complete: tasks.count { |t| t.status == :complete },
    pending: tasks.count { |t| t.status == :pending },
    in_flight: tasks.count { |t| t.status == :in_flight },
    blocked: tasks.count do |t|
      t.status == :pending &&
        t.depends_on.any? { |dep| !completed_cards.include?(dep) }
    end,
    unblocked: unblocked_tasks(tasks).size,
    tasks: tasks.map do |t|
      {
        todo_id: t.todo_id,
        fizzy_card: t.fizzy_card,
        title: t.title,
        status: t.status,
        depends_on: t.depends_on,
        assignees: t.assignees,
        due_on: t.due_on
      }
    end
  }
end

.extract_deploy_env(title, description = nil) ⇒ String?

Extract deploy environment from epic title or description. Supports:

[deploy:dev02]  — in title (preferred)
deploy:dev02    — in description (fallback)

Parameters:

  • title (String)

    Epic/todolist title

  • description (String, nil) (defaults to: nil)

    Optional description to check as fallback

Returns:

  • (String, nil)

    Environment name or nil



134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
# File 'lib/brainiac/plugins/basecamp/epic.rb', line 134

def extract_deploy_env(title, description = nil)
  # Try [deploy:env] format in title (preferred)
  if title
    marker = "[deploy:"
    value_start = title.index(marker)
    if value_start
      value_start += marker.length
      value_end = title.index("]", value_start)
      if value_end
        environment = title[value_start...value_end].strip
        return environment unless environment.empty?
      end
    end
  end

  # Fallback: try deploy:env in description
  if description && (match = description.match(/deploy:(\S+)/i))
    return match[1].strip
  end

  nil
end

.parse_todos(todos) ⇒ Array<Task>

Parse todos from a todolist into structured tasks with dependency graph.

Parameters:

  • todos (Array<Hash>)

    Raw todo data from Basecamp API

Returns:

  • (Array<Task>)

    Parsed tasks with card refs and dependencies



30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# File 'lib/brainiac/plugins/basecamp/epic.rb', line 30

def parse_todos(todos)
  todos.map do |todo|
    title = todo["title"] || todo["content"] || ""
    description = todo["description"] || ""
    todo_id = todo["id"]
    completed = todo["completed"] || false
    assignees = (todo["assignees"] || []).map { |a| a["name"] || a["id"].to_s }
    due_on = todo["due_on"]

    fizzy_card = extract_fizzy_card(title) || extract_fizzy_card_from_description(description)
    depends_on = extract_dependencies(title)
    depends_on = extract_dependencies(description) if depends_on.empty?

    Task.new(
      todo_id: todo_id,
      title: title,
      fizzy_card: fizzy_card,
      depends_on: depends_on,
      status: completed ? :complete : :pending,
      completed: completed,
      description: description,
      assignees: assignees,
      due_on: due_on
    )
  end
end

.unblocked_tasks(tasks) ⇒ Array<Task>

Determine which tasks are unblocked (all dependencies satisfied).

Parameters:

  • tasks (Array<Task>)

    All tasks in the epic

Returns:

  • (Array<Task>)

    Tasks ready to be worked on



61
62
63
64
65
66
67
68
69
# File 'lib/brainiac/plugins/basecamp/epic.rb', line 61

def unblocked_tasks(tasks)
  completed_cards = tasks.select { |t| t.status == :complete }.map(&:fizzy_card).compact

  tasks.select do |task|
    task.status == :pending &&
      task.fizzy_card &&
      task.depends_on.all? { |dep| completed_cards.include?(dep) }
  end
end