jirametrics 2.4 → 2.11

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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/lib/jirametrics/aggregate_config.rb +9 -4
  3. data/lib/jirametrics/aging_work_bar_chart.rb +13 -11
  4. data/lib/jirametrics/aging_work_in_progress_chart.rb +105 -41
  5. data/lib/jirametrics/aging_work_table.rb +54 -7
  6. data/lib/jirametrics/blocked_stalled_change.rb +1 -1
  7. data/lib/jirametrics/board.rb +44 -15
  8. data/lib/jirametrics/board_config.rb +7 -3
  9. data/lib/jirametrics/board_movement_calculator.rb +147 -0
  10. data/lib/jirametrics/change_item.rb +19 -6
  11. data/lib/jirametrics/chart_base.rb +63 -27
  12. data/lib/jirametrics/css_variable.rb +1 -1
  13. data/lib/jirametrics/cycletime_config.rb +59 -8
  14. data/lib/jirametrics/cycletime_histogram.rb +68 -3
  15. data/lib/jirametrics/cycletime_scatterplot.rb +3 -6
  16. data/lib/jirametrics/daily_wip_by_age_chart.rb +2 -4
  17. data/lib/jirametrics/daily_wip_by_blocked_stalled_chart.rb +2 -2
  18. data/lib/jirametrics/daily_wip_by_parent_chart.rb +0 -4
  19. data/lib/jirametrics/daily_wip_chart.rb +7 -9
  20. data/lib/jirametrics/data_quality_report.rb +219 -41
  21. data/lib/jirametrics/dependency_chart.rb +37 -10
  22. data/lib/jirametrics/download_config.rb +12 -0
  23. data/lib/jirametrics/downloader.rb +68 -50
  24. data/lib/jirametrics/estimate_accuracy_chart.rb +1 -2
  25. data/lib/jirametrics/examples/aggregated_project.rb +7 -21
  26. data/lib/jirametrics/examples/standard_project.rb +18 -34
  27. data/lib/jirametrics/expedited_chart.rb +8 -9
  28. data/lib/jirametrics/exporter.rb +28 -11
  29. data/lib/jirametrics/file_config.rb +23 -6
  30. data/lib/jirametrics/file_system.rb +39 -3
  31. data/lib/jirametrics/flow_efficiency_scatterplot.rb +111 -0
  32. data/lib/jirametrics/groupable_issue_chart.rb +1 -3
  33. data/lib/jirametrics/html/aging_work_bar_chart.erb +3 -12
  34. data/lib/jirametrics/html/aging_work_in_progress_chart.erb +22 -5
  35. data/lib/jirametrics/html/aging_work_table.erb +6 -4
  36. data/lib/jirametrics/html/cycletime_histogram.erb +74 -0
  37. data/lib/jirametrics/html/cycletime_scatterplot.erb +1 -10
  38. data/lib/jirametrics/html/daily_wip_chart.erb +1 -10
  39. data/lib/jirametrics/html/expedited_chart.erb +1 -10
  40. data/lib/jirametrics/html/flow_efficiency_scatterplot.erb +85 -0
  41. data/lib/jirametrics/html/hierarchy_table.erb +1 -1
  42. data/lib/jirametrics/html/index.css +28 -5
  43. data/lib/jirametrics/html/index.erb +8 -4
  44. data/lib/jirametrics/html/sprint_burndown.erb +1 -10
  45. data/lib/jirametrics/html/throughput_chart.erb +1 -10
  46. data/lib/jirametrics/html_report_config.rb +33 -23
  47. data/lib/jirametrics/issue.rb +232 -47
  48. data/lib/jirametrics/jira_gateway.rb +16 -3
  49. data/lib/jirametrics/project_config.rb +245 -134
  50. data/lib/jirametrics/rules.rb +2 -2
  51. data/lib/jirametrics/self_or_issue_dispatcher.rb +2 -0
  52. data/lib/jirametrics/settings.json +5 -2
  53. data/lib/jirametrics/sprint_burndown.rb +3 -3
  54. data/lib/jirametrics/status.rb +84 -19
  55. data/lib/jirametrics/status_collection.rb +77 -39
  56. data/lib/jirametrics/throughput_chart.rb +1 -1
  57. data/lib/jirametrics/value_equality.rb +2 -2
  58. data/lib/jirametrics.rb +22 -6
  59. metadata +10 -13
  60. data/lib/jirametrics/discard_changes_before.rb +0 -37
  61. data/lib/jirametrics/html/data_quality_report.erb +0 -126
