jirametrics 2.11 → 2.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. checksums.yaml +4 -4
  2. data/bin/jirametrics-mcp +5 -0
  3. data/lib/jirametrics/aggregate_config.rb +10 -2
  4. data/lib/jirametrics/aging_work_bar_chart.rb +191 -133
  5. data/lib/jirametrics/aging_work_in_progress_chart.rb +43 -11
  6. data/lib/jirametrics/aging_work_table.rb +14 -17
  7. data/lib/jirametrics/anonymizer.rb +81 -6
  8. data/lib/jirametrics/atlassian_document_format.rb +160 -0
  9. data/lib/jirametrics/bar_chart_range.rb +17 -0
  10. data/lib/jirametrics/blocked_stalled_change.rb +5 -3
  11. data/lib/jirametrics/board.rb +34 -11
  12. data/lib/jirametrics/board_config.rb +5 -1
  13. data/lib/jirametrics/board_feature.rb +14 -0
  14. data/lib/jirametrics/board_movement_calculator.rb +10 -2
  15. data/lib/jirametrics/cfd_data_builder.rb +108 -0
  16. data/lib/jirametrics/change_item.rb +43 -20
  17. data/lib/jirametrics/chart_base.rb +143 -6
  18. data/lib/jirametrics/css_variable.rb +1 -1
  19. data/lib/jirametrics/cumulative_flow_diagram.rb +208 -0
  20. data/lib/jirametrics/{cycletime_config.rb → cycle_time_config.rb} +22 -5
  21. data/lib/jirametrics/cycletime_histogram.rb +15 -101
  22. data/lib/jirametrics/cycletime_scatterplot.rb +17 -83
  23. data/lib/jirametrics/daily_view.rb +306 -0
  24. data/lib/jirametrics/daily_wip_by_age_chart.rb +4 -5
  25. data/lib/jirametrics/daily_wip_by_blocked_stalled_chart.rb +14 -4
  26. data/lib/jirametrics/daily_wip_by_parent_chart.rb +4 -2
  27. data/lib/jirametrics/daily_wip_chart.rb +30 -8
  28. data/lib/jirametrics/data_quality_report.rb +43 -12
  29. data/lib/jirametrics/dependency_chart.rb +6 -3
  30. data/lib/jirametrics/download_config.rb +15 -0
  31. data/lib/jirametrics/downloader.rb +128 -71
  32. data/lib/jirametrics/downloader_for_cloud.rb +287 -0
  33. data/lib/jirametrics/downloader_for_data_center.rb +95 -0
  34. data/lib/jirametrics/estimate_accuracy_chart.rb +74 -12
  35. data/lib/jirametrics/estimation_configuration.rb +25 -0
  36. data/lib/jirametrics/examples/aggregated_project.rb +2 -2
  37. data/lib/jirametrics/examples/standard_project.rb +42 -27
  38. data/lib/jirametrics/expedited_chart.rb +3 -1
  39. data/lib/jirametrics/exporter.rb +26 -6
  40. data/lib/jirametrics/file_config.rb +10 -12
  41. data/lib/jirametrics/file_system.rb +59 -3
  42. data/lib/jirametrics/fix_version.rb +13 -0
  43. data/lib/jirametrics/flow_efficiency_scatterplot.rb +5 -1
  44. data/lib/jirametrics/github_gateway.rb +115 -0
  45. data/lib/jirametrics/groupable_issue_chart.rb +11 -1
  46. data/lib/jirametrics/grouping_rules.rb +26 -4
  47. data/lib/jirametrics/html/aging_work_bar_chart.erb +5 -5
  48. data/lib/jirametrics/html/aging_work_in_progress_chart.erb +3 -1
  49. data/lib/jirametrics/html/aging_work_table.erb +7 -0
  50. data/lib/jirametrics/html/collapsible_issues_panel.erb +2 -2
  51. data/lib/jirametrics/html/cumulative_flow_diagram.erb +503 -0
  52. data/lib/jirametrics/html/daily_wip_chart.erb +40 -5
  53. data/lib/jirametrics/html/estimate_accuracy_chart.erb +4 -12
  54. data/lib/jirametrics/html/expedited_chart.erb +6 -14
  55. data/lib/jirametrics/html/flow_efficiency_scatterplot.erb +4 -8
  56. data/lib/jirametrics/html/index.css +320 -69
  57. data/lib/jirametrics/html/index.erb +11 -20
  58. data/lib/jirametrics/html/index.js +164 -0
  59. data/lib/jirametrics/html/legacy_colors.css +174 -0
  60. data/lib/jirametrics/html/sprint_burndown.erb +17 -15
  61. data/lib/jirametrics/html/throughput_chart.erb +42 -11
  62. data/lib/jirametrics/html/{cycletime_histogram.erb → time_based_histogram.erb} +61 -59
  63. data/lib/jirametrics/html/{cycletime_scatterplot.erb → time_based_scatterplot.erb} +15 -11
  64. data/lib/jirametrics/html/wip_by_column_chart.erb +250 -0
  65. data/lib/jirametrics/html_generator.rb +32 -0
  66. data/lib/jirametrics/html_report_config.rb +52 -55
  67. data/lib/jirametrics/issue.rb +329 -106
  68. data/lib/jirametrics/issue_collection.rb +33 -0
  69. data/lib/jirametrics/issue_printer.rb +97 -0
  70. data/lib/jirametrics/jira_gateway.rb +81 -14
  71. data/lib/jirametrics/mcp_server.rb +531 -0
  72. data/lib/jirametrics/project_config.rb +151 -18
  73. data/lib/jirametrics/pull_request.rb +30 -0
  74. data/lib/jirametrics/pull_request_cycle_time_histogram.rb +77 -0
  75. data/lib/jirametrics/pull_request_cycle_time_scatterplot.rb +88 -0
  76. data/lib/jirametrics/pull_request_review.rb +13 -0
  77. data/lib/jirametrics/raw_javascript.rb +17 -0
  78. data/lib/jirametrics/settings.json +6 -1
  79. data/lib/jirametrics/sprint.rb +13 -0
  80. data/lib/jirametrics/sprint_burndown.rb +45 -37
  81. data/lib/jirametrics/sprint_issue_change_data.rb +3 -3
  82. data/lib/jirametrics/status.rb +1 -1
  83. data/lib/jirametrics/status_collection.rb +7 -0
  84. data/lib/jirametrics/stitcher.rb +81 -0
  85. data/lib/jirametrics/throughput_by_completed_resolution_chart.rb +22 -0
  86. data/lib/jirametrics/throughput_chart.rb +73 -23
  87. data/lib/jirametrics/time_based_histogram.rb +139 -0
  88. data/lib/jirametrics/time_based_scatterplot.rb +107 -0
  89. data/lib/jirametrics/user.rb +12 -0
  90. data/lib/jirametrics/wip_by_column_chart.rb +236 -0
  91. data/lib/jirametrics.rb +83 -64
  92. metadata +65 -6
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'jirametrics/groupable_issue_chart'
4
+
5
+ class TimeBasedScatterplot < ChartBase
6
+ include GroupableIssueChart
7
+
8
+ def initialize
9
+ super
10
+
11
+ @percentage_lines = []
12
+ @highest_y_value = 0
13
+ end
14
+
15
+ def run
16
+ items = all_items
17
+ data_sets = create_datasets items
18
+ overall_percent_line = calculate_percent_line(items)
19
+ @percentage_lines << [overall_percent_line, CssVariable['--cycletime-scatterplot-overall-trendline-color']]
20
+
21
+ return "<h1 class='foldable'>#{@header_text}</h1><div>No data matched the selected criteria. Nothing to show.</div>" if data_sets.empty?
22
+
23
+ wrap_and_render(binding, __FILE__)
24
+ end
25
+
26
+ def create_datasets items
27
+ data_sets = []
28
+
29
+ group_issues(items).each do |rules, items_by_type|
30
+ label = rules.label
31
+ color = rules.color
32
+ percent_line = calculate_percent_line items_by_type
33
+ data = items_by_type.filter_map { |item| data_for_item(item, rules: rules) }
34
+ data_sets << {
35
+ label: "#{label} (85% at #{label_days(percent_line)})",
36
+ data: data,
37
+ fill: false,
38
+ showLine: false,
39
+ backgroundColor: color
40
+ }
41
+
42
+ data_sets << trend_line_data_set(label: label, data: data, color: color)
43
+
44
+ @percentage_lines << [percent_line, color]
45
+ end
46
+ data_sets
47
+ end
48
+
49
+ def show_trend_lines
50
+ @show_trend_lines = true
51
+ end
52
+
53
+ def trend_line_data_set label:, data:, color:
54
+ points = data.collect do |hash|
55
+ [Time.parse(hash[:x]).to_i, hash[:y]]
56
+ end
57
+
58
+ # The trend calculation works with numbers only so convert Time to an int and back
59
+ calculator = TrendLineCalculator.new(points)
60
+ data_points = calculator.chart_datapoints(
61
+ range: time_range.begin.to_i..time_range.end.to_i,
62
+ max_y: @highest_y_value
63
+ )
64
+ data_points.each do |point_hash|
65
+ point_hash[:x] = chart_format Time.at(point_hash[:x])
66
+ end
67
+
68
+ {
69
+ type: 'line',
70
+ label: "#{label} Trendline",
71
+ data: data_points,
72
+ fill: false,
73
+ borderWidth: 1,
74
+ markerType: 'none',
75
+ borderColor: color,
76
+ borderDash: [6, 3],
77
+ pointStyle: 'dash',
78
+ hidden: !@show_trend_lines
79
+ }
80
+ end
81
+
82
+ def minimum_y_value
83
+ nil
84
+ end
85
+
86
+ def data_for_item item, rules: nil
87
+ y = y_value(item)
88
+ min = minimum_y_value
89
+ return nil if min && y < min
90
+
91
+ @highest_y_value = y if @highest_y_value < y
92
+
93
+ {
94
+ y: y,
95
+ x: chart_format(x_value(item)),
96
+ title: [title_value(item, rules: rules)]
97
+ }
98
+ end
99
+
100
+ def calculate_percent_line items
101
+ min = minimum_y_value
102
+ times = items.collect { |item| y_value(item) }
103
+ times.reject! { |y| min && y < min }
104
+ index = times.size * 85 / 100
105
+ times.sort[index]
106
+ end
107
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ class User
4
+ def initialize raw:
5
+ @raw = raw
6
+ end
7
+
8
+ def account_id = @raw['accountId']
9
+ def avatar_url = @raw['avatarUrls']['16x16']
10
+ def active? = @raw['active']
11
+ def display_name = @raw['displayName']
12
+ end
@@ -0,0 +1,236 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'jirametrics/chart_base'
4
+
5
+ class WipByColumnChart < ChartBase
6
+ attr_accessor :possible_statuses, :board_id
7
+
8
+ ColumnStats = Struct.new(:name, :min_wip_limit, :max_wip_limit, :wip_history, keyword_init: true)
9
+
10
+ def initialize block
11
+ super()
12
+ header_text 'WIP by column'
13
+ description_text <<-HTML
14
+ <p>
15
+ This chart shows how much time each board column has spent at different WIP (Work in Progress) levels.
16
+ </p>
17
+ <p>
18
+ Each row on the Y axis is a WIP level (the number of items in that column at the same time).
19
+ Each column on the X axis is a board column.
20
+ The horizontal bars show what percentage of the total time that column spent at that WIP level —
21
+ a wider bar means more time was spent there.
22
+ </p>
23
+ <p>
24
+ A column whose widest bar is at WIP&nbsp;1 was almost always working on one item at a time, often called
25
+ single-piece-flow. This team is likely collaborating very well and might have been
26
+ <a href="https://blog.mikebowler.ca/2021/06/19/pair-programming/">pairing</a> or
27
+ <a href="https://blog.mikebowler.ca/2023/04/22/ensemble-programming/">mobbing/ensembling</a>
28
+ and these teams tend to be very effective.
29
+ </p>
30
+ <p>
31
+ A column with wide bars at high WIP levels usually indicates a team that is highly siloed. Where each person
32
+ is working by themselves.
33
+ </p>
34
+ <p>
35
+ The dashed lines show the minimum and maximum WIP limits configured on the board.
36
+ If the widest bar sits well above the maximum limit, the limit may be set too low or not being respected.
37
+ If the widest bar sits below the minimum limit, consider whether that limit is still meaningful.
38
+ </p>
39
+ <p>
40
+ Hover over any bar to see the exact percentage.
41
+ </p>
42
+ <% if @all_boards[@board_id].team_managed_kanban? %>
43
+ <p>
44
+ If the data looks a bit off then that's probably because you're using a Team Managed project in "kanban mode".
45
+ For this specific case, we are unable to tell if an item is actually visible on the board and so we may
46
+ be reporting more items started than you actually see on the board. See
47
+ <a href="https://jirametrics.org/faq/#team-managed-kanban-backlog">the FAQ</a>.
48
+ </p>
49
+ <% end %>
50
+ HTML
51
+
52
+ instance_eval(&block)
53
+ end
54
+
55
+ def show_recommendations
56
+ @show_recommendations = true
57
+ end
58
+
59
+ def run
60
+ @header_text += " on board: #{current_board.name}"
61
+ stats = column_stats
62
+ @column_names = stats.collect(&:name)
63
+ @wip_data = stats.collect do |stat|
64
+ total = stat.wip_history.sum { |_wip, seconds| seconds }.to_f
65
+ next [] if total.zero?
66
+
67
+ stat.wip_history.collect { |wip, seconds| { 'wip' => wip, 'pct' => format_pct(seconds, total) } }
68
+ end
69
+ @max_wip = stats.flat_map { |s| s.wip_history.collect { |wip, _| wip } }.max || 0
70
+ @wip_limits = stats.collect { |s| { 'min' => s.min_wip_limit, 'max' => s.max_wip_limit } }
71
+ @recommendations = @show_recommendations ? compute_recommendations(stats) : Array.new(stats.size)
72
+
73
+ trim_zero_end_columns
74
+ @recommendation_texts = @show_recommendations ? build_recommendation_texts : []
75
+
76
+ wrap_and_render(binding, __FILE__)
77
+ end
78
+
79
+ def column_stats
80
+ board = current_board
81
+ columns = board.visible_columns
82
+ status_to_column = build_status_to_column_map(columns)
83
+ relevant_issues = @issues.select { |issue| issue.board.id == @board_id }
84
+
85
+ current_column = initial_column_state(relevant_issues, status_to_column)
86
+ events = events_within_range(relevant_issues, status_to_column)
87
+ column_wip_seconds = compute_wip_seconds(columns, current_column, events)
88
+
89
+ columns.collect.with_index do |column, index|
90
+ ColumnStats.new(
91
+ name: column.name,
92
+ min_wip_limit: column.min,
93
+ max_wip_limit: column.max,
94
+ wip_history: column_wip_seconds[index].sort.to_a
95
+ )
96
+ end
97
+ end
98
+
99
+ private
100
+
101
+ def trim_zero_end_columns
102
+ all_zero = @wip_data.map { |col| col.none? { |e| e['wip'].positive? } }
103
+ first = all_zero.index(false)
104
+ return unless first
105
+
106
+ last = all_zero.rindex(false)
107
+ @column_names = @column_names[first..last]
108
+ @wip_data = @wip_data[first..last]
109
+ @wip_limits = @wip_limits[first..last]
110
+ @recommendations = @recommendations[first..last]
111
+ @max_wip = @wip_data.flat_map { |col| col.map { |e| e['wip'] } }.max || 0
112
+ end
113
+
114
+ def compute_recommendations stats
115
+ stats.collect do |stat|
116
+ next nil if stat.wip_history.empty?
117
+
118
+ total = stat.wip_history.sum { |_wip, seconds| seconds }.to_f
119
+ next nil if total.zero?
120
+
121
+ cumulative = 0
122
+ stat.wip_history.sort.find do |_wip, seconds|
123
+ cumulative += seconds
124
+ cumulative / total >= 0.85
125
+ end&.first
126
+ end
127
+ end
128
+
129
+ def build_recommendation_texts
130
+ @column_names.each_with_index.filter_map do |name, i|
131
+ rec = @recommendations[i]
132
+ next if rec.nil?
133
+
134
+ next "Almost nothing passes through column '#{name}'. Do we still need it?" if rec.zero?
135
+
136
+ max = @wip_limits[i]['max']
137
+ if max.nil?
138
+ "Add a WIP limit to column '#{name}' — suggested maximum: #{rec}"
139
+ elsif rec < max
140
+ "Lower the WIP limit for '#{name}' from #{max} to #{rec}"
141
+ elsif rec > max
142
+ "Raise the WIP limit for '#{name}' from #{max} to #{rec}"
143
+ end
144
+ end
145
+ end
146
+
147
+ def format_pct seconds, total
148
+ raw = seconds / total * 100.0
149
+ (1..10).each do |decimals|
150
+ rounded = raw.round(decimals)
151
+ next if rounded.zero? && raw.positive?
152
+ next if rounded >= 100.0 && raw < 100.0
153
+
154
+ return rounded
155
+ end
156
+ raw
157
+ end
158
+
159
+ def build_status_to_column_map columns
160
+ columns.each_with_object({}).with_index do |(column, map), index|
161
+ column.status_ids.each { |id| map[id] = index }
162
+ end
163
+ end
164
+
165
+ def initial_column_state relevant_issues, status_to_column
166
+ relevant_issues.each_with_object({}) do |issue, hash|
167
+ started_time, stopped_time = issue.board.cycletime.started_stopped_times(issue)
168
+ in_wip = started_time &&
169
+ started_time <= time_range.begin &&
170
+ (stopped_time.nil? || stopped_time > time_range.begin)
171
+ unless in_wip
172
+ hash[issue] = nil
173
+ next
174
+ end
175
+
176
+ last_change = issue.status_changes.reverse.find { |c| c.time <= time_range.begin }
177
+ hash[issue] = last_change ? status_to_column[last_change.value_id] : nil
178
+ end
179
+ end
180
+
181
+ def events_within_range relevant_issues, status_to_column
182
+ events = []
183
+ relevant_issues.each do |issue|
184
+ started_time, stopped_time = issue.board.cycletime.started_stopped_times(issue)
185
+ next unless started_time
186
+
187
+ # Issue starts within the window: add an explicit event to enter WIP in its current column
188
+ if started_time > time_range.begin && started_time <= time_range.end
189
+ last_change = issue.status_changes.reverse.find { |c| c.time <= started_time }
190
+ events << [started_time, issue, last_change ? status_to_column[last_change.value_id] : nil]
191
+ end
192
+
193
+ # Status changes while the issue is actively in WIP and within the window
194
+ issue.status_changes.each do |change|
195
+ next unless change.time > time_range.begin
196
+ next if change.time > time_range.end
197
+ next unless change.time >= started_time
198
+ next if stopped_time && change.time >= stopped_time
199
+
200
+ events << [change.time, issue, status_to_column[change.value_id]]
201
+ end
202
+
203
+ # Issue stops within the window: add an explicit event to exit WIP
204
+ if stopped_time && stopped_time > time_range.begin && stopped_time <= time_range.end
205
+ events << [stopped_time, issue, nil]
206
+ end
207
+ end
208
+ events.sort_by!(&:first)
209
+ end
210
+
211
+ def compute_wip_seconds columns, current_column, events
212
+ wip_counts = Array.new(columns.size, 0)
213
+ current_column.each_value { |col| wip_counts[col] += 1 unless col.nil? }
214
+
215
+ column_wip_seconds = Array.new(columns.size) { Hash.new(0) }
216
+ prev_time = time_range.begin
217
+
218
+ events.each do |time, issue, new_col|
219
+ elapsed = (time - prev_time).to_i
220
+ if elapsed.positive?
221
+ wip_counts.each_with_index { |wip, idx| column_wip_seconds[idx][wip] += elapsed }
222
+ prev_time = time
223
+ end
224
+
225
+ old_col = current_column[issue]
226
+ wip_counts[old_col] -= 1 unless old_col.nil?
227
+ wip_counts[new_col] += 1 unless new_col.nil?
228
+ current_column[issue] = new_col
229
+ end
230
+
231
+ elapsed = (time_range.end - prev_time).to_i
232
+ wip_counts.each_with_index { |wip, idx| column_wip_seconds[idx][wip] += elapsed } if elapsed.positive?
233
+
234
+ column_wip_seconds
235
+ end
236
+ end
data/lib/jirametrics.rb CHANGED
@@ -1,6 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'English'
3
4
  require 'thor'
