jirametrics 2.4 → 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 (100) hide show
  1. checksums.yaml +4 -4
  2. data/bin/jirametrics-mcp +5 -0
  3. data/lib/jirametrics/aggregate_config.rb +16 -3
  4. data/lib/jirametrics/aging_work_bar_chart.rb +193 -133
  5. data/lib/jirametrics/aging_work_in_progress_chart.rb +138 -42
  6. data/lib/jirametrics/aging_work_table.rb +63 -19
  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 +6 -4
  11. data/lib/jirametrics/board.rb +74 -22
  12. data/lib/jirametrics/board_config.rb +11 -3
  13. data/lib/jirametrics/board_feature.rb +14 -0
  14. data/lib/jirametrics/board_movement_calculator.rb +155 -0
  15. data/lib/jirametrics/cfd_data_builder.rb +108 -0
  16. data/lib/jirametrics/change_item.rb +54 -18
  17. data/lib/jirametrics/chart_base.rb +203 -30
  18. data/lib/jirametrics/css_variable.rb +2 -2
  19. data/lib/jirametrics/cumulative_flow_diagram.rb +208 -0
  20. data/lib/jirametrics/cycle_time_config.rb +137 -0
  21. data/lib/jirametrics/cycletime_histogram.rb +17 -38
  22. data/lib/jirametrics/cycletime_scatterplot.rb +18 -87
  23. data/lib/jirametrics/daily_view.rb +306 -0
  24. data/lib/jirametrics/daily_wip_by_age_chart.rb +5 -8
  25. data/lib/jirametrics/daily_wip_by_blocked_stalled_chart.rb +15 -5
  26. data/lib/jirametrics/daily_wip_by_parent_chart.rb +4 -6
  27. data/lib/jirametrics/daily_wip_chart.rb +36 -16
  28. data/lib/jirametrics/data_quality_report.rb +251 -42
  29. data/lib/jirametrics/dependency_chart.rb +42 -12
  30. data/lib/jirametrics/download_config.rb +27 -0
  31. data/lib/jirametrics/downloader.rb +185 -110
  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 +75 -14
  35. data/lib/jirametrics/estimation_configuration.rb +25 -0
  36. data/lib/jirametrics/examples/aggregated_project.rb +9 -23
  37. data/lib/jirametrics/examples/standard_project.rb +57 -58
  38. data/lib/jirametrics/expedited_chart.rb +11 -10
  39. data/lib/jirametrics/exporter.rb +51 -14
  40. data/lib/jirametrics/file_config.rb +21 -6
  41. data/lib/jirametrics/file_system.rb +96 -4
  42. data/lib/jirametrics/fix_version.rb +13 -0
  43. data/lib/jirametrics/flow_efficiency_scatterplot.rb +115 -0
  44. data/lib/jirametrics/github_gateway.rb +115 -0
  45. data/lib/jirametrics/groupable_issue_chart.rb +12 -4
  46. data/lib/jirametrics/grouping_rules.rb +26 -4
  47. data/lib/jirametrics/html/aging_work_bar_chart.erb +8 -17
  48. data/lib/jirametrics/html/aging_work_in_progress_chart.erb +24 -5
  49. data/lib/jirametrics/html/aging_work_table.erb +13 -4
  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 +41 -15
  53. data/lib/jirametrics/html/estimate_accuracy_chart.erb +4 -12
  54. data/lib/jirametrics/html/expedited_chart.erb +7 -24
  55. data/lib/jirametrics/html/flow_efficiency_scatterplot.erb +81 -0
  56. data/lib/jirametrics/html/hierarchy_table.erb +1 -1
  57. data/lib/jirametrics/html/index.css +336 -62
  58. data/lib/jirametrics/html/index.erb +16 -21
  59. data/lib/jirametrics/html/index.js +164 -0
  60. data/lib/jirametrics/html/legacy_colors.css +174 -0
  61. data/lib/jirametrics/html/sprint_burndown.erb +18 -25
  62. data/lib/jirametrics/html/throughput_chart.erb +43 -21
  63. data/lib/jirametrics/html/time_based_histogram.erb +123 -0
  64. data/lib/jirametrics/html/{cycletime_scatterplot.erb → time_based_scatterplot.erb} +16 -21
  65. data/lib/jirametrics/html/wip_by_column_chart.erb +250 -0
  66. data/lib/jirametrics/html_generator.rb +32 -0
  67. data/lib/jirametrics/html_report_config.rb +83 -76
  68. data/lib/jirametrics/issue.rb +499 -91
  69. data/lib/jirametrics/issue_collection.rb +33 -0
  70. data/lib/jirametrics/issue_printer.rb +97 -0
  71. data/lib/jirametrics/jira_gateway.rb +96 -16
  72. data/lib/jirametrics/mcp_server.rb +531 -0
  73. data/lib/jirametrics/project_config.rb +374 -130
  74. data/lib/jirametrics/pull_request.rb +30 -0
  75. data/lib/jirametrics/pull_request_cycle_time_histogram.rb +77 -0
  76. data/lib/jirametrics/pull_request_cycle_time_scatterplot.rb +88 -0
  77. data/lib/jirametrics/pull_request_review.rb +13 -0
  78. data/lib/jirametrics/raw_javascript.rb +17 -0
  79. data/lib/jirametrics/rules.rb +2 -2
  80. data/lib/jirametrics/self_or_issue_dispatcher.rb +2 -0
  81. data/lib/jirametrics/settings.json +10 -2
  82. data/lib/jirametrics/sprint.rb +13 -0
  83. data/lib/jirametrics/sprint_burndown.rb +47 -39
  84. data/lib/jirametrics/sprint_issue_change_data.rb +3 -3
  85. data/lib/jirametrics/status.rb +84 -19
  86. data/lib/jirametrics/status_collection.rb +83 -38
  87. data/lib/jirametrics/stitcher.rb +81 -0
  88. data/lib/jirametrics/throughput_by_completed_resolution_chart.rb +22 -0
  89. data/lib/jirametrics/throughput_chart.rb +73 -23
  90. data/lib/jirametrics/time_based_histogram.rb +139 -0
  91. data/lib/jirametrics/time_based_scatterplot.rb +107 -0
  92. data/lib/jirametrics/user.rb +12 -0
  93. data/lib/jirametrics/value_equality.rb +2 -2
  94. data/lib/jirametrics/wip_by_column_chart.rb +236 -0
  95. data/lib/jirametrics.rb +101 -66
  96. metadata +72 -16
  97. data/lib/jirametrics/cycletime_config.rb +0 -69
  98. data/lib/jirametrics/discard_changes_before.rb +0 -37
  99. data/lib/jirametrics/html/cycletime_histogram.erb +0 -47
  100. data/lib/jirametrics/html/data_quality_report.erb +0 -126