@@ -3,30 +3,65 @@
3
3
  require 'jirametrics/value_equality'
4
4
 
5
5
  class Status
6
- include ValueEquality
7
- attr_reader :id, :category_name, :category_id, :project_id
6
+ attr_reader :id, :project_id, :category
8
7
  attr_accessor :name
9
8
 
10
- def initialize name: nil, id: nil, category_name: nil, category_id: nil, project_id: nil, raw: nil
11
- @name = name
12
- @id = id
13
- @category_name = category_name
14
- @category_id = category_id
15
- @project_id = project_id
9
+ class Category
10
+ attr_reader :id, :name, :key
11
+
12
+ def initialize id:, name:, key:
13
+ @id = id
14
+ @name = name
15
+ @key = key
16
+ end
16
17
 
17
- return unless raw
18
+ def to_s
19
+ "#{name.inspect}:#{id.inspect}"
20
+ end
18
21
 
19
- @raw = raw
20
- @name = raw['name']
21
- @id = raw['id'].to_i
22
+ def <=> other
23
+ id <=> other.id
24
+ end
25
+
26
+ def == other
27
+ id == other.id
28
+ end
29
+
30
+ def eql?(other) = id.eql?(other.id)
31
+ def hash = id.hash
32
+
33
+ def new? = (@key == 'new')
34
+ def indeterminate? = (@key == 'indeterminate')
35
+ def done? = (@key == 'done')
36
+ end
37
+
38
+ def self.from_raw raw
39
+ raise "raw cannot be nil" if raw.nil?
22
40
 
23
41
  category_config = raw['statusCategory']
24
- @category_name = category_config['name']
25
- @category_id = category_config['id'].to_i
42
+ raise "statusCategory can't be nil in #{category_config.inspect}" if category_config.nil?
43
+
44
+ Status.new(
45
+ name: raw['name'],
46
+ id: raw['id'].to_i,
47
+ category_name: category_config['name'],
48
+ category_id: category_config['id'].to_i,
49
+ category_key: category_config['key'],
50
+ project_id: raw['scope']&.[]('project')&.[]('id'),
51
+ artificial: false
52
+ )
53
+ end
54
+
55
+ def initialize name:, id:, category_name:, category_id:, category_key:, project_id: nil, artificial: true
56
+ # These checks are needed because nils used to be possible and now they aren't.
57
+ raise 'id cannot be nil' if id.nil?
58
+ raise 'category_id cannot be nil' if category_id.nil?
26
59
 
27
- # If this is a NextGen project then this status may be project specific. When this field is
28
- # nil then the status is global.
29
- @project_id = raw['scope']&.[]('project')&.[]('id')
60
+ @name = name
61
+ @id = id
62
+ @category = Category.new id: category_id, name: category_name, key: category_key
63
+ @project_id = project_id
64
+ @artificial = artificial
30
65
  end
31
66
 
32
67
  def project_scoped?
@@ -38,8 +73,38 @@ class Status
38
73
  end
39
74
 
40
75
  def to_s
41
- "Status(name=#{@name.inspect}, id=#{@id.inspect}," \
42
- " category_name=#{@category_name.inspect}, category_id=#{@category_id.inspect}, project_id=#{@project_id})"
76
+ "#{name.inspect}:#{id.inspect}"
77
+ end
78
+
79
+ def artificial?
80
+ @artificial
81
+ end
82
+
83
+ def == other
84
+ return false unless other.is_a? Status
85
+
86
+ @id == other.id && @name == other.name && @category.id == other.category.id && @category.name == other.category.name
87
+ end
88
+
89
+ def eql?(other)
90
+ self == other
91
+ end
92
+
93
+ def <=> other
94
+ result = @name.casecmp(other.name)
95
+ result = @id <=> other.id if result.zero?
96
+ result
97
+ end
98
+
99
+ def inspect
100
+ result = []
101
+ result << "Status(name: #{@name.inspect}"
102
+ result << "id: #{@id.inspect}"
103
+ result << "project_id: #{@project_id}" if @project_id
104
+ category = self.category
105
+ result << "category: {name:#{category.name.inspect}, id: #{category.id.inspect}, key: #{category.key.inspect}}"
106
+ result << 'artificial' if artificial?
107
+ result.join(', ') << ')'
43
108
  end