5
+ require 'require_all'
6
+
7
+ # This one does need to be loaded early. The rest will be loaded later.
8
+ require 'jirametrics/file_system'
4
9
 
5
10
  class JiraMetrics < Thor
6
11
  def self.exit_on_failure?
@@ -43,76 +48,90 @@ class JiraMetrics < Thor
43
48
 
44
49
  option :config
45
50
  desc 'info', 'Dump information about one issue'
46
- def info keys
51
+ def info key
47
52
  load_config options[:config]
48
- Exporter.instance.info(keys, name_filter: options[:name] || '*')
53
+ Exporter.instance.info(key, name_filter: options[:name] || '*')
49
54
  end
50
55
 
51
- private
56
+ option :config
57
+ option :name
58
+ desc 'mcp', 'Start in MCP (Model Context Protocol) server mode'
59
+ def mcp
60
+ # Redirect stdout to stderr for the entire startup phase so that any
61
+ # incidental output (from config files, gem loading, etc.) does not
62
+ # corrupt the JSON-RPC channel before the MCP transport takes over.
63
+ original_stdout = $stdout.dup
64
+ $stdout.reopen($stderr)
65
+
66
+ load_config options[:config]
67
+ require 'jirametrics/mcp_server'
68
+
69
+ Exporter.instance.file_system.log_only = true
52
70
 
