jirametrics 2.22 → 2.27

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 (75) hide show
  1. checksums.yaml +4 -4
  2. data/lib/jirametrics/aggregate_config.rb +10 -2
  3. data/lib/jirametrics/aging_work_bar_chart.rb +20 -6
  4. data/lib/jirametrics/aging_work_table.rb +4 -5
  5. data/lib/jirametrics/anonymizer.rb +74 -1
  6. data/lib/jirametrics/atlassian_document_format.rb +93 -93
  7. data/lib/jirametrics/blocked_stalled_change.rb +5 -3
  8. data/lib/jirametrics/board.rb +20 -8
  9. data/lib/jirametrics/board_feature.rb +14 -0
  10. data/lib/jirametrics/board_movement_calculator.rb +2 -2
  11. data/lib/jirametrics/cfd_data_builder.rb +108 -0
  12. data/lib/jirametrics/change_item.rb +4 -3
  13. data/lib/jirametrics/chart_base.rb +94 -2
  14. data/lib/jirametrics/css_variable.rb +1 -1
  15. data/lib/jirametrics/cumulative_flow_diagram.rb +208 -0
  16. data/lib/jirametrics/{cycletime_config.rb → cycle_time_config.rb} +1 -2
  17. data/lib/jirametrics/cycletime_histogram.rb +15 -103
  18. data/lib/jirametrics/cycletime_scatterplot.rb +13 -98
  19. data/lib/jirametrics/daily_view.rb +36 -12
  20. data/lib/jirametrics/daily_wip_by_age_chart.rb +1 -1
  21. data/lib/jirametrics/daily_wip_by_blocked_stalled_chart.rb +1 -1
  22. data/lib/jirametrics/daily_wip_by_parent_chart.rb +4 -2
  23. data/lib/jirametrics/daily_wip_chart.rb +29 -7
  24. data/lib/jirametrics/data_quality_report.rb +38 -12
  25. data/lib/jirametrics/dependency_chart.rb +2 -2
  26. data/lib/jirametrics/download_config.rb +15 -0
  27. data/lib/jirametrics/downloader.rb +87 -5
  28. data/lib/jirametrics/downloader_for_cloud.rb +52 -10
  29. data/lib/jirametrics/downloader_for_data_center.rb +2 -1
  30. data/lib/jirametrics/estimate_accuracy_chart.rb +42 -4
  31. data/lib/jirametrics/examples/aggregated_project.rb +2 -2
  32. data/lib/jirametrics/examples/standard_project.rb +29 -19
  33. data/lib/jirametrics/expedited_chart.rb +3 -1
  34. data/lib/jirametrics/exporter.rb +3 -1
  35. data/lib/jirametrics/file_system.rb +35 -2
  36. data/lib/jirametrics/flow_efficiency_scatterplot.rb +5 -1
  37. data/lib/jirametrics/github_gateway.rb +115 -0
  38. data/lib/jirametrics/groupable_issue_chart.rb +4 -0
  39. data/lib/jirametrics/grouping_rules.rb +26 -4
  40. data/lib/jirametrics/html/aging_work_bar_chart.erb +3 -4
  41. data/lib/jirametrics/html/aging_work_table.erb +3 -0
  42. data/lib/jirametrics/html/cumulative_flow_diagram.erb +503 -0
  43. data/lib/jirametrics/html/daily_wip_chart.erb +38 -5
  44. data/lib/jirametrics/html/estimate_accuracy_chart.erb +2 -12
  45. data/lib/jirametrics/html/expedited_chart.erb +3 -13
  46. data/lib/jirametrics/html/flow_efficiency_scatterplot.erb +2 -8
  47. data/lib/jirametrics/html/index.css +117 -0
  48. data/lib/jirametrics/html/index.erb +6 -0
  49. data/lib/jirametrics/html/index.js +52 -2
  50. data/lib/jirametrics/html/sprint_burndown.erb +7 -13
  51. data/lib/jirametrics/html/throughput_chart.erb +40 -9
  52. data/lib/jirametrics/html/{cycletime_histogram.erb → time_based_histogram.erb} +59 -59
  53. data/lib/jirametrics/html/{cycletime_scatterplot.erb → time_based_scatterplot.erb} +11 -7
  54. data/lib/jirametrics/html_generator.rb +2 -1
  55. data/lib/jirametrics/html_report_config.rb +23 -16
  56. data/lib/jirametrics/issue.rb +101 -96
  57. data/lib/jirametrics/issue_printer.rb +97 -0
  58. data/lib/jirametrics/jira_gateway.rb +6 -3
  59. data/lib/jirametrics/mcp_server.rb +305 -0
  60. data/lib/jirametrics/project_config.rb +80 -7
  61. data/lib/jirametrics/pull_request.rb +30 -0
  62. data/lib/jirametrics/pull_request_cycle_time_histogram.rb +77 -0
  63. data/lib/jirametrics/pull_request_cycle_time_scatterplot.rb +88 -0
  64. data/lib/jirametrics/pull_request_review.rb +13 -0
  65. data/lib/jirametrics/raw_javascript.rb +4 -0
  66. data/lib/jirametrics/settings.json +3 -1
  67. data/lib/jirametrics/sprint_burndown.rb +3 -1
  68. data/lib/jirametrics/status.rb +1 -1
  69. data/lib/jirametrics/stitcher.rb +7 -1
  70. data/lib/jirametrics/throughput_by_completed_resolution_chart.rb +22 -0
  71. data/lib/jirametrics/throughput_chart.rb +73 -23
  72. data/lib/jirametrics/time_based_histogram.rb +139 -0
  73. data/lib/jirametrics/time_based_scatterplot.rb +107 -0
  74. data/lib/jirametrics.rb +28 -0
  75. metadata +47 -5