44
109
 
45
110
  def value_equality_ignored_variables
@@ -4,68 +4,106 @@ class StatusNotFoundError < StandardError
4
4
  end
5
5
 
6
6
  class StatusCollection
7
+ attr_reader :historical_status_mappings
8
+
7
9
  def initialize
8
10
  @list = []
11
+ @historical_status_mappings = {} # 'name:id' => category
9
12
  end
10
13
 
11
- def filter_status_names category_name:, including: nil, excluding: nil
12
- including = expand_statuses including
13
- excluding = expand_statuses excluding
14
+ # Return the status matching this id or nil if it can't be found.
15
+ def find_by_id id
16
+ @list.find { |status| status.id == id }
17
+ end
14
18
 
15
- @list.filter_map do |status|
16
- keep = status.category_name == category_name ||
17
- including.any? { |s| s.name == status.name }
18
- keep = false if excluding.any? { |s| s.name == status.name }
19
+ def find_all_by_name identifier
20
+ name, id = parse_name_id identifier
19
21
 
20
- status.name if keep
21
- end
22
- end
22
+ if id
23
+ status = find_by_id id
24
+ return [] if status.nil?
23
25
 
24
- def expand_statuses names_or_ids
25
- result = []
26
- return result if names_or_ids.nil?
27
-
28
- names_or_ids = [names_or_ids] unless names_or_ids.is_a? Array
29
-
30
- names_or_ids.each do |name_or_id|
31
- status = @list.find { |s| s.name == name_or_id || s.id == name_or_id }
32
- if status.nil?
33
- if block_given?
34
- yield name_or_id
35
- next
36
- else
37
- all_status_names = @list.collect { |s| "#{s.name.inspect}:#{s.id.inspect}" }.uniq.sort.join(', ')
38
- raise StatusNotFoundError, "Status not found: \"#{name_or_id}\". Possible statuses are: #{all_status_names}"
39
- end
26
+ if name && status.name != name
27
+ raise "Specified status ID of #{id} does not match specified name #{name.inspect}. " \
28
+ "You might have meant one of these: #{self}."
40
29
  end
41
-
42
- result << status
30
+ [status]
31
+ else
32
+ @list.select { |status| status.name == name }
43
33
  end
44
- result
45
34
  end
46
35
 
47
- def todo including: nil, excluding: nil
48
- filter_status_names category_name: 'To Do', including: including, excluding: excluding
36
+ def find_all_categories
37
+ @list
38
+ .collect(&:category)
39
+ .uniq
40
+ .sort_by(&:id)
49
41
  end
50
42
 
51
- def in_progress including: nil, excluding: nil
52
- filter_status_names category_name: 'In Progress', including: including, excluding: excluding
43
+ def parse_name_id name
44
+ # Names could arrive in one of the following formats: "Done:3", "3", "Done"
45
+ if name =~ /^(.*):(\d+)$/
46
+ [$1, $2.to_i]
47
+ elsif name.match?(/^\d+$/)
48
+ [nil, name.to_i]
49
+ else
50
+ [name, nil]
51
+ end
53
52
  end
54
53
 
55
- def done including: nil, excluding: nil
56
- filter_status_names category_name: 'Done', including: including, excluding: excluding
57
- end
54
+ def find_all_categories_by_name identifier
55
+ key = nil
56
+ id = nil
58
57
 
59
- def find_by_name name
60
- find { |status| status.name == name }
58
+ if identifier.is_a? Symbol
59
+ key = identifier.to_s
60
+ else
61
+ name, id = parse_name_id identifier
62
+ end
63
+
64
+ find_all_categories.select { |c| c.id == id || c.name == name || c.key == key }
61
65
  end