53
- def load_config config_file
54
- config_file = './config.rb' if config_file.nil?
71
+ projects = {}
72
+ aggregates = {}
73
+ Exporter.instance.each_project_config(name_filter: options[:name] || '*') do |project|
74
+ project.evaluate_next_level
75
+ project.run load_only: true
76
+ projects[project.name || 'default'] = {
77
+ issues: project.issues,
78
+ today: project.time_range.end.to_date,
79
+ end_time: project.time_range.end
80
+ }
81
+ rescue StandardError => e
82
+ if e.message.start_with? 'This is an aggregated project'
83
+ names = project.aggregate_project_names
84
+ aggregates[project.name] = names if names.any?
85
+ next
86
+ end
87
+ next if e.message.start_with? 'No data found'
55
88
 
56
- if File.exist? config_file
57
- # The fact that File.exist can see the file does not mean that require will be
58
- # able to load it. Convert this to an absolute pathname now for require.
59
- config_file = File.absolute_path(config_file).to_s
60
- else
61
- puts "Cannot find configuration file #{config_file.inspect}"
62
- exit 1
89
+ raise
63
90
  end
64
91
 
65
- require 'jirametrics/value_equality'
66
- require 'jirametrics/chart_base'
67
- require 'jirametrics/rules'
68
- require 'jirametrics/grouping_rules'
69
- require 'jirametrics/daily_wip_chart'
70
- require 'jirametrics/groupable_issue_chart'
71
- require 'jirametrics/css_variable'
72
-
73
- require 'jirametrics/aggregate_config'
74
- require 'jirametrics/expedited_chart'
75
- require 'jirametrics/board_config'
76
- require 'jirametrics/file_config'
77
- require 'jirametrics/jira_gateway'
78
- require 'jirametrics/trend_line_calculator'
79
- require 'jirametrics/status'
80
- require 'jirametrics/issue_link'
81
- require 'jirametrics/estimate_accuracy_chart'
82
- require 'jirametrics/status_collection'
83
- require 'jirametrics/sprint'
84
- require 'jirametrics/issue'
85
- require 'jirametrics/daily_wip_by_age_chart'
86
- require 'jirametrics/daily_wip_by_parent_chart'
87
- require 'jirametrics/aging_work_in_progress_chart'
88
- require 'jirametrics/cycletime_scatterplot'
89
- require 'jirametrics/flow_efficiency_scatterplot'
90
- require 'jirametrics/sprint_issue_change_data'
91
- require 'jirametrics/cycletime_histogram'
92
- require 'jirametrics/daily_wip_by_blocked_stalled_chart'
93
- require 'jirametrics/html_report_config'
94
- require 'jirametrics/data_quality_report'
95
- require 'jirametrics/aging_work_bar_chart'
96
- require 'jirametrics/change_item'
97
- require 'jirametrics/project_config'
98
- require 'jirametrics/dependency_chart'
99
- require 'jirametrics/cycletime_config'
100
- require 'jirametrics/tree_organizer'
101
- require 'jirametrics/aging_work_table'
102
- require 'jirametrics/sprint_burndown'
103
- require 'jirametrics/self_or_issue_dispatcher'
104
- require 'jirametrics/throughput_chart'
105
- require 'jirametrics/exporter'
106
- require 'jirametrics/file_system'
107
- require 'jirametrics/blocked_stalled_change'
108
- require 'jirametrics/board_column'
109
- require 'jirametrics/anonymizer'
110
- require 'jirametrics/downloader'
111
- require 'jirametrics/fix_version'
112
- require 'jirametrics/download_config'
113
- require 'jirametrics/columns_config'
114
- require 'jirametrics/hierarchy_table'
115
- require 'jirametrics/board'
116
- load config_file
92
+ $stdout.reopen(original_stdout)
93
+ original_stdout.close
94
+ McpServer.new(projects: projects, aggregates: aggregates, timezone_offset: Exporter.instance.timezone_offset).run
95
+ end
96
+
97
+ option :config
98
+ desc 'stitch', 'Dump information about one issue'
99
+ def stitch stitch_file = 'stitcher.erb'
100
+ load_config options[:config]
101
+ Exporter.instance.stitch stitch_file
102
+ end
103
+
104
+ def self.log_uncaught_exception exception, file_system: nil
105
+ return unless exception && !exception.is_a?(SystemExit)
106
+
107
+ begin
108
+ file_system ||= Exporter.instance.file_system
109
+ return if file_system.logfile == $stdout
110
+
111
+ file_system.logfile.puts "#{exception.class}: #{exception.message}"
112
+ exception.backtrace&.each { |line| file_system.logfile.puts "\t#{line}" }
113
+ rescue StandardError
114
+ # Exporter may not be initialized, or the logfile may already be closed
115
+ end
116
+ end
117
+
118
+ at_exit { JiraMetrics.log_uncaught_exception $ERROR_INFO }
119
+
120
+ no_commands do
121
+ def load_config config_file, file_system: FileSystem.new
122
+ config_file = './config.rb' if config_file.nil?
123
+
124
+ if file_system.file_exist? config_file
125
+ # The fact that File.exist can see the file does not mean that require will be
126
+ # able to load it. Convert this to an absolute pathname now for require.
127
+ config_file = File.absolute_path(config_file).to_s
128
+ else
129
+ file_system.error "Cannot find configuration file #{config_file.inspect}"
130
+ exit 1
131
+ end
132
+
133
+ require_rel 'jirametrics'
134
+ load config_file
135
+ end
117
136
  end