@@ -9,5 +9,7 @@
9
9
  "expedited_priority_names": ["Critical", "Highest"],
10
10
  "priority_order": ["Lowest", "Low", "Medium", "High", "Highest"],
11
11
 
12
- "cache_cycletime_calculations": true
12
+ "cache_cycletime_calculations": true,
13
+
14
+ "date_annotations": []
13
15
  }
@@ -29,6 +29,8 @@ class SprintBurndown < ChartBase
29
29
  </div>
30
30
  #{describe_non_working_days}
31
31
  TEXT
32
+ @x_axis_title = 'Date'
33
+ @y_axis_title = 'Items remaining'
32
34
  end
33
35
 
34
36
  def options= arg
@@ -132,7 +134,7 @@ class SprintBurndown < ChartBase
132
134
 
133
135
  estimate_display_name = current_board.estimation_configuration.display_name
134
136
 
135
- issue_completed_time = issue.board.cycletime.started_stopped_times(issue).last
137
+ issue_completed_time = issue.started_stopped_times.last
136
138
  completed_has_been_tracked = false
137
139
 
138
140
  issue.changes.each do |change|
@@ -36,7 +36,7 @@ class Status
36
36
  end
37
37
 
38
38
  def self.from_raw raw
39
- raise "raw cannot be nil" if raw.nil?
39
+ raise 'raw cannot be nil' if raw.nil?
40
40
 
41
41
  category_config = raw['statusCategory']
42
42
  raise "statusCategory can't be nil in #{category_config.inspect}" if category_config.nil?
@@ -44,7 +44,8 @@ class Stitcher < HtmlGenerator
44
44
  stitch_content = @all_stitches.find { |s| s.file == from_file && s.title == title && s.type == type }
45
45
  return stitch_content.content if stitch_content
46
46
 
47
- raise "Unable to find content in file #{from_file.inspect} matching title: #{title.inspect}"
47
+ file_system.error "Unable to find content in file #{from_file.inspect} matching title: #{title.inspect}"
48
+ ''
48
49
  end
49
50
 
50
51
  def parse_file filename
@@ -59,6 +60,11 @@ class Stitcher < HtmlGenerator
59
60
  if matches[:seam] == 'start'
60
61
  content = +''
61
62
  else
63
+ if content.nil? || content.strip.empty?
64
+ file_system.warning "Seam found with no content in #{filename.inspect}: " \
65
+ "id=#{matches[:id].strip.inspect}, class=#{matches[:clazz].strip.inspect}, " \
66
+ "title=#{matches[:title].strip.inspect}"
67
+ end
62
68
  @all_stitches << Stitcher::StitchContent.new(
63
69
  file: filename, title: matches[:title], type: matches[:type], content: content
64
70
  )
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'jirametrics/throughput_chart'
4
+
5
+ class ThroughputByCompletedResolutionChart < ThroughputChart
6
+ def initialize block
7
+ super
8
+ header_text 'Throughput, grouped by completion status and resolution'
9
+ description_text nil
10
+ end
11
+
12
+ def default_grouping_rules issue, rules
13
+ status, resolution = issue.status_resolution_at_done
14
+ if resolution
15
+ rules.label = "#{status.name}:#{resolution}"
16
+ rules.label_hint = "Status: #{status.name.inspect}:#{status.id}, resolution: #{resolution.inspect}"
17
+ else
18
+ rules.label = status.name
19
+ rules.label_hint = "Status: #{status.name.inspect}:#{status.id}"
20
+ end
21
+ end
22
+ end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'cgi'
4
+
3
5
  class ThroughputChart < ChartBase