62
66
 
63
- def find(&block)= @list.find(&block)
64
67
  def collect(&block) = @list.collect(&block)
68
+ def find(&block) = @list.find(&block)
65
69
  def each(&block) = @list.each(&block)
66
70
  def select(&block) = @list.select(&block)
67
71
  def <<(arg) = @list << arg
68
72
  def empty? = @list.empty?
69
73
  def clear = @list.clear
70
74
  def delete(object) = @list.delete(object)
75
+
76
+ def to_s
77
+ "[#{@list.sort.join(', ')}]"
78
+ end
79
+
80
+ def inspect
81
+ "StatusCollection#{self}"
82
+ end
83
+
84
+ def fabricate_status_for id:, name:
85
+ category = @historical_status_mappings["#{name.inspect}:#{id.inspect}"]
86
+ category = in_progress_category if category.nil?
87
+
88
+ status = Status.new(
89
+ name: name,
90
+ id: id,
91
+ category_name: category.name,
92
+ category_id: category.id,
93
+ category_key: category.key,
94
+ artificial: true
95
+ )
96
+ @list << status
97
+ status
98
+ end
99
+
100
+ private
101
+
102
+ # Return the in-progress category or raise an error if we can't find one.
103
+ def in_progress_category
104
+ first_in_progress_status = find { |s| s.category.indeterminate? }
105
+ raise "Can't find even one in-progress status in #{self}" unless first_in_progress_status
106
+
107
+ first_in_progress_status.category
108
+ end
71
109
  end
@@ -82,7 +82,7 @@ class ThroughputChart < ChartBase
82
82
  def throughput_dataset periods:, completed_issues:
83
83
  periods.collect do |period|
84
84
  closed_issues = completed_issues.filter_map do |issue|
85
- stop_date = issue.board.cycletime.stopped_time(issue)&.to_date
85
+ stop_date = issue.board.cycletime.started_stopped_dates(issue).last
86
86
  [stop_date, issue] if stop_date && period.include?(stop_date)
87
87
  end
88
88
 
@@ -9,9 +9,9 @@ module ValueEquality
9
9
  names = object.instance_variables
10
10
  if object.respond_to? :value_equality_ignored_variables
11
11
  ignored_variables = object.value_equality_ignored_variables
12
- names.reject! { |n| ignored_variables.include? n }
12
+ names.reject! { |n| ignored_variables.include? n.to_sym }
13
13
  end
14
- names.map { |variable| instance_variable_get variable }
14
+ names.map { |variable| object.instance_variable_get variable }
15
15
  end
16
16
 
17
17
  code.call(self) == code.call(other)
data/lib/jirametrics.rb CHANGED
@@ -3,9 +3,20 @@
3
3
  require 'thor'
4
4
 
5
5
  class JiraMetrics < Thor
6
+ def self.exit_on_failure?
7
+ true
8
+ end
9
+
10
+ map %w[--version -v] => :__print_version
11
+
12
+ desc '--version, -v', 'print the version'
13
+ def __print_version
14
+ puts Gem.loaded_specs['jirametrics'].version
15
+ end
16
+
6
17
  option :config
7
18
  option :name
8
- desc 'export only', "Export data into either reports or CSV's as per the configuration"
19
+ desc 'export', "Export data into either reports or CSV's as per the configuration"
9
20
  def export
10
21
  load_config options[:config]
11
22
  Exporter.instance.export(name_filter: options[:name] || '*')
@@ -13,7 +24,7 @@ class JiraMetrics < Thor
13
24
 
14
25
  option :config
15
26
  option :name
16
- desc 'download only', 'Download data from Jira'
27
+ desc 'download', 'Download data from Jira'
17
28
  def download
18
29
  load_config options[:config]
19
30
  Exporter.instance.download(name_filter: options[:name] || '*')
@@ -21,7 +32,7 @@ class JiraMetrics < Thor
21
32
 
22
33
  option :config
23
34
  option :name