118
137
  end
metadata CHANGED
@@ -1,14 +1,42 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jirametrics
3
3
  version: !ruby/object:Gem::Version
4
- version: '2.11'
4
+ version: '2.30'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Bowler
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-03-11 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: mutant-rspec
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '0'
19
+ type: :development
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: mcp
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
12
40
  - !ruby/object:Gem::Dependency
13
41
  name: random-word
14
42
  requirement: !ruby/object:Gem::Requirement
@@ -55,28 +83,36 @@ description: Extract metrics from Jira and export to either a report or to CSV f
55
83
  email: mbowler@gargoylesoftware.com
56
84
  executables:
57
85
  - jirametrics
86
+ - jirametrics-mcp
58
87
  extensions: []
59
88
  extra_rdoc_files: []
60
89
  files:
61
90
  - bin/jirametrics
91
+ - bin/jirametrics-mcp
62
92
  - lib/jirametrics.rb
63
93
  - lib/jirametrics/aggregate_config.rb
64
94
  - lib/jirametrics/aging_work_bar_chart.rb
65
95
  - lib/jirametrics/aging_work_in_progress_chart.rb
66
96
  - lib/jirametrics/aging_work_table.rb
67
97
  - lib/jirametrics/anonymizer.rb
98
+ - lib/jirametrics/atlassian_document_format.rb
99
+ - lib/jirametrics/bar_chart_range.rb
68
100
  - lib/jirametrics/blocked_stalled_change.rb
