Module: Brainiac::Plugins::Basecamp::TaskState

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

Overview

The single authority for an epic task's lifecycle. Task hashes remain serializable so existing persisted epics continue to work unchanged.

Defined Under Namespace

Classes: InvalidTransition

Constant Summary collapse

STATES =
%w[pending in_flight in_review final_decision complete merge_failed].freeze
TRANSITIONS =
{
  dispatch: { "pending" => "in_flight" },
  submit_for_review: { "pending" => "in_review", "in_flight" => "in_review" },
  request_changes: { "in_review" => "in_flight" },
  approve: { "in_review" => "final_decision" },
  complete: {
    "pending" => "complete",
    "in_flight" => "complete",
    "in_review" => "complete",
    "final_decision" => "complete"
  },
  merge_failed: { "final_decision" => "merge_failed" },
  retry_merge: { "merge_failed" => "final_decision" }
}.freeze

Class Method Summary collapse

Class Method Details

.in?(task, *states) ⇒ Boolean

Returns:

  • (Boolean)


34
35
36
# File 'lib/brainiac/plugins/basecamp/task_state.rb', line 34

def in?(task, *states)
  states.flatten.map(&:to_s).include?(state(task))
end

.migrate!(task, triggered_by: "state_machine_migration", at: Time.now) ⇒ Object

Adds a history entry for state created before this state machine existed, without changing its current status.

Raises:



68
69
70
71
72
73
74
75
76
77
78
79
80
81
# File 'lib/brainiac/plugins/basecamp/task_state.rb', line 68

def migrate!(task, triggered_by: "state_machine_migration", at: Time.now)
  return task if task.key?("transitions")

  current = state(task)
  raise InvalidTransition, "unknown task state #{current}" unless STATES.include?(current)

  task["transitions"] = [{
    "from_state" => nil,
    "to_state" => current,
    "triggered_by" => triggered_by,
    "timestamp" => at.iso8601
  }]
  task
end

.state(task) ⇒ Object



30
31
32
# File 'lib/brainiac/plugins/basecamp/task_state.rb', line 30

def state(task)
  (task["status"] || "pending").to_s
end

.transition!(task, event, triggered_by:, at: Time.now, guard: true) ⇒ Object

Raises:



38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
# File 'lib/brainiac/plugins/basecamp/task_state.rb', line 38

def transition!(task, event, triggered_by:, at: Time.now, guard: true)
  event = event.to_sym
  from_state = state(task)
  transitions = TRANSITIONS.fetch(event, {})
  # Idempotent no-op: if the task is already in the target state for this
  # event, silently return. This is intentional for recovery paths where a
  # task may be re-processed after a restart — the audit trail already
  # captured the original transition, so logging again would be noise.
  return task if transitions.value?(from_state)

  to_state = transitions[from_state]

  raise InvalidTransition, "cannot #{event} task from #{from_state}" unless to_state
  raise InvalidTransition, "guard failed for #{event} from #{from_state}" unless guard

  migrate!(task, at: at)
  timestamp = at.iso8601
  task["status"] = to_state
  task["transitions"] ||= []
  task["transitions"] << {
    "from_state" => from_state,
    "to_state" => to_state,
    "triggered_by" => triggered_by.to_s,
    "timestamp" => timestamp
  }
  task
end