Almirah 0.4.2 → 0.4.4

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 (54) hide show
  1. checksums.yaml +4 -4
  2. data/bin/almirah +2 -2
  3. data/lib/almirah/doc_fabric.rb +25 -0
  4. data/lib/almirah/doc_items/blockquote.rb +15 -16
  5. data/lib/almirah/doc_items/code_block.rb +19 -21
  6. data/lib/almirah/doc_items/controlled_paragraph.rb +4 -4
  7. data/lib/almirah/doc_items/controlled_table.rb +11 -11
  8. data/lib/almirah/doc_items/controlled_table_row.rb +15 -18
  9. data/lib/almirah/doc_items/doc_footer.rb +10 -13
  10. data/lib/almirah/doc_items/doc_item.rb +2 -2
  11. data/lib/almirah/doc_items/heading.rb +1 -1
  12. data/lib/almirah/doc_items/image.rb +25 -27
  13. data/lib/almirah/doc_items/markdown_list.rb +2 -2
  14. data/lib/almirah/doc_items/markdown_table.rb +9 -2
  15. data/lib/almirah/doc_items/scope_table.rb +188 -0
  16. data/lib/almirah/doc_items/text_line.rb +26 -22
  17. data/lib/almirah/doc_items/todo_block.rb +15 -16
  18. data/lib/almirah/doc_items/work_item.rb +129 -0
  19. data/lib/almirah/doc_parser.rb +15 -8
  20. data/lib/almirah/doc_types/base_document.rb +31 -7
  21. data/lib/almirah/doc_types/coverage.rb +3 -3
  22. data/lib/almirah/doc_types/critical_chain_page.rb +218 -0
  23. data/lib/almirah/doc_types/decision.rb +152 -7
  24. data/lib/almirah/doc_types/decision_grouping.rb +17 -0
  25. data/lib/almirah/doc_types/decisions_overview.rb +591 -31
  26. data/lib/almirah/doc_types/implementation.rb +98 -98
  27. data/lib/almirah/doc_types/index.rb +11 -12
  28. data/lib/almirah/doc_types/persistent_document.rb +1 -1
  29. data/lib/almirah/doc_types/planning_dates.rb +17 -0
  30. data/lib/almirah/doc_types/protocol.rb +16 -20
  31. data/lib/almirah/doc_types/risk_record.rb +77 -0
  32. data/lib/almirah/doc_types/risk_registry_page.rb +141 -0
  33. data/lib/almirah/doc_types/risks_overview.rb +102 -0
  34. data/lib/almirah/doc_types/rpn_rendering.rb +23 -0
  35. data/lib/almirah/doc_types/source_file.rb +1 -1
  36. data/lib/almirah/doc_types/traceability.rb +124 -133
  37. data/lib/almirah/dom/doc_section.rb +1 -1
  38. data/lib/almirah/navigation_pane.rb +9 -13
  39. data/lib/almirah/project/critical_chain.rb +117 -0
  40. data/lib/almirah/project/doc_linker.rb +4 -4
  41. data/lib/almirah/project/fever_chart.rb +94 -0
  42. data/lib/almirah/project/project_data.rb +17 -2
  43. data/lib/almirah/project/work_item_scheduler.rb +167 -0
  44. data/lib/almirah/project/working_calendar.rb +112 -0
  45. data/lib/almirah/project.rb +307 -9
  46. data/lib/almirah/project_configuration.rb +176 -29
  47. data/lib/almirah/project_template.rb +6 -6
  48. data/lib/almirah/project_utility.rb +3 -5
  49. data/lib/almirah/search/specifications_db.rb +2 -2
  50. data/lib/almirah/source_file_parser.rb +2 -3
  51. data/lib/almirah/templates/css/main.css +173 -1
  52. data/lib/almirah/templates/scripts/main.js +3 -1
  53. data/lib/almirah.rb +1 -2
  54. metadata +14 -1
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_document'
4
+ require_relative 'rpn_rendering'
5
+
6
+ # The all-registries summary page (ADR-219): build/risks/overview.html, the
7
+ # target of the top-menu Risks button. One table row per registry, in
8
+ # file-system order, with the columns Risk Registry (linked to the registry
9
+ # page), Total Risks, Open Risks, Highest RPN and Average RPN — the RPN
10
+ # aggregates computed over the registry's leading RPN group, ignoring records
11
+ # whose group value is blank. The Risk Registry cell shows the registry
12
+ # preface's frontmatter title, falling back to the folder name (ENH-221).
13
+ class RisksOverview < BaseDocument
14
+ include RpnRendering
15
+
16
+ OPEN_EXCLUDED_STATUS = 'Closed'
17
+
18
+ attr_accessor :registries, :configuration, :prefaces
19
+
20
+ # `registries` is the ordered list of [name, records] pairs; `prefaces`
21
+ # maps a registry name to its parsed overview.md, when it has one.
22
+ def initialize(registries, configuration, prefaces = {})
23
+ super()
24
+ @registries = registries
25
+ @configuration = configuration
26
+ @prefaces = prefaces
27
+ @id = 'overview'
28
+ @title = 'Risk Registries'
29
+ end
30
+
31
+ def to_console
32
+ puts "\e[36mRisks Overview: #{@id}\e[0m"
33
+ end
34
+
35
+ def to_html(output_file_path)
36
+ html_rows = ['']
37
+ html_rows.append "<h1>#{@title}</h1>\n"
38
+ html_rows.append render_registries_table
39
+ save_html_to_file(html_rows, nil, output_file_path)
40
+ end
41
+
42
+ private
43
+
44
+ def render_registries_table
45
+ s = "<table class=\"controlled risks_overview\">\n"
46
+ s += "\t<thead>\n"
47
+ s += "\t\t<th>Risk Registry</th>\n"
48
+ s += "\t\t<th>Total Risks</th>\n"
49
+ s += "\t\t<th>Open Risks</th>\n"
50
+ s += "\t\t<th>Highest RPN</th>\n"
51
+ s += "\t\t<th>Average RPN</th>\n"
52
+ s += "</thead>\n"
53
+ @registries.each { |name, records| s += render_registry_row(name, records) }
54
+ s + "</table>\n"
55
+ end
56
+
57
+ def render_registry_row(name, records)
58
+ group = @configuration.get_risk_rpn_groups(name).first
59
+ values = group ? records.filter_map { |r| r.rpn_value(group) } : []
60
+ s = "\t<tr>\n"
61
+ s += "\t\t<td class=\"item_text\" style='padding: 5px;'>\
62
+ <a name=\"#{name}\" id=\"#{name}\" href=\"./#{name}/overview.html\" class=\"external\" \
63
+ title=\"Risk Registry\">#{registry_title(name)}</a></td>\n"
64
+ s += "\t\t<td class=\"item_rpn\">#{records.length}</td>\n"
65
+ s += "\t\t<td class=\"item_rpn\">#{open_count(records)}</td>\n"
66
+ s += render_highest_cell(values, group)
67
+ s += render_average_cell(values)
68
+ s + "\t</tr>\n"
69
+ end
70
+
71
+ # The registry preface's frontmatter title (ENH-221) — the same source the
72
+ # registry page heading uses — or the folder name when the registry has no
73
+ # preface or the preface carries no title.
74
+ def registry_title(name)
75
+ params = @prefaces[name]&.frontmatter&.parameters
76
+ (params && params['title']) || name
77
+ end
78
+
79
+ # Every record whose marked status is not Closed counts as open — including
80
+ # records with no current-status marker (ADR-219 keeps the count honest to
81
+ # the marker; another terminal status is the registry preface's to document).
82
+ def open_count(records)
83
+ records.count { |r| r.current_status.to_s.strip != OPEN_EXCLUDED_STATUS }
84
+ end
85
+
86
+ # The worst risk's verdict carries up: the cell keeps the leading group's
87
+ # threshold colouring. Blank without a group or computable values.
88
+ def render_highest_cell(values, group)
89
+ return "\t\t<td class=\"item_rpn\"></td>\n" if values.empty?
90
+
91
+ highest = values.max
92
+ classes = ['item_rpn', rpn_threshold_class(highest, group)].compact.join(' ')
93
+ "\t\t<td class=\"#{classes}\">#{format_rpn(highest)}</td>\n"
94
+ end
95
+
96
+ def render_average_cell(values)
97
+ return "\t\t<td class=\"item_rpn\"></td>\n" if values.empty?
98
+
99
+ average = (values.sum.to_f / values.length).round(1)
100
+ "\t\t<td class=\"item_rpn\">#{format_rpn(average)}</td>\n"
101
+ end
102
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Shared RPN cell presentation (ADR-217, reused by the registries summary of
4
+ # ADR-219): the threshold band class and the integer-when-whole formatting.
5
+ module RpnRendering
6
+ # Acceptable at or below the acceptable bound, unacceptable at or above the
7
+ # unacceptable bound, caution between them (the ALARP band); a lone bound
8
+ # leaves the rest of the range as caution. nil when no thresholds configured.
9
+ def rpn_threshold_class(value, group)
10
+ if group[:acceptable] && value <= group[:acceptable]
11
+ 'rpn_acceptable'
12
+ elsif group[:unacceptable] && value >= group[:unacceptable]
13
+ 'rpn_unacceptable'
14
+ elsif group[:acceptable] || group[:unacceptable]
15
+ 'rpn_caution'
16
+ end
17
+ end
18
+
19
+ # Whole values print as integers (8 * 3 -> 24, not 24.0).
20
+ def format_rpn(value)
21
+ value == value.to_i ? value.to_i.to_s : value.to_s
22
+ end
23
+ end
@@ -63,7 +63,7 @@ class SourceFile < PersistentDocument
63
63
  save_to_file(html_rows, nil, output_file_path)
64
64
  end
65
65
 
66
- def save_to_file(html_rows, nav_pane, output_file_path) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
66
+ def save_to_file(html_rows, nav_pane, output_file_path)
67
67
  gem_root = File.expand_path './../../..', File.dirname(__FILE__)
68
68
  template_file = "#{gem_root}/lib/almirah/templates/page.html"
69
69
 
@@ -1,142 +1,133 @@
1
- require_relative "base_document"
1
+ require_relative 'base_document'
2
2
 
3
3
  class Traceability < BaseDocument
4
-
5
- attr_accessor :top_doc
6
- attr_accessor :bottom_doc
7
- attr_accessor :items
8
- attr_accessor :is_agregated
9
- attr_accessor :traced_items
10
-
11
- def initialize(top_doc, bottom_doc)
12
- super()
13
- @top_doc = top_doc
14
- @bottom_doc = bottom_doc
15
- @is_agregated = if bottom_doc
16
- false
17
- else
18
- true
19
- end
20
- @traced_items = {}
21
-
22
- if @is_agregated
23
- @id = top_doc.id + "-all"
24
- else
25
- @id = top_doc.id + "-" + bottom_doc.id
26
- end
27
-
28
- @title = "Traceability Matrix: " + @id
29
- end
30
-
31
- def to_console
32
- puts "\e[35m" + "Traceability: " + @id + "\e[0m"
4
+ attr_accessor :top_doc, :bottom_doc, :items, :is_agregated, :traced_items
5
+
6
+ def initialize(top_doc, bottom_doc)
7
+ super()
8
+ @top_doc = top_doc
9
+ @bottom_doc = bottom_doc
10
+ @is_agregated = if bottom_doc
11
+ false
12
+ else
13
+ true
14
+ end
15
+ @traced_items = {}
16
+
17
+ @id = if @is_agregated
18
+ top_doc.id + '-all'
19
+ else
20
+ top_doc.id + '-' + bottom_doc.id
21
+ end
22
+
23
+ @title = 'Traceability Matrix: ' + @id
24
+ end
25
+
26
+ def to_console
27
+ puts "\e[35m" + 'Traceability: ' + @id + "\e[0m"
28
+ end
29
+
30
+ def to_html(nav_pane, output_file_path)
31
+ html_rows = []
32
+
33
+ html_rows.append('')
34
+ s = "<h1>#{@title}</h1>\n"
35
+ s += "<table class=\"controlled\">\n"
36
+ s += "\t<thead> <th>#</th> <th style='font-weight: bold;'>#{@top_doc.title}</th> "
37
+ s += if @bottom_doc
38
+ "<th>#</th> <th style='font-weight: bold;'>#{@bottom_doc.title}</th> "
39
+ else
40
+ "<th>#</th> <th style='font-weight: bold;'>All References</th> "
41
+ end
42
+ s += "<th style='font-weight: bold;'>Document Section</th>"
43
+ s += "</thead>\n"
44
+ html_rows.append s
45
+
46
+ sorted_items = @top_doc.controlled_items.sort_by { |w| w.id }
47
+
48
+ sorted_items.each do |top_item|
49
+ row = render_table_row top_item
50
+ html_rows.append row
33
51
  end
34
-
35
- def to_html(nav_pane, output_file_path)
36
-
37
- html_rows = Array.new
38
-
39
- html_rows.append('')
40
- s = "<h1>#{@title}</h1>\n"
41
- s += "<table class=\"controlled\">\n"
42
- s += "\t<thead> <th>#</th> <th style='font-weight: bold;'>#{@top_doc.title}</th> "
43
- if @bottom_doc
44
- s += "<th>#</th> <th style='font-weight: bold;'>#{@bottom_doc.title}</th> "
45
- else
46
- s += "<th>#</th> <th style='font-weight: bold;'>All References</th> "
52
+ html_rows.append "</table>\n"
53
+
54
+ save_html_to_file(html_rows, nav_pane, output_file_path)
55
+ end
56
+
57
+ def render_table_row(top_item)
58
+ s = ''
59
+ top_f_text = top_item.format_string(top_item.text)
60
+ id_color = ''
61
+
62
+ if top_item.down_links
63
+
64
+ if @is_agregated
65
+
66
+ top_item_rendered = false
67
+
68
+ top_item.down_links.each do |bottom_item|
69
+ id_color = "style='background-color: ##{bottom_item.parent_doc.color};'"
70
+ bottom_f_text = bottom_item.format_string(bottom_item.text)
71
+ document_section = bottom_item.parent_heading.get_section_info_html
72
+ s += "\t<tr>\n"
73
+ s += "\t\t<td class=\"item_id\"><a href=\"./../#{top_item.parent_doc.id}/#{top_item.parent_doc.id}.html##{top_item.id}\" class=\"external\">#{top_item.id}</a></td>\n"
74
+ s += "\t\t<td class=\"item_text\" style='width: 34%;'>#{top_f_text}</td>\n"
75
+ s += "\t\t<td class=\"item_id\" #{id_color}><a href=\"./../#{bottom_item.parent_doc.id}/#{bottom_item.parent_doc.id}.html##{bottom_item.id}\" class=\"external\">#{bottom_item.id}</a></td>\n"
76
+ s += "\t\t<td class=\"item_text\" style='width: 34%;'>#{bottom_f_text}</td>\n"
77
+ s += "\t\t<td class=\"item_text\" style='width: 16%;'>#{document_section}</td>\n"
78
+ s += "\t</tr>\n"
79
+ top_item_rendered = true
80
+ @traced_items[top_item.id.to_s.downcase] = top_item
47
81
  end
48
- s += "<th style='font-weight: bold;'>Document Section</th>"
49
- s += "</thead>\n"
50
- html_rows.append s
51
-
52
- sorted_items = @top_doc.controlled_items.sort_by { |w| w.id }
53
-
54
- sorted_items.each do |top_item|
55
- row = render_table_row top_item
56
- html_rows.append row
82
+ unless top_item_rendered
83
+ s += "\t<tr>\n"
84
+ s += "\t\t<td class=\"item_id\"><a href=\"./../#{top_item.parent_doc.id}/#{top_item.parent_doc.id}.html##{top_item.id}\" class=\"external\">#{top_item.id}</a></td>\n"
85
+ s += "\t\t<td class=\"item_text\" style='width: 34%;'>#{top_f_text}</td>\n"
86
+ s += "\t\t<td class=\"item_id\"></td>\n"
87
+ s += "\t\t<td class=\"item_text\" style='width: 34%;'></td>\n"
88
+ s += "\t\t<td class=\"item_text\" style='width: 16%;'></td>\n"
89
+ s += "\t</tr>\n"
57
90
  end
58
- html_rows.append "</table>\n"
59
91
 
60
- self.save_html_to_file(html_rows, nav_pane, output_file_path)
61
-
62
- end
63
-
64
- def render_table_row(top_item)
65
- s = ""
66
- top_f_text = top_item.format_string( top_item.text )
67
- id_color = ""
68
-
69
- if top_item.down_links
70
-
71
- if @is_agregated
72
-
73
- top_item_rendered = false
74
-
75
- top_item.down_links.each do |bottom_item|
76
- id_color = "style='background-color: ##{bottom_item.parent_doc.color};'"
77
- bottom_f_text = bottom_item.format_string( bottom_item.text )
78
- document_section = bottom_item.parent_heading.get_section_info_html
79
- s += "\t<tr>\n"
80
- s += "\t\t<td class=\"item_id\"><a href=\"./../#{top_item.parent_doc.id}/#{top_item.parent_doc.id}.html##{top_item.id}\" class=\"external\">#{top_item.id}</a></td>\n"
81
- s += "\t\t<td class=\"item_text\" style='width: 34%;'>#{top_f_text}</td>\n"
82
- s += "\t\t<td class=\"item_id\" #{id_color}><a href=\"./../#{bottom_item.parent_doc.id}/#{bottom_item.parent_doc.id}.html##{bottom_item.id}\" class=\"external\">#{bottom_item.id}</a></td>\n"
83
- s += "\t\t<td class=\"item_text\" style='width: 34%;'>#{bottom_f_text}</td>\n"
84
- s += "\t\t<td class=\"item_text\" style='width: 16%;'>#{document_section}</td>\n"
85
- s += "\t</tr>\n"
86
- top_item_rendered = true
87
- @traced_items[top_item.id.to_s.downcase] = top_item
88
- end
89
- unless top_item_rendered
90
- s += "\t<tr>\n"
91
- s += "\t\t<td class=\"item_id\"><a href=\"./../#{top_item.parent_doc.id}/#{top_item.parent_doc.id}.html##{top_item.id}\" class=\"external\">#{top_item.id}</a></td>\n"
92
- s += "\t\t<td class=\"item_text\" style='width: 34%;'>#{top_f_text}</td>\n"
93
- s += "\t\t<td class=\"item_id\"></td>\n"
94
- s += "\t\t<td class=\"item_text\" style='width: 34%;'></td>\n"
95
- s += "\t\t<td class=\"item_text\" style='width: 16%;'></td>\n"
96
- s += "\t</tr>\n"
97
- end
98
-
99
- else
100
- top_item_rendered = false
101
- top_item.down_links.each do |bottom_item|
102
-
103
- id_color = ""
104
-
105
- if bottom_item.parent_doc.id == @bottom_doc.id
106
-
107
- bottom_f_text = bottom_item.format_string( bottom_item.text )
108
- document_section = bottom_item.parent_heading.get_section_info_html
109
-
110
- s += "\t<tr>\n"
111
- s += "\t\t<td class=\"item_id\" #{id_color}><a href=\"./../#{top_item.parent_doc.id}/#{top_item.parent_doc.id}.html##{top_item.id}\" class=\"external\">#{top_item.id}</a></td>\n"
112
- s += "\t\t<td class=\"item_text\" style='width: 34%;'>#{top_f_text}</td>\n"
113
- s += "\t\t<td class=\"item_id\"><a href=\"./../#{bottom_item.parent_doc.id}/#{bottom_item.parent_doc.id}.html##{bottom_item.id}\" class=\"external\">#{bottom_item.id}</a></td>\n"
114
- s += "\t\t<td class=\"item_text\" style='width: 34%;'>#{bottom_f_text}</td>\n"
115
- s += "\t\t<td class=\"item_text\" style='width: 16%;'>#{document_section}</td>\n"
116
- s += "\t</tr>\n"
117
- top_item_rendered = true
118
- @traced_items[top_item.id.to_s.downcase] = top_item
119
- end
120
- end
121
- unless top_item_rendered
122
- s += "\t<tr>\n"
123
- s += "\t\t<td class=\"item_id\" #{id_color}><a href=\"./../#{top_item.parent_doc.id}/#{top_item.parent_doc.id}.html##{top_item.id}\" class=\"external\">#{top_item.id}</a></td>\n"
124
- s += "\t\t<td class=\"item_text\" style='width: 34%;'>#{top_f_text}</td>\n"
125
- s += "\t\t<td class=\"item_id\"></td>\n"
126
- s += "\t\t<td class=\"item_text\" style='width: 34%;'></td>\n"
127
- s += "\t\t<td class=\"item_text\" style='width: 16%;'></td>\n"
128
- s += "\t</tr>\n"
129
- end
130
- end
131
- else
132
- s += "\t<tr>\n"
133
- s += "\t\t<td class=\"item_id\"><a href=\"./../#{top_item.parent_doc.id}/#{top_item.parent_doc.id}.html##{top_item.id}\" class=\"external\">#{top_item.id}</a></td>\n"
134
- s += "\t\t<td class=\"item_text\" style='width: 34%;'>#{top_f_text}</td>\n"
135
- s += "\t\t<td class=\"item_id\"></td>\n"
136
- s += "\t\t<td class=\"item_text\" style='width: 34%;'></td>\n"
137
- s += "\t\t<td class=\"item_text\" style='width: 16%;'></td>\n"
138
- s += "\t</tr>\n"
92
+ else
93
+ top_item_rendered = false
94
+ top_item.down_links.each do |bottom_item|
95
+ id_color = ''
96
+
97
+ next unless bottom_item.parent_doc.id == @bottom_doc.id
98
+
99
+ bottom_f_text = bottom_item.format_string(bottom_item.text)
100
+ document_section = bottom_item.parent_heading.get_section_info_html
101
+
102
+ s += "\t<tr>\n"
103
+ s += "\t\t<td class=\"item_id\" #{id_color}><a href=\"./../#{top_item.parent_doc.id}/#{top_item.parent_doc.id}.html##{top_item.id}\" class=\"external\">#{top_item.id}</a></td>\n"
104
+ s += "\t\t<td class=\"item_text\" style='width: 34%;'>#{top_f_text}</td>\n"
105
+ s += "\t\t<td class=\"item_id\"><a href=\"./../#{bottom_item.parent_doc.id}/#{bottom_item.parent_doc.id}.html##{bottom_item.id}\" class=\"external\">#{bottom_item.id}</a></td>\n"
106
+ s += "\t\t<td class=\"item_text\" style='width: 34%;'>#{bottom_f_text}</td>\n"
107
+ s += "\t\t<td class=\"item_text\" style='width: 16%;'>#{document_section}</td>\n"
108
+ s += "\t</tr>\n"
109
+ top_item_rendered = true
110
+ @traced_items[top_item.id.to_s.downcase] = top_item
111
+ end
112
+ unless top_item_rendered
113
+ s += "\t<tr>\n"
114
+ s += "\t\t<td class=\"item_id\" #{id_color}><a href=\"./../#{top_item.parent_doc.id}/#{top_item.parent_doc.id}.html##{top_item.id}\" class=\"external\">#{top_item.id}</a></td>\n"
115
+ s += "\t\t<td class=\"item_text\" style='width: 34%;'>#{top_f_text}</td>\n"
116
+ s += "\t\t<td class=\"item_id\"></td>\n"
117
+ s += "\t\t<td class=\"item_text\" style='width: 34%;'></td>\n"
118
+ s += "\t\t<td class=\"item_text\" style='width: 16%;'></td>\n"
119
+ s += "\t</tr>\n"
139
120
  end
140
- return s
121
+ end
122
+ else
123
+ s += "\t<tr>\n"
124
+ s += "\t\t<td class=\"item_id\"><a href=\"./../#{top_item.parent_doc.id}/#{top_item.parent_doc.id}.html##{top_item.id}\" class=\"external\">#{top_item.id}</a></td>\n"
125
+ s += "\t\t<td class=\"item_text\" style='width: 34%;'>#{top_f_text}</td>\n"
126
+ s += "\t\t<td class=\"item_id\"></td>\n"
127
+ s += "\t\t<td class=\"item_text\" style='width: 34%;'></td>\n"
128
+ s += "\t\t<td class=\"item_text\" style='width: 16%;'></td>\n"
129
+ s += "\t</tr>\n"
141
130
  end
131
+ s
132
+ end
142
133
  end
@@ -7,7 +7,7 @@ class DocSection
7
7
  @parent_section = nil
8
8
  end
9
9
 
10
- def to_html # rubocop:disable Metrics/MethodLength
10
+ def to_html
11
11
  s = ''
12
12
  s += "\t<li onclick=\"nav_toggle_expand_list(this, event)\">" \
13
13
  '<span class="fa-li"><i class="fa fa-minus-square-o"> </i></span>'
@@ -1,17 +1,13 @@
1
-
2
1
  class NavigationPane
2
+ attr_accessor :specifications
3
3
 
4
- attr_accessor :specifications
4
+ def initialize(specification)
5
+ @doc = specification
6
+ end
5
7
 
6
- def initialize(specification)
7
- @doc = specification
8
- end
8
+ def to_html
9
+ return @doc.dom.section_tree_to_html if @doc.dom
9
10
 
10
- def to_html
11
- if @doc.dom
12
- return @doc.dom.section_tree_to_html()
13
- else
14
- return ''
15
- end
16
- end
17
- end
11
+ ''
12
+ end
13
+ end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'work_item_scheduler'
4
+
5
+ # Resource-levelled scheduler variant for the critical chain (ADR-195): each
6
+ # row's duration is its focused estimate, and rows are prioritised by the longest
7
+ # downstream chain of focused durations (ties: record sequence, then step). The
8
+ # forward pass, resource levelling, binding-predecessor tracking, and chain
9
+ # tracing are all inherited from WorkItemScheduler.
10
+ class ChainScheduler < WorkItemScheduler
11
+ def duration_for(work_item)
12
+ work_item.focused_estimate
13
+ end
14
+
15
+ private
16
+
17
+ def priority_key(work_item, memo)
18
+ [-downstream_length(work_item, memo, []), work_item.record_sequence, work_item.step]
19
+ end
20
+
21
+ # The longest path of focused durations from this row through its in-scope
22
+ # successors to a sink. Memoised and cycle-guarded.
23
+ def downstream_length(work_item, memo, stack)
24
+ return memo[work_item] if memo.key?(work_item)
25
+ return duration_for(work_item) if stack.include?(work_item)
26
+
27
+ stack.push(work_item)
28
+ tail = scoped_successors(work_item).map { |s| downstream_length(s, memo, stack) }.max || 0
29
+ stack.pop
30
+ memo[work_item] = duration_for(work_item) + tail
31
+ end
32
+ end
33
+
34
+ # The Gantt's scheduler (ADR-201/195): real focused-estimate durations like the
35
+ # critical-chain scheduler, but rounded up to whole day columns with a one-day
36
+ # minimum so an unestimated row (focused 0) still shows a visible bar. The chain
37
+ # and buffer keep their exact-duration math in ChainScheduler / CriticalChain.
38
+ class GanttScheduler < ChainScheduler
39
+ def duration_for(work_item)
40
+ [work_item.focused_estimate.ceil, 1].max
41
+ end
42
+ end
43
+
44
+ # The critical chain and project buffer for one decision group's Scope rows
45
+ # (ADR-195). This object serves two distinct jobs that want opposite treatment of
46
+ # completed work, so it keeps two views of the same Scope rows:
47
+ #
48
+ # - the *scheduling* chain (`chain` / `buffer` / `projected_duration`) excludes
49
+ # Done rows as finished work — you do not re-schedule what is already done — and
50
+ # reports the duration of the work still remaining;
51
+ # - the *baseline* chain (`baseline_chain` / `baseline_buffer`) keeps every row,
52
+ # Done included, as the plan was originally sized. Buffer-consumption accounting
53
+ # (the fever chart, ADR-196) reads this view so that a chain row which overran
54
+ # and then completed keeps consuming buffer instead of vanishing the moment it
55
+ # is marked Done, and so the consumption denominator stays a stable plan-time
56
+ # baseline rather than shrinking as rows finish (issue-207).
57
+ #
58
+ # A predecessor outside the given set (a row in another group) drops out and is
59
+ # treated as an already-available input. The buffer aggregates the safety
60
+ # (safe - focused, clamped at 0) along a chain and cuts it by buffer_ratio,
61
+ # rounded up.
62
+ class CriticalChain
63
+ def initialize(work_items, buffer_ratio: 0.5)
64
+ @all_items = work_items
65
+ @items = work_items.reject(&:done?)
66
+ @buffer_ratio = buffer_ratio
67
+ @scheduler = ChainScheduler.new(@items)
68
+ @baseline_scheduler = ChainScheduler.new(@all_items)
69
+ end
70
+
71
+ # The remaining chain (Done rows excluded) in start order — drives the chain
72
+ # table and the projected completion of the work still to do.
73
+ def chain
74
+ @scheduler.critical_chain
75
+ end
76
+
77
+ # The chain as originally planned, completed rows included (issue-207). Buffer
78
+ # consumption is accounted against this set so completed overruns stay visible.
79
+ def baseline_chain
80
+ @baseline_scheduler.critical_chain
81
+ end
82
+
83
+ # Chain length in working days (the latest finish).
84
+ def length
85
+ @scheduler.makespan
86
+ end
87
+
88
+ # ceil(buffer_ratio * Σ_chain max(safe - focused, 0)) over the remaining chain.
89
+ def buffer
90
+ buffer_for(chain)
91
+ end
92
+
93
+ # The plan-time buffer over the full baseline chain — the stable denominator for
94
+ # buffer-consumption %, unaffected by rows completing (issue-207).
95
+ def baseline_buffer
96
+ buffer_for(baseline_chain)
97
+ end
98
+
99
+ def projected_duration
100
+ length + buffer
101
+ end
102
+
103
+ # False when no row carries a positive focused estimate, so the overview marks
104
+ # the group "unestimated" rather than reporting a misleadingly short plan.
105
+ # Reads the baseline so a fully-completed group still reports as estimated and
106
+ # keeps its (100%-complete) fever chart instead of collapsing to "not sized".
107
+ def estimated?
108
+ @all_items.any? { |wi| wi.focused_estimate.positive? }
109
+ end
110
+
111
+ private
112
+
113
+ def buffer_for(rows)
114
+ safety = rows.sum { |wi| [wi.safe_estimate - wi.focused_estimate, 0].max }
115
+ (@buffer_ratio * safety).ceil
116
+ end
117
+ end
@@ -17,7 +17,7 @@ class DocLinker
17
17
  result
18
18
  end
19
19
 
20
- def self.link_protocol_to_spec(protocol, specification) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
20
+ def self.link_protocol_to_spec(protocol, specification)
21
21
  top_document = specification
22
22
  bottom_document = protocol
23
23
 
@@ -44,7 +44,7 @@ class DocLinker
44
44
  end
45
45
  end
46
46
 
47
- def self.link_decision_to_spec(decision, specification) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
47
+ def self.link_decision_to_spec(decision, specification)
48
48
  top_document = specification
49
49
  bottom_document = decision
50
50
 
@@ -68,7 +68,7 @@ class DocLinker
68
68
  end
69
69
  end
70
70
 
71
- def self.link_source_file_to_spec(source_file, specification) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
71
+ def self.link_source_file_to_spec(source_file, specification)
72
72
  top_document = specification
73
73
  bottom_document = source_file
74
74
 
@@ -92,7 +92,7 @@ class DocLinker
92
92
  end
93
93
  end
94
94
 
95
- def self.link_two_specifications(doc_a, doc_b) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
95
+ def self.link_two_specifications(doc_a, doc_b)
96
96
  if doc_b.up_link_docs.key?(doc_a.id.to_s)
97
97
  top_document = doc_a
98
98
  bottom_document = doc_b