4
6
  include GroupableIssueChart
5
7
 
@@ -10,42 +12,54 @@ class ThroughputChart < ChartBase
10
12
 
11
13
  header_text 'Throughput Chart'
12
14
  description_text <<-TEXT
13
- <div class="p">
14
- This chart shows how many items we completed per week
15
+ <div>Throughput data is very useful for#{' '}
16
+ <a href="https://blog.mikebowler.ca/2024/06/02/probabilistic-forecasting/">probabilistic forecasting</a>,
17
+ to determine when we'll be done. Try it now with the
18
+ <a href="<%= throughput_forecaster_url %>" target="_blank" rel="noopener noreferrer">
19
+ Focused Objective throughput forecaster,</a> to see how long it would take to complete all of the
20
+ <%= @not_started_count %> items you currently have in your backlog.
15
21
  </div>
16
22
  #{describe_non_working_days}
17
23
  TEXT
24
+ @x_axis_title = nil
25
+ @y_axis_title = 'Count of items'
18
26
 
19
27
  init_configuration_block(block) do
20
- grouping_rules do |issue, rule|
21
- rule.label = issue.type
22
- rule.color = color_for type: issue.type
23
- end
28
+ grouping_rules { |issue, rule| default_grouping_rules(issue, rule) }
24
29
  end
25
30
  end
26
31
 
27
32
  def run
33
+ # This is saved as an instance variable so that it's accessible later when rendering the description text
34
+ @not_started_count = issues.count { |issue| issue.started_stopped_times.first.nil? }
35
+
28
36
  completed_issues = completed_issues_in_range include_unstarted: true
29
37
  rules_to_issues = group_issues completed_issues
30
38
  data_sets = []
31
- if rules_to_issues.size > 1
32
- data_sets << weekly_throughput_dataset(
33
- completed_issues: completed_issues,
34
- label: 'Totals',
35
- color: CssVariable['--throughput_chart_total_line_color'],
36
- dashed: true
37
- )
38
- end
39
+ total_data_set = weekly_throughput_dataset(
40
+ completed_issues: completed_issues,
41
+ label: 'Totals',
42
+ color: CssVariable['--throughput_chart_total_line_color'],
43
+ dashed: true
44
+ )
45
+ @throughput_samples = total_data_set[:data].collect { |d| d[:y] }
46
+ data_sets << total_data_set if rules_to_issues.size > 1
39
47
 
40
48
  rules_to_issues.each_key do |rules|
41
49
  data_sets << weekly_throughput_dataset(
42
- completed_issues: rules_to_issues[rules], label: rules.label, color: rules.color
50
+ completed_issues: rules_to_issues[rules], label: rules.label, color: rules.color,
51
+ label_hint: rules.label_hint
43
52
  )
44
53
  end
45
54
 
46
55
  wrap_and_render(binding, __FILE__)
47
56
  end
48
57
 
58
+ def default_grouping_rules issue, rule
59
+ rule.label = issue.type
60
+ rule.color = color_for type: issue.type
61
+ end
62
+
49
63
  def calculate_time_periods
50
64
  first_day = @date_range.begin
51
65
  first_day = case first_day.wday
@@ -65,10 +79,22 @@ class ThroughputChart < ChartBase
65
79
  end
66
80
  end
67
81
 