@@ -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,11 +1,27 @@
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
11
+ def self.exit_on_failure?
12
+ true
13
+ end
14
+
15
+ map %w[--version -v] => :__print_version
16
+
17
+ desc '--version, -v', 'print the version'
18
+ def __print_version
19
+ puts Gem.loaded_specs['jirametrics'].version
20
+ end
21
+
6
22
  option :config
7
23
  option :name
8
- desc 'export only', "Export data into either reports or CSV's as per the configuration"
24
+ desc 'export', "Export data into either reports or CSV's as per the configuration"
9
25
  def export
10
26
  load_config options[:config]
11
27
  Exporter.instance.export(name_filter: options[:name] || '*')
@@ -13,7 +29,7 @@ class JiraMetrics < Thor
13
29
 
14
30
  option :config
15
31
  option :name
16
- desc 'download only', 'Download data from Jira'
32
+ desc 'download', 'Download data from Jira'
17
33
  def download
18
34
  load_config options[:config]
19
35
  Exporter.instance.download(name_filter: options[:name] || '*')
@@ -21,7 +37,7 @@ class JiraMetrics < Thor
21
37
 
22
38
  option :config
23
39
  option :name
24
- desc 'download and export', 'Same as running download, followed by export'
40
+ desc 'go', 'Same as running download, followed by export'
25
41
  def go
26
42
  load_config options[:config]
27
43
  Exporter.instance.download(name_filter: options[:name] || '*')
@@ -30,73 +46,92 @@ class JiraMetrics < Thor
30
46
  Exporter.instance.export(name_filter: options[:name] || '*')
31
47
  end
32
48
 
33
- private
49
+ option :config
50
+ desc 'info', 'Dump information about one issue'
51
+ def info key
52
+ load_config options[:config]
53
+ Exporter.instance.info(key, name_filter: options[:name] || '*')
54
+ end
55
+
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
34
70
 