24
- desc 'download and export', 'Same as running download, followed by export'
35
+ desc 'go', 'Same as running download, followed by export'
25
36
  def go
26
37
  load_config options[:config]
27
38
  Exporter.instance.download(name_filter: options[:name] || '*')
@@ -30,6 +41,13 @@ class JiraMetrics < Thor
30
41
  Exporter.instance.export(name_filter: options[:name] || '*')
31
42
  end
32
43
 
44
+ option :config
45
+ desc 'info', 'Dump information about one issue'
46
+ def info keys
47
+ load_config options[:config]
48
+ Exporter.instance.info(keys, name_filter: options[:name] || '*')
49
+ end
50
+
33
51
  private
34
52
 
35
53
  def load_config config_file
@@ -50,7 +68,6 @@ class JiraMetrics < Thor
50
68
  require 'jirametrics/grouping_rules'
51
69
  require 'jirametrics/daily_wip_chart'
52
70
  require 'jirametrics/groupable_issue_chart'
53
- require 'jirametrics/discard_changes_before'
54
71
  require 'jirametrics/css_variable'
55
72
 
56
73
  require 'jirametrics/aggregate_config'
@@ -69,6 +86,7 @@ class JiraMetrics < Thor
69
86
  require 'jirametrics/daily_wip_by_parent_chart'
70
87
  require 'jirametrics/aging_work_in_progress_chart'
71
88
  require 'jirametrics/cycletime_scatterplot'
89
+ require 'jirametrics/flow_efficiency_scatterplot'
72
90
  require 'jirametrics/sprint_issue_change_data'
73
91
  require 'jirametrics/cycletime_histogram'
74
92
  require 'jirametrics/daily_wip_by_blocked_stalled_chart'
@@ -97,6 +115,4 @@ class JiraMetrics < Thor
97
115
  require 'jirametrics/board'
98
116
  load config_file
99
117
  end
100
-
101
- # Dir.foreach('lib/jirametrics') {|file| puts "require 'jirametrics/#{$1}'" if file =~ /^(.+)\.rb$/}
102
118
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jirametrics
3
3
  version: !ruby/object:Gem::Version
4
- version: '2.4'
4
+ version: '2.11'
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: 2025-03-11 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: random-word
@@ -52,8 +51,7 @@ dependencies:
52
51
  - - "~>"
53
52
  - !ruby/object:Gem::Version
54
53
  version: 1.2.2
55
- description: Tool to extract metrics from Jira and export to either a report or to
56
- CSV files
54
+ description: Extract metrics from Jira and export to either a report or to CSV files
57
55
  email: mbowler@gargoylesoftware.com
58
56
  executables:
59
57
  - jirametrics
@@ -71,6 +69,7 @@ files:
71
69
  - lib/jirametrics/board.rb
72
70
  - lib/jirametrics/board_column.rb
73
71
  - lib/jirametrics/board_config.rb
72
+ - lib/jirametrics/board_movement_calculator.rb
74
73
  - lib/jirametrics/change_item.rb
75
74
  - lib/jirametrics/chart_base.rb
76
75
  - lib/jirametrics/columns_config.rb
@@ -84,7 +83,6 @@ files:
84
83
  - lib/jirametrics/daily_wip_chart.rb
85
84
  - lib/jirametrics/data_quality_report.rb
86
85
  - lib/jirametrics/dependency_chart.rb
87
- - lib/jirametrics/discard_changes_before.rb
88
86
  - lib/jirametrics/download_config.rb
89
87
  - lib/jirametrics/downloader.rb
90
88
  - lib/jirametrics/estimate_accuracy_chart.rb
@@ -95,6 +93,7 @@ files:
95
93
  - lib/jirametrics/file_config.rb
96
94
  - lib/jirametrics/file_system.rb
97
95
  - lib/jirametrics/fix_version.rb
96
+ - lib/jirametrics/flow_efficiency_scatterplot.rb
98
97
  - lib/jirametrics/groupable_issue_chart.rb
99
98
  - lib/jirametrics/grouping_rules.rb
100
99
  - lib/jirametrics/hierarchy_table.rb