68
- def weekly_throughput_dataset completed_issues:, label:, color:, dashed: false
82
+ def calculate_custom_periods
83
+ last_days = @issue_periods.values.compact.uniq.sort
84
+ last_days.each_with_index.map do |last_day, i|
85
+ first_day = i.zero? ? @date_range.begin : last_days[i - 1] + 1
86
+ first_day..last_day
87
+ end
88
+ end
89
+
90
+ def weekly_throughput_dataset completed_issues:, label:, color:, dashed: false, label_hint: nil
91
+ periods = @issue_periods&.values&.any? ? calculate_custom_periods : calculate_time_periods
69
92
  result = {
70
93
  label: label,
71
- data: throughput_dataset(periods: calculate_time_periods, completed_issues: completed_issues),
94
+ label_hint: label_hint,
95
+ data: throughput_dataset(
96
+ periods: periods, completed_issues: completed_issues, label_hint: label_hint
97
+ ),
72
98
  fill: false,
73
99
  showLine: true,
74
100
  borderColor: color,
@@ -79,20 +105,44 @@ class ThroughputChart < ChartBase
79
105
  result
80
106
  end
81
107
 
82
- def throughput_dataset periods:, completed_issues:
108
+ def throughput_forecaster_url
109
+ params = {
110
+ throughputMode: 'data',
111
+ samplesText: @throughput_samples.join(','),
112
+ storyLow: @not_started_count,
113
+ storyHigh: @not_started_count
114
+ }
115
+
116
+ query = params.map { |k, v| "#{k}=#{CGI.escape(v.to_s)}" }.join('&')
117
+ "https://focusedobjective.com/throughput?#{query}"
118
+ end
119
+
120
+ def throughput_dataset periods:, completed_issues:, label_hint: nil
121
+ custom_mode = @issue_periods&.values&.any?
83
122
  periods.collect do |period|
84
123
  closed_issues = completed_issues.filter_map do |issue|
85
- stop_date = issue.board.cycletime.started_stopped_dates(issue).last
86
- [stop_date, issue] if stop_date && period.include?(stop_date)
124
+ stop_date = issue.started_stopped_dates.last
125
+ next unless stop_date
126
+
127
+ if custom_mode
128
+ [stop_date, issue] if @issue_periods[issue] == period.end
129
+ elsif period.include?(stop_date)
130
+ [stop_date, issue]
131
+ end
87
132
  end
88
133
 
89
134
  date_label = "on #{period.end}"
90
135
  date_label = "between #{period.begin} and #{period.end}" unless period.begin == period.end
91
136
 
92
- { y: closed_issues.size,
137
+ with_label_hint = label_hint ? " with #{label_hint}" : ''
138
+ {
139
+ y: closed_issues.size,
93
140
  x: "#{period.end}T23:59:59",
94
- title: ["#{closed_issues.size} items completed #{date_label}"] +
95
- closed_issues.collect { |_stop_date, issue| "#{issue.key} : #{issue.summary}" }
141
+ title: ["#{closed_issues.size} items closed#{with_label_hint} #{date_label}"] +
142
+ closed_issues.collect do |_stop_date, issue|
143
+ hint = @issue_hints&.fetch(issue, nil)
144
+ "#{issue.key} : #{issue.summary}#{" #{hint}" if hint}"
145
+ end
96
146
  }
97
147
  end
98
148
  end
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'jirametrics/groupable_issue_chart'
4
+
5
+ class TimeBasedHistogram < ChartBase
6
+ include GroupableIssueChart
7
+
8
+ attr_reader :show_stats
9
+
10
+ def initialize
11
+ super
12
+
13
+ percentiles [50, 85, 98]
14
+ @show_stats = true
15
+ end
16
+
17
+ def percentiles percs = nil
18
+ @percentiles = percs unless percs.nil?
19
+ @percentiles
20
+ end
21
+
22
+ def disable_stats
23
+ @show_stats = false
24
+ end
25
+
26
+ def run
27
+ histogram_items = all_items
28
+ rules_to_items = group_issues histogram_items
29
+
30
+ the_stats = {}
31
+
32
+ overall_histogram = histogram_data_for(items: histogram_items).transform_values(&:size)
33
+ the_stats[:all] = stats_for histogram_data: overall_histogram, percentiles: @percentiles
34
+ data_sets = rules_to_items.keys.collect do |rules|
35
+ the_label = rules.label
36
+ the_histogram = histogram_data_for(items: rules_to_items[rules])
37
+ if @show_stats
38
+ the_stats[the_label] = stats_for(
39
+ histogram_data: the_histogram.transform_values(&:size), percentiles: @percentiles
40
+ )
41
+ end
42
+
43
+ data_set_for(
44
+ histogram_data: the_histogram,
45
+ label: the_label,
46
+ color: rules.color
47
+ )
48
+ end
49
+
50
+ if data_sets.empty?
51
+ return "<h1 class='foldable'>#{@header_text}</h1>" \
52
+ '<div>No data matched the selected criteria. Nothing to show.</div>'
53
+ end
54
+
55
+ wrap_and_render(binding, __FILE__)
56
+ end
57
+
58
+ def histogram_data_for items:
59
+ items_hash = {}
60
+ items.each do |item|
61
+ days = value_for_item item
62
+ (items_hash[days] ||= []) << item if days.positive?
63
+ end
64
+ items_hash
65
+ end
66
+
67
+ def stats_for histogram_data:, percentiles:
68
+ return {} if histogram_data.empty?
69
+
70
+ total_values = histogram_data.values.sum
71
+
72
+ # Calculate the average
73
+ weighted_sum = histogram_data.reduce(0) { |sum, (value, frequency)| sum + (value * frequency) }
74
+ average = total_values.zero? ? 0 : weighted_sum.to_f / total_values
75
+
76
+ # Find the mode (or modes!) and the spread of the distribution
77
+ sorted_histogram = histogram_data.sort_by { |_value, frequency| frequency }
78
+ max_freq = sorted_histogram[-1][1]
79
+ mode = sorted_histogram.select { |_v, f| f == max_freq }
80
+
81
+ minmax = histogram_data.keys.minmax
82
+
83
+ # Calculate percentiles
84
+ sorted_values = histogram_data.keys.sort
85
+ cumulative_counts = {}
86
+ cumulative_sum = 0
87
+
88
+ sorted_values.each do |value|
89
+ cumulative_sum += histogram_data[value]
90
+ cumulative_counts[value] = cumulative_sum
91
+ end
92
+
93
+ percentile_results = {}
94
+ percentiles.each do |percentile|
95
+ rank = (percentile / 100.0) * total_values
96
+ percentile_value = sorted_values.find { |value| cumulative_counts[value] >= rank }
97
+ percentile_results[percentile] = percentile_value
98
+ end
99
+
100
+ {
101
+ average: average,
102
+ mode: mode.collect(&:first).sort,
103
+ min: minmax[0],
104
+ max: minmax[1],
105
+ percentiles: percentile_results
106
+ }
107
+ end
108
+
109
+ def sort_items items
110
+ items
111
+ end
112
+
113
+ def label_for_item item, hint:
114
+ raise NotImplementedError, "#{self.class} must implement label_for_item"
115
+ end
116
+
117
+ def data_set_for histogram_data:, label:, color:
118
+ {
119
+ type: 'bar',
120
+ label: label,
121
+ data: histogram_data.keys.sort.filter_map do |days|
122
+ items = histogram_data[days]
123
+ next if items.empty?
124
+
125
+ {
126
+ x: days,
127
+ y: items.size,
128
+ title: [title_for_item(count: items.size, value: days)] +
129
+ sort_items(items).collect do |item|
130
+ hint = @issue_hints&.fetch(item, nil)
131
+ label_for_item(item, hint: hint)
132
+ end
133
+ }
134
+ end,
135
+ backgroundColor: color,
136
+ borderRadius: 0
137
+ }
138
+ end
139
+ end
@@ -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
data/lib/jirametrics.rb CHANGED
@@ -52,6 +52,34 @@ class JiraMetrics < Thor
52
52
  Exporter.instance.info(key, name_filter: options[:name] || '*')
53
53
  end
54
54
 
55
+ option :config
56
+ option :name
57
+ desc 'mcp', 'Start in MCP (Model Context Protocol) server mode'
58
+ def mcp
59
+ load_config options[:config]
60
+ require 'jirametrics/mcp_server'
61
+
62
+ Exporter.instance.file_system.log_only = true
63
+
64
+ projects = {}
65
+ Exporter.instance.each_project_config(name_filter: options[:name] || '*') do |project|
66
+ project.evaluate_next_level
67
+ project.run load_only: true
68
+ projects[project.name || 'default'] = {
69
+ issues: project.issues,
70
+ today: project.time_range.end.to_date,
71
+ end_time: project.time_range.end
72
+ }
73
+ rescue StandardError => e
74
+ next if e.message.start_with? 'This is an aggregated project'
75
+ next if e.message.start_with? 'No data found'
76
+
77
+ raise
78
+ end
79
+
80
+ McpServer.new(projects: projects, timezone_offset: Exporter.instance.timezone_offset).run
81
+ end
82
+
55
83
  option :config
56
84
  desc 'stitch', 'Dump information about one issue'
57
85
  def stitch stitch_file = 'stitcher.erb'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jirametrics
3
3
  version: !ruby/object:Gem::Version
4
- version: '2.22'
4
+ version: '2.27'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Bowler
@@ -9,6 +9,34 @@ bindir: bin
9
9
  cert_chain: []
10
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
@@ -71,12 +99,15 @@ files:
71
99
  - lib/jirametrics/board.rb
72
100
  - lib/jirametrics/board_column.rb
73
101
  - lib/jirametrics/board_config.rb
102
+ - lib/jirametrics/board_feature.rb
74
103
  - lib/jirametrics/board_movement_calculator.rb
104
+ - lib/jirametrics/cfd_data_builder.rb
75
105
  - lib/jirametrics/change_item.rb
76
106
  - lib/jirametrics/chart_base.rb
77
107
  - lib/jirametrics/columns_config.rb
78
108
  - lib/jirametrics/css_variable.rb
79
- - lib/jirametrics/cycletime_config.rb
109
+ - lib/jirametrics/cumulative_flow_diagram.rb
110
+ - lib/jirametrics/cycle_time_config.rb
80
111
  - lib/jirametrics/cycletime_histogram.rb
81
112
  - lib/jirametrics/cycletime_scatterplot.rb
82
113
  - lib/jirametrics/daily_view.rb
@@ -100,6 +131,7 @@ files:
100
131
  - lib/jirametrics/file_system.rb
101
132
  - lib/jirametrics/fix_version.rb
102
133
  - lib/jirametrics/flow_efficiency_scatterplot.rb
134
+ - lib/jirametrics/github_gateway.rb
103
135
  - lib/jirametrics/groupable_issue_chart.rb
104
136
  - lib/jirametrics/grouping_rules.rb
105
137
  - lib/jirametrics/hierarchy_table.rb
@@ -107,8 +139,7 @@ files:
107
139
  - lib/jirametrics/html/aging_work_in_progress_chart.erb
108
140
  - lib/jirametrics/html/aging_work_table.erb
109
141
  - lib/jirametrics/html/collapsible_issues_panel.erb
110
- - lib/jirametrics/html/cycletime_histogram.erb
111
- - lib/jirametrics/html/cycletime_scatterplot.erb
142
+ - lib/jirametrics/html/cumulative_flow_diagram.erb
112
143
  - lib/jirametrics/html/daily_wip_chart.erb
113
144
  - lib/jirametrics/html/estimate_accuracy_chart.erb
114
145
  - lib/jirametrics/html/expedited_chart.erb
@@ -119,13 +150,21 @@ files:
119
150
  - lib/jirametrics/html/index.js
120
151
  - lib/jirametrics/html/sprint_burndown.erb
121
152
  - lib/jirametrics/html/throughput_chart.erb
153
+ - lib/jirametrics/html/time_based_histogram.erb
154
+ - lib/jirametrics/html/time_based_scatterplot.erb
122
155
  - lib/jirametrics/html_generator.rb
123
156
  - lib/jirametrics/html_report_config.rb
124
157
  - lib/jirametrics/issue.rb
125
158
  - lib/jirametrics/issue_collection.rb
126
159
  - lib/jirametrics/issue_link.rb
160
+ - lib/jirametrics/issue_printer.rb
127
161
  - lib/jirametrics/jira_gateway.rb
162
+ - lib/jirametrics/mcp_server.rb
128
163
  - lib/jirametrics/project_config.rb
164
+ - lib/jirametrics/pull_request.rb
165
+ - lib/jirametrics/pull_request_cycle_time_histogram.rb
166
+ - lib/jirametrics/pull_request_cycle_time_scatterplot.rb
167
+ - lib/jirametrics/pull_request_review.rb
129
168
  - lib/jirametrics/raw_javascript.rb
130
169
  - lib/jirametrics/rules.rb
131
170
  - lib/jirametrics/self_or_issue_dispatcher.rb
@@ -136,7 +175,10 @@ files:
136
175
  - lib/jirametrics/status.rb
137
176
  - lib/jirametrics/status_collection.rb
138
177
  - lib/jirametrics/stitcher.rb
178
+ - lib/jirametrics/throughput_by_completed_resolution_chart.rb
139
179
  - lib/jirametrics/throughput_chart.rb
180
+ - lib/jirametrics/time_based_histogram.rb
181
+ - lib/jirametrics/time_based_scatterplot.rb
140
182
  - lib/jirametrics/tree_organizer.rb
141
183
  - lib/jirametrics/trend_line_calculator.rb
142
184
  - lib/jirametrics/user.rb
@@ -163,7 +205,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
163
205
  - !ruby/object:Gem::Version
164
206
  version: '0'
165
207
  requirements: []
166
- rubygems_version: 3.6.9
208
+ rubygems_version: 4.0.8
167
209
  specification_version: 4
168
210
  summary: Extract Jira metrics
169
211
  test_files: []