35
- def load_config config_file
36
- 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'
37
88
 
38
- if File.exist? config_file
39
- # The fact that File.exist can see the file does not mean that require will be
40
- # able to load it. Convert this to an absolute pathname now for require.
41
- config_file = File.absolute_path(config_file).to_s
42
- else
43
- puts "Cannot find configuration file #{config_file.inspect}"
44
- exit 1
89
+ raise
45
90
  end
46
91
 
47
- require 'jirametrics/value_equality'
48
- require 'jirametrics/chart_base'
49
- require 'jirametrics/rules'
50
- require 'jirametrics/grouping_rules'
51
- require 'jirametrics/daily_wip_chart'
52
- require 'jirametrics/groupable_issue_chart'
53
- require 'jirametrics/discard_changes_before'
54
- require 'jirametrics/css_variable'
55
-
56
- require 'jirametrics/aggregate_config'
57
- require 'jirametrics/expedited_chart'
58
- require 'jirametrics/board_config'
59
- require 'jirametrics/file_config'
60
- require 'jirametrics/jira_gateway'
61
- require 'jirametrics/trend_line_calculator'
62
- require 'jirametrics/status'
63
- require 'jirametrics/issue_link'
64
- require 'jirametrics/estimate_accuracy_chart'
65
- require 'jirametrics/status_collection'
66
- require 'jirametrics/sprint'
67
- require 'jirametrics/issue'
68
- require 'jirametrics/daily_wip_by_age_chart'
69
- require 'jirametrics/daily_wip_by_parent_chart'
70
- require 'jirametrics/aging_work_in_progress_chart'
71
- require 'jirametrics/cycletime_scatterplot'
72
- require 'jirametrics/sprint_issue_change_data'
73
- require 'jirametrics/cycletime_histogram'
74
- require 'jirametrics/daily_wip_by_blocked_stalled_chart'
75
- require 'jirametrics/html_report_config'
76
- require 'jirametrics/data_quality_report'
77
- require 'jirametrics/aging_work_bar_chart'
78
- require 'jirametrics/change_item'
79
- require 'jirametrics/project_config'
80
- require 'jirametrics/dependency_chart'
81
- require 'jirametrics/cycletime_config'
82
- require 'jirametrics/tree_organizer'
83
- require 'jirametrics/aging_work_table'
84
- require 'jirametrics/sprint_burndown'
85
- require 'jirametrics/self_or_issue_dispatcher'
86
- require 'jirametrics/throughput_chart'
87
- require 'jirametrics/exporter'
88
- require 'jirametrics/file_system'
89
- require 'jirametrics/blocked_stalled_change'
90
- require 'jirametrics/board_column'
91
- require 'jirametrics/anonymizer'
92
- require 'jirametrics/downloader'
93
- require 'jirametrics/fix_version'
94
- require 'jirametrics/download_config'
95
- require 'jirametrics/columns_config'
96
- require 'jirametrics/hierarchy_table'
97
- require 'jirametrics/board'
98
- 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
99
95
  end
100
96
 
101
- # Dir.foreach('lib/jirametrics') {|file| puts "require 'jirametrics/#{$1}'" if file =~ /^(.+)\.rb$/}
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
136
+ end
102
137
  end
metadata CHANGED
@@ -1,15 +1,42 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jirametrics
3
3
  version: !ruby/object:Gem::Version
4
- version: '2.4'
4
+ version: '2.30'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Bowler
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2024-08-17 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
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'
13
40
  - !ruby/object:Gem::Dependency
14
41
  name: random-word
15
42
  requirement: !ruby/object:Gem::Requirement
@@ -52,42 +79,52 @@ dependencies:
52
79
  - - "~>"
53
80
  - !ruby/object:Gem::Version
54
81
  version: 1.2.2
55
- description: Tool to extract metrics from Jira and export to either a report or to
56
- CSV files
82
+ description: Extract metrics from Jira and export to either a report or to CSV files
57
83
  email: mbowler@gargoylesoftware.com
58
84
  executables:
59
85
  - jirametrics
86
+ - jirametrics-mcp
60
87
  extensions: []
61
88
  extra_rdoc_files: []
62
89
  files:
63
90
  - bin/jirametrics
91
+ - bin/jirametrics-mcp
64
92
  - lib/jirametrics.rb
65
93
  - lib/jirametrics/aggregate_config.rb