69
101
  - lib/jirametrics/board.rb
70
102
  - lib/jirametrics/board_column.rb
71
103
  - lib/jirametrics/board_config.rb
104
+ - lib/jirametrics/board_feature.rb
72
105
  - lib/jirametrics/board_movement_calculator.rb
106
+ - lib/jirametrics/cfd_data_builder.rb
73
107
  - lib/jirametrics/change_item.rb
74
108
  - lib/jirametrics/chart_base.rb
75
109
  - lib/jirametrics/columns_config.rb
76
110
  - lib/jirametrics/css_variable.rb
77
- - lib/jirametrics/cycletime_config.rb
111
+ - lib/jirametrics/cumulative_flow_diagram.rb
112
+ - lib/jirametrics/cycle_time_config.rb
78
113
  - lib/jirametrics/cycletime_histogram.rb
79
114
  - lib/jirametrics/cycletime_scatterplot.rb
115
+ - lib/jirametrics/daily_view.rb
80
116
  - lib/jirametrics/daily_wip_by_age_chart.rb
81
117
  - lib/jirametrics/daily_wip_by_blocked_stalled_chart.rb
82
118
  - lib/jirametrics/daily_wip_by_parent_chart.rb
@@ -85,7 +121,10 @@ files:
85
121
  - lib/jirametrics/dependency_chart.rb
