Class: Agentilda::Linear::Issues

Inherits:
Object
  • Object
show all
Defined in:
lib/agentilda/linear/issue.rb

Overview

A plan's record of what it has put into Linear.

Without it, a second linear import cannot tell an issue it created last week from one it has never created, and the only two outcomes are duplicating everything or searching Linear by title on every run. The file is the memory, and it is a committed artifact rather than a dotfile so the record travels with the plan and shows up in review.

The Synced column is what makes an update cheap to decide: it is a digest of the payload last pushed, so a run can tell — with no network at all — whether anything about this issue has changed since. A digest that no longer matches is the whole trigger for an update.

Constant Summary collapse

FILENAME =

The file this reads and writes.

"linear.md"
PARENT =

The unit key given to the issue that stands for the whole folder. Its children carry their own unit keys; it carries this, so one table can hold a plan and its parts without a second shape to parse.

"PLAN"

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(dir:) ⇒ Issues

Returns a new instance of Issues.

Parameters:

  • dir (String)

    absolute path to the plan folder



103
104
105
# File 'lib/agentilda/linear/issue.rb', line 103

def initialize(dir:)
  @dir = dir
end

Class Method Details

.digest(payload) ⇒ String

Fingerprint of a payload, short enough to read in a table.

Parameters:

  • payload (Object)

    anything with a stable #inspect ordering

Returns:

  • (String)

    eight hex characters



48
# File 'lib/agentilda/linear/issue.rb', line 48

def self.digest(payload) = ::Digest::SHA256.hexdigest(JSON.generate(payload))[0, 8]

.escape(text) ⇒ String

Parameters:

  • text (String)

Returns:

  • (String)


100
# File 'lib/agentilda/linear/issue.rb', line 100

def self.escape(text) = text.to_s.gsub("|", "\\|").gsub(/\s+/, " ").strip

.heading(team, project) ⇒ String

Parameters:

  • team (String)
  • project (Hash, nil)

Returns:

  • (String)


87
88
89
90
91
# File 'lib/agentilda/linear/issue.rb', line 87

def self.heading(team, project)
  return "Team **#{team}**." unless project

  "Team **#{team}** · project #{link(project[:name], project[:url])}"
end

Parameters:

  • text (String)
  • url (String, nil)

Returns:

  • (String)


96
# File 'lib/agentilda/linear/issue.rb', line 96

def self.link(text, url) = url ? "[#{escape(text)}](#{url})" : escape(text)

.order(issues) ⇒ Array<Agentilda::Linear::Issue>

The folder's own issue first, then its children. A table that opens with a child reads as a list of unrelated issues.

Parameters:

Returns:



79
80
81
82
# File 'lib/agentilda/linear/issue.rb', line 79

def self.order(issues)
  parent, children = issues.partition { |i| i.unit == PARENT }
  parent + children
end

.render(team:, project:, issues:) ⇒ String

Parameters:

  • team (String)

    the team key, e.g. "TAX"

  • project (Hash, nil)

    {name:, url:}

  • issues (Array<Agentilda::Linear::Issue>)

Returns:

  • (String)


54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# File 'lib/agentilda/linear/issue.rb', line 54

def self.render(team:, project:, issues:)
  rows = order(issues).map { |i|
    "| #{i.unit} | #{link(i.identifier, i.url)} | #{escape(i.title)} | #{i.state} | #{i.digest} |"
  }

  <<~MARKDOWN
    # Linear

    #{heading(team, project)}

    | Unit | Issue | Title | State | Synced |
    | :--- | :---- | :---- | ----: | :----- |
    #{rows.join("\n")}

    <!-- Written by `agentilda linear import --commit`. Do not hand-edit the
         Synced column: it fingerprints what was last pushed, and a row whose
         fingerprint no longer matches the plan is what triggers an update. -->
  MARKDOWN
end

Instance Method Details

#allArray<Agentilda::Linear::Issue>

Returns possibly empty.

Returns:



108
# File 'lib/agentilda/linear/issue.rb', line 108

def all = @all ||= parse

#by_unitHash{String => Agentilda::Linear::Issue}

Returns keyed by unit.

Returns:



111
# File 'lib/agentilda/linear/issue.rb', line 111

def by_unit = @by_unit ||= all.to_h { |issue| [issue.unit, issue] }

#exist?Boolean

Returns:

  • (Boolean)


127
# File 'lib/agentilda/linear/issue.rb', line 127

def exist? = File.file?(path)

#parentAgentilda::Linear::Issue?

The issue standing for the folder itself.

Returns:



121
# File 'lib/agentilda/linear/issue.rb', line 121

def parent = by_unit[PARENT]

#pathString

Returns absolute path to linear.md.

Returns:

  • (String)

    absolute path to linear.md



124
# File 'lib/agentilda/linear/issue.rb', line 124

def path = File.join(dir, FILENAME)

#projectHash?

The project this plan was last filed under, if any.

Returns:

  • (Hash, nil)

    {name:, url:}



116
# File 'lib/agentilda/linear/issue.rb', line 116

def project = (@project ||= [parse_project])[0]

#write(team:, project:, issues:) ⇒ String

Returns the path written.

Parameters:

Returns:

  • (String)

    the path written



133
134
135
136
137
# File 'lib/agentilda/linear/issue.rb', line 133

def write(team:, project:, issues:)
  File.write(path, self.class.render(team:, project:, issues:))
  @all = @by_unit = @project = nil
  path
end