66
94
  - lib/jirametrics/aging_work_bar_chart.rb
67
95
  - lib/jirametrics/aging_work_in_progress_chart.rb
68
96
  - lib/jirametrics/aging_work_table.rb
69
97
  - lib/jirametrics/anonymizer.rb
98
+ - lib/jirametrics/atlassian_document_format.rb
99
+ - lib/jirametrics/bar_chart_range.rb
70
100
  - lib/jirametrics/blocked_stalled_change.rb
71
101
  - lib/jirametrics/board.rb
72
102
  - lib/jirametrics/board_column.rb
73
103
  - lib/jirametrics/board_config.rb
104
+ - lib/jirametrics/board_feature.rb
105
+ - lib/jirametrics/board_movement_calculator.rb
106
+ - lib/jirametrics/cfd_data_builder.rb
74
107
  - lib/jirametrics/change_item.rb
75
108
  - lib/jirametrics/chart_base.rb
76
109
  - lib/jirametrics/columns_config.rb
77
110
  - lib/jirametrics/css_variable.rb
78
- - lib/jirametrics/cycletime_config.rb
111
+ - lib/jirametrics/cumulative_flow_diagram.rb
112
+ - lib/jirametrics/cycle_time_config.rb
79
113
  - lib/jirametrics/cycletime_histogram.rb
80
114
  - lib/jirametrics/cycletime_scatterplot.rb
115
+ - lib/jirametrics/daily_view.rb
81
116
  - lib/jirametrics/daily_wip_by_age_chart.rb
82
117
  - lib/jirametrics/daily_wip_by_blocked_stalled_chart.rb
83
118
  - lib/jirametrics/daily_wip_by_parent_chart.rb
84
119
  - lib/jirametrics/daily_wip_chart.rb
85
120
  - lib/jirametrics/data_quality_report.rb
86
121
  - lib/jirametrics/dependency_chart.rb
87
- - lib/jirametrics/discard_changes_before.rb
88
122
  - lib/jirametrics/download_config.rb
89
123
  - lib/jirametrics/downloader.rb
124
+ - lib/jirametrics/downloader_for_cloud.rb
125
+ - lib/jirametrics/downloader_for_data_center.rb
90
126
  - lib/jirametrics/estimate_accuracy_chart.rb
127
+ - lib/jirametrics/estimation_configuration.rb
91
128
  - lib/jirametrics/examples/aggregated_project.rb
92
129
  - lib/jirametrics/examples/standard_project.rb
93
130
  - lib/jirametrics/expedited_chart.rb
@@ -95,6 +132,8 @@ files:
95
132
  - lib/jirametrics/file_config.rb
96
133
  - lib/jirametrics/file_system.rb
97
134
  - lib/jirametrics/fix_version.rb
135
+ - lib/jirametrics/flow_efficiency_scatterplot.rb
136
+ - lib/jirametrics/github_gateway.rb
98
137
  - lib/jirametrics/groupable_issue_chart.rb
99
138
  - lib/jirametrics/grouping_rules.rb
100
139
  - lib/jirametrics/hierarchy_table.rb
@@ -102,22 +141,35 @@ files:
102
141
  - lib/jirametrics/html/aging_work_in_progress_chart.erb
103
142
  - lib/jirametrics/html/aging_work_table.erb
104
143
  - lib/jirametrics/html/collapsible_issues_panel.erb
105
- - lib/jirametrics/html/cycletime_histogram.erb
106
- - lib/jirametrics/html/cycletime_scatterplot.erb
144
+ - lib/jirametrics/html/cumulative_flow_diagram.erb
107
145
  - lib/jirametrics/html/daily_wip_chart.erb
108
- - lib/jirametrics/html/data_quality_report.erb
109
146
  - lib/jirametrics/html/estimate_accuracy_chart.erb
110
147
  - lib/jirametrics/html/expedited_chart.erb
148
+ - lib/jirametrics/html/flow_efficiency_scatterplot.erb
111
149
  - lib/jirametrics/html/hierarchy_table.erb
112
150
  - lib/jirametrics/html/index.css
113
151
  - lib/jirametrics/html/index.erb
152
+ - lib/jirametrics/html/index.js
153
+ - lib/jirametrics/html/legacy_colors.css
114
154
  - lib/jirametrics/html/sprint_burndown.erb
115
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
116
160
  - lib/jirametrics/html_report_config.rb