86
122
  - lib/jirametrics/download_config.rb
87
123
  - lib/jirametrics/downloader.rb
124
+ - lib/jirametrics/downloader_for_cloud.rb
125
+ - lib/jirametrics/downloader_for_data_center.rb
88
126
  - lib/jirametrics/estimate_accuracy_chart.rb
127
+ - lib/jirametrics/estimation_configuration.rb
89
128
  - lib/jirametrics/examples/aggregated_project.rb
90
129
  - lib/jirametrics/examples/standard_project.rb
91
130
  - lib/jirametrics/expedited_chart.rb
@@ -94,6 +133,7 @@ files:
94
133
  - lib/jirametrics/file_system.rb
95
134
  - lib/jirametrics/fix_version.rb
96
135
  - lib/jirametrics/flow_efficiency_scatterplot.rb
136
+ - lib/jirametrics/github_gateway.rb
97
137
  - lib/jirametrics/groupable_issue_chart.rb
98
138
  - lib/jirametrics/grouping_rules.rb
99
139
  - lib/jirametrics/hierarchy_table.rb
@@ -101,8 +141,7 @@ files:
101
141
  - lib/jirametrics/html/aging_work_in_progress_chart.erb
102
142
  - lib/jirametrics/html/aging_work_table.erb
103
143
  - lib/jirametrics/html/collapsible_issues_panel.erb