@@ -105,9 +104,9 @@ files:
105
104
  - lib/jirametrics/html/cycletime_histogram.erb
106
105
  - lib/jirametrics/html/cycletime_scatterplot.erb
107
106
  - lib/jirametrics/html/daily_wip_chart.erb
108
- - lib/jirametrics/html/data_quality_report.erb
109
107
  - lib/jirametrics/html/estimate_accuracy_chart.erb
110
108
  - lib/jirametrics/html/expedited_chart.erb
109
+ - lib/jirametrics/html/flow_efficiency_scatterplot.erb
111
110
  - lib/jirametrics/html/hierarchy_table.erb
112
111
  - lib/jirametrics/html/index.css
113
112
  - lib/jirametrics/html/index.erb
@@ -130,15 +129,14 @@ files:
130
129
  - lib/jirametrics/tree_organizer.rb
131
130
  - lib/jirametrics/trend_line_calculator.rb
132
131
  - lib/jirametrics/value_equality.rb
133
- homepage: https://github.com/mikebowler/jirametrics
132
+ homepage: https://jirametrics.org
134
133
  licenses:
135
134
  - Apache-2.0
136
135
  metadata:
137
136
  rubygems_mfa_required: 'true'
138
137
  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:
138
+ changelog_uri: https://jirametrics.org/changes
139
+ documentation_uri: https://jirametrics.org
142
140
  rdoc_options: []
143
141
  require_paths:
144
142
  - lib
@@ -153,8 +151,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
153
151
  - !ruby/object:Gem::Version
154
152
  version: '0'
155
153
  requirements: []
156
- rubygems_version: 3.5.15
157
- signing_key:
154
+ rubygems_version: 3.6.2
158
155
  specification_version: 4
159
156
  summary: Extract Jira metrics
160
157
  test_files: []