117
161
  - lib/jirametrics/issue.rb
162
+ - lib/jirametrics/issue_collection.rb
118
163
  - lib/jirametrics/issue_link.rb
164
+ - lib/jirametrics/issue_printer.rb
119
165
  - lib/jirametrics/jira_gateway.rb
166
+ - lib/jirametrics/mcp_server.rb
120
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
121
173
  - lib/jirametrics/rules.rb
122
174
  - lib/jirametrics/self_or_issue_dispatcher.rb
123
175
  - lib/jirametrics/settings.json
@@ -126,19 +178,24 @@ files:
126
178
  - lib/jirametrics/sprint_issue_change_data.rb
127
179
  - lib/jirametrics/status.rb
128
180
  - lib/jirametrics/status_collection.rb
181
+ - lib/jirametrics/stitcher.rb
182
+ - lib/jirametrics/throughput_by_completed_resolution_chart.rb
129
183
  - lib/jirametrics/throughput_chart.rb
184
+ - lib/jirametrics/time_based_histogram.rb
185
+ - lib/jirametrics/time_based_scatterplot.rb
130
186
  - lib/jirametrics/tree_organizer.rb
131
187
  - lib/jirametrics/trend_line_calculator.rb
188
+ - lib/jirametrics/user.rb
132
189
  - lib/jirametrics/value_equality.rb
133
- homepage: https://github.com/mikebowler/jirametrics
190
+ - lib/jirametrics/wip_by_column_chart.rb
191
+ homepage: https://jirametrics.org
134
192
  licenses:
135
193
  - Apache-2.0
136
194
  metadata:
137
195
  rubygems_mfa_required: 'true'
138
196
  bug_tracker_uri: https://github.com/mikebowler/jirametrics/issues
139
- changelog_uri: https://github.com/mikebowler/jirametrics/wiki/Changes
140
- documentation_uri: https://github.com/mikebowler/jirametrics/wiki
141
- post_install_message:
197
+ changelog_uri: https://jirametrics.org/changes
198
+ documentation_uri: https://jirametrics.org
142
199
  rdoc_options: []
143
200
  require_paths:
144
201
  - lib
@@ -153,8 +210,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
153
210
  - !ruby/object:Gem::Version
154
211
  version: '0'
155
212
  requirements: []
156
- rubygems_version: 3.5.15
157
- signing_key:
213
+ rubygems_version: 4.0.10
158
214
  specification_version: 4
159
215
  summary: Extract Jira metrics
160
216
  test_files: []
@@ -1,69 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'jirametrics/self_or_issue_dispatcher'
4
- require 'date'
5
-
6
- class CycleTimeConfig
7
- include SelfOrIssueDispatcher
8
-
9
- attr_reader :label, :parent_config
10
-
11
- def initialize parent_config:, label:, block:, today: Date.today
12
- @parent_config = parent_config
13
- @label = label
14
- @today = today
15
- instance_eval(&block) unless block.nil?
16
- end
17
-
18
- def start_at block = nil
19
- @start_at = block unless block.nil?
20
- @start_at
21
- end
22
-
23
- def stop_at block = nil
24
- @stop_at = block unless block.nil?
25
- @stop_at
26
- end
27
-
28
- def in_progress? issue
29
- started_time(issue) && stopped_time(issue).nil?
30
- end
31
-
32
- def done? issue
33
- stopped_time(issue)
34
- end
35
-
36
- def started_time issue
37
- @start_at.call(issue)
38
- end
39
-
40
- def stopped_time issue
41
- @stop_at.call(issue)
42
- end
43
-
44
- def cycletime issue
45
- start = started_time(issue)
46
- stop = stopped_time(issue)
47
- return nil if start.nil? || stop.nil?
48
-
49
- (stop.to_date - start.to_date).to_i + 1
50
- end
51
-
52
- def age issue, today: nil
53
- start = started_time(issue)
54
- stop = today || @today || Date.today
55
- return nil if start.nil? || stop.nil?
56
-
57
- (stop.to_date - start.to_date).to_i + 1
58
- end
59
-
60
- def possible_statuses
61
- if parent_config.is_a? BoardConfig
62
- project_config = parent_config.project_config
63
- else
64
- # TODO: This will go away when cycletimes are no longer supported inside html_reports
65
- project_config = parent_config.file_config.project_config
66
- end
67
- project_config.possible_statuses
68
- end
69
- end