104
- - lib/jirametrics/html/cycletime_histogram.erb
105
- - lib/jirametrics/html/cycletime_scatterplot.erb
144
+ - lib/jirametrics/html/cumulative_flow_diagram.erb
106
145
  - lib/jirametrics/html/daily_wip_chart.erb
107
146
  - lib/jirametrics/html/estimate_accuracy_chart.erb
108
147
  - lib/jirametrics/html/expedited_chart.erb
@@ -110,13 +149,27 @@ files:
110
149
  - lib/jirametrics/html/hierarchy_table.erb
111
150
  - lib/jirametrics/html/index.css
112
151
  - lib/jirametrics/html/index.erb
152
+ - lib/jirametrics/html/index.js
153
+ - lib/jirametrics/html/legacy_colors.css
113
154
  - lib/jirametrics/html/sprint_burndown.erb
114
155
  - lib/jirametrics/html/throughput_chart.erb
156
+ - lib/jirametrics/html/time_based_histogram.erb
157
+ - lib/jirametrics/html/time_based_scatterplot.erb
158
+ - lib/jirametrics/html/wip_by_column_chart.erb
159
+ - lib/jirametrics/html_generator.rb
115
160
  - lib/jirametrics/html_report_config.rb
116
161
  - lib/jirametrics/issue.rb