@@ -1,37 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module DiscardChangesBefore
4
- def discard_changes_before status_becomes: nil, &block
5
- if status_becomes
6
- status_becomes = [status_becomes] unless status_becomes.is_a? Array
7
-
8
- block = lambda do |issue|
9
- trigger_statuses = status_becomes.collect do |status_name|
10
- if status_name == :backlog
11
- issue.board.backlog_statuses.collect(&:name)
12
- else
13
- status_name
14
- end
15
- end.flatten
16
-
17
- time = nil
18
- issue.changes.each do |change|
19
- time = change.time if change.status? && trigger_statuses.include?(change.value) && change.artificial? == false
20
- end
21
- time
22
- end
23
- end
24
-
25
- issues_cutoff_times = []
26
- issues.each do |issue|
27
- cutoff_time = block.call(issue)
28
- issues_cutoff_times << [issue, cutoff_time] if cutoff_time
29
- end
30
-
31
- discard_changes_before_hook issues_cutoff_times
32
-
33
- issues_cutoff_times.each do |issue, cutoff_time|
34
- issue.changes.reject! { |change| change.status? && change.time <= cutoff_time && change.artificial? == false }
35
- end
36
- end
37
- end
@@ -1,126 +0,0 @@
1
- <%
2
- problems = problems_for :discarded_changes
3
- unless problems.empty?
4
- %>
5
- <p>
6
- <span class="quality_note_bullet">⮕</span> <%= label_issues problems.size %> have had information discarded. This configuration is set
7
- to "reset the clock" if an item is moved back to the backlog after it's been started. This hides important
8
- information and makes the data less accurate. <b>Moving items back to the backlog is strongly discouraged.</b>
9
- <%= collapsible_issues_panel problems %>
10
- </p>
11
- <%
12
- end
13
- %>
14
-
15
- <%
16
- problems = problems_for :completed_but_not_started
17
- unless problems.empty?
18
- percentage_work_included = ((issues.size - problems.size).to_f / issues.size * 100).to_i
19
- %>
20
- <p>
21
- <span class="quality_note_bullet">⮕</span> <%= label_issues problems.size %> were discarded from all charts using cycletime (scatterplot, histogram, etc) as we couldn't determine when they started.
22
- <% if percentage_work_included < 85 %>
23
- Consider whether looking at only <%= percentage_work_included %>% of the total data points is enough to come to any reasonable conclusions. See <a href="https://en.wikipedia.org/wiki/Survivorship_bias">Survivorship Bias</a>.
24
- <% end %>
25
- <%= collapsible_issues_panel problems %>
26
- </p>
27
- <%
28
- end
29
- %>
30
-
31
- <%
32
- problems = problems_for :status_changes_after_done
33
- unless problems.empty?
34
- %>
35
- <p>
36
- <span class="quality_note_bullet">⮕</span> <%= label_issues problems.size %> had a status change after being identified as done. We should question whether they were really done at that point or if we stopped the clock too early.
37
- <%= collapsible_issues_panel problems %>
38
- </p>
39
- <%
40
- end
41
- %>
42
-
43
- <%
44
- problems = problems_for :backwards_through_status_categories
45
- unless problems.empty?
46
- %>
47
- <p>
48
- <span class="quality_note_bullet">⮕</span> <%= label_issues problems.size %> moved backwards across the board, <b>crossing status categories</b>. This will almost certainly have impacted timings as the end times are often taken at status category boundaries. You should assume that any timing measurements for this item are wrong.
49
- <%= collapsible_issues_panel problems %>
50
- </p>
51
- <%
52
- end
53
- %>
54
-
55
- <%
56
- problems = problems_for :backwords_through_statuses
57
- unless problems.empty?
58
- %>
59
- <p>
60
- <span class="quality_note_bullet">⮕</span> <%= label_issues problems.size %> moved backwards across the board. Depending where we have set the start and end points, this may give us incorrect timing data. Note that these items did not cross a status category and may not have affected metrics.
61
- <%= collapsible_issues_panel problems %>
62
- </p>
63
- <%
64
- end
65
- %>
66
-
67
- <%
68
- problems = problems_for :status_not_on_board
69
- unless problems.empty?
70
- %>
71
- <p>
72
- <span class="quality_note_bullet">⮕</span> <%= label_issues problems.size %> were not visible on the board for some period of time. This may impact timings as the work was likely to have been forgotten if it wasn't visible.
73
- <%= collapsible_issues_panel problems %>
74
- </p>
75
- <%
76
- end
77
- %>
78
-
79
- <%
80
- problems = problems_for :created_in_wrong_status
81
- unless problems.empty?
82
- %>
83
- <p>
84
- <span class="quality_note_bullet">⮕</span> <%= label_issues problems.size %> were created in a status not designated as Backlog. This will impact the measurement of start times and will therefore impact whether it's shown as in progress or not.
85
- <%= collapsible_issues_panel problems %>
86
- </p>
87
- <%
88
- end
89
- %>
90
-
91
- <%
92
- problems = problems_for :stopped_before_started
93
- unless problems.empty?
94
- %>
95
- <p>
96
- <span class="quality_note_bullet">⮕</span> <%= label_issues problems.size %> were stopped before they were started and this will play havoc with any cycletime or WIP calculations. The most common case for this is when an item gets closed and then moved back into an in-progress status.
97
- <%= collapsible_issues_panel problems %>
98
- </p>
99
- <%
100
- end
101
- %>
102
-
103
- <%
104
- problems = problems_for :issue_not_started_but_subtasks_have
105
- unless problems.empty?
106
- %>
107
- <p>
108
- <span class="quality_note_bullet">⮕</span> <%= label_issues problems.size %> still showing 'not started' while sub-tasks underneath them have started. This is almost always a mistake; if we're working on subtasks, the top level
109
- item should also have started.
110
- <%= collapsible_issues_panel problems %>
111
- </p>
112
- <%
113
- end
114
- %>
115
-
116
- <%
117
- problems = problems_for :issue_on_multiple_boards
118
- unless problems.empty?
119
- %>
120
- <p>
121
- <span class="quality_note_bullet">⮕</span> For <%= label_issues problems.size %>, we have an issue that shows up on more than one board. This could result in more data points showing up on a chart then there really should be.
122
- <%= collapsible_issues_panel problems, :hide_board_column %>
123
- </p>
124
- <%
125
- end
126
- %>