162
+ - lib/jirametrics/issue_collection.rb
117
163
  - lib/jirametrics/issue_link.rb
164
+ - lib/jirametrics/issue_printer.rb
118
165
  - lib/jirametrics/jira_gateway.rb
166
+ - lib/jirametrics/mcp_server.rb
119
167
  - lib/jirametrics/project_config.rb
168
+ - lib/jirametrics/pull_request.rb
169
+ - lib/jirametrics/pull_request_cycle_time_histogram.rb
170
+ - lib/jirametrics/pull_request_cycle_time_scatterplot.rb
171
+ - lib/jirametrics/pull_request_review.rb
172
+ - lib/jirametrics/raw_javascript.rb
120
173
  - lib/jirametrics/rules.rb
121
174
  - lib/jirametrics/self_or_issue_dispatcher.rb
122
175
  - lib/jirametrics/settings.json
@@ -125,10 +178,16 @@ files:
125
178
  - lib/jirametrics/sprint_issue_change_data.rb
126
179
  - lib/jirametrics/status.rb
127
180
  - lib/jirametrics/status_collection.rb
181
+ - lib/jirametrics/stitcher.rb
182
+ - lib/jirametrics/throughput_by_completed_resolution_chart.rb
128
183
  - lib/jirametrics/throughput_chart.rb
184
+ - lib/jirametrics/time_based_histogram.rb
185
+ - lib/jirametrics/time_based_scatterplot.rb
129
186
  - lib/jirametrics/tree_organizer.rb
130
187
  - lib/jirametrics/trend_line_calculator.rb
188
+ - lib/jirametrics/user.rb
131
189
  - lib/jirametrics/value_equality.rb
190
+ - lib/jirametrics/wip_by_column_chart.rb
132
191
  homepage: https://jirametrics.org
133
192
  licenses:
134
193
  - Apache-2.0
@@ -151,7 +210,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
151
210
  - !ruby/object:Gem::Version
152
211
  version: '0'
153
212
  requirements: []
154
- rubygems_version: 3.6.2
213
+ rubygems_version: 4.0.10
155
214
  specification_version: 4
156
215
  summary: Extract Jira metrics
157
216
  test_files: []