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
@@ -1,104 +1,104 @@
1
1
  require_relative 'base_document'
2
2
 
3
3
  class Implementation < BaseDocument
4
- attr_accessor :top_doc, :bottom_doc, :items, :is_agregated, :traced_items
5
-
6
- def initialize(top_doc)
7
- super()
8
- @top_doc = top_doc
9
-
10
- @is_agregated = true
11
- @traced_items = {}
12
-
13
- @id = if @is_agregated
14
- top_doc.id + '-sources'
15
- else
16
- top_doc.id + '-' + bottom_doc.id
17
- end
18
-
19
- @title = 'Implementation Matrix: ' + @id
4
+ attr_accessor :top_doc, :bottom_doc, :items, :is_agregated, :traced_items
5
+
6
+ def initialize(top_doc)
7
+ super()
8
+ @top_doc = top_doc
9
+
10
+ @is_agregated = true
11
+ @traced_items = {}
12
+
13
+ @id = if @is_agregated
14
+ top_doc.id + '-sources'
15
+ else
16
+ top_doc.id + '-' + bottom_doc.id
17
+ end
18
+
19
+ @title = 'Implementation Matrix: ' + @id
20
+ end
21
+
22
+ def to_console
23
+ puts "\e[35m" + 'Implementation: ' + @id + "\e[0m"
24
+ end
25
+
26
+ def to_html(nav_pane, output_file_path)
27
+ html_rows = []
28
+
29
+ html_rows.append('')
30
+ s = "<h1>#{@title}</h1>\n"
31
+ s += "<table class=\"controlled\">\n"
32
+ s += "\t<thead>"
33
+ s += "\t\t<th>#</th>"
34
+ s += "\t\t<th style='font-weight: bold;'>#{@top_doc.title}</th>"
35
+ s += "\t\t<th>#</th>"
36
+ s += "\t\t<th style='font-weight: bold;'>Repository</th>"
37
+ s += "\t\t<th style='font-weight: bold;'>File Name</th>"
38
+ s += "\t\t<th style='font-weight: bold;'>Comment</th>"
39
+ s += "\t</thead>\n"
40
+ html_rows.append s
41
+
42
+ sorted_items = @top_doc.controlled_items.sort_by { |w| w.id }
43
+
44
+ sorted_items.each do |top_item|
45
+ row = render_table_row top_item
46
+ html_rows.append row
20
47
  end
21
-
22
- def to_console
23
- puts "\e[35m" + 'Implementation: ' + @id + "\e[0m"
24
- end
25
-
26
- def to_html(nav_pane, output_file_path)
27
- html_rows = []
28
-
29
- html_rows.append('')
30
- s = "<h1>#{@title}</h1>\n"
31
- s += "<table class=\"controlled\">\n"
32
- s += "\t<thead>"
33
- s += "\t\t<th>#</th>"
34
- s += "\t\t<th style='font-weight: bold;'>#{@top_doc.title}</th>"
35
- s += "\t\t<th>#</th>"
36
- s += "\t\t<th style='font-weight: bold;'>Repository</th>"
37
- s += "\t\t<th style='font-weight: bold;'>File Name</th>"
38
- s += "\t\t<th style='font-weight: bold;'>Comment</th>"
39
- s += "\t</thead>\n"
40
- html_rows.append s
41
-
42
- sorted_items = @top_doc.controlled_items.sort_by { |w| w.id }
43
-
44
- sorted_items.each do |top_item|
45
- row = render_table_row top_item
46
- html_rows.append row
47
- end
48
- html_rows.append "</table>\n"
49
-
50
- save_html_to_file(html_rows, nav_pane, output_file_path)
51
- end
52
-
53
- def render_table_row(top_item)
54
- s = ''
55
- top_f_text = top_item.format_string(top_item.text)
56
- id_color = ''
57
-
58
- if top_item.source_code_links && top_item.source_code_links.length.positive?
59
-
60
- top_item_rendered = false
61
-
62
- top_item.source_code_links.each do |bottom_item|
63
- id_color = "style='background-color: #cff4d2;'"
64
- bottom_f_text = bottom_item.format_string(bottom_item.text)
65
- file_name = bottom_item.parent_doc.id
66
- repository = bottom_item.parent_doc.repository
67
-
68
- p = bottom_item.parent_doc.html_file_path.split('/build/source_files/').last
69
- html_source_file_relative_path = "./../../source_files/#{p}"
70
-
71
- s += "\t<tr>\n"
72
- 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"
73
- s += "\t\t<td class=\"item_text\" style='width: 28%;'>#{top_f_text}</td>\n"
74
- s += "\t\t<td class=\"item_id\"><a href=\"#{html_source_file_relative_path}##{bottom_item.id}\" class=\"external\">#{bottom_item.id}</a></td>\n"
75
- s += "\t\t<td class=\"item_text\" style='width: 16%;'>#{repository}</td>\n"
76
- s += "\t\t<td class=\"item_text\" style='width: 16%;'>#{file_name}</td>\n"
77
- s += "\t\t<td class=\"item_text\" style='width: 28%;'>#{bottom_f_text}</td>\n"
78
- s += "\t</tr>\n"
79
- top_item_rendered = true
80
- @traced_items[top_item.id.to_s.downcase] = top_item
81
- end
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: 28%;'>#{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: 16%;'></td>\n"
88
- s += "\t\t<td class=\"item_text\" style='width: 16%;'></td>\n"
89
- s += "\t\t<td class=\"item_text\" style='width: 28%;'></td>\n"
90
- s += "\t</tr>\n"
91
- end
92
- else
93
- s += "\t<tr>\n"
94
- 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"
95
- s += "\t\t<td class=\"item_text\" style='width: 28%;'>#{top_f_text}</td>\n"
96
- s += "\t\t<td class=\"item_id\"></td>\n"
97
- s += "\t\t<td class=\"item_text\" style='width: 16%;'></td>\n"
98
- s += "\t\t<td class=\"item_text\" style='width: 16%;'></td>\n"
99
- s += "\t\t<td class=\"item_text\" style='width: 28%;'></td>\n"
100
- s += "\t</tr>\n"
101
- end
102
- s
48
+ html_rows.append "</table>\n"
49
+
50
+ save_html_to_file(html_rows, nav_pane, output_file_path)
51
+ end
52
+
53
+ def render_table_row(top_item)
54
+ s = ''
55
+ top_f_text = top_item.format_string(top_item.text)
56
+ id_color = ''
57
+
58
+ if top_item.source_code_links && top_item.source_code_links.length.positive?
59
+
60
+ top_item_rendered = false
61
+
62
+ top_item.source_code_links.each do |bottom_item|
63
+ id_color = "style='background-color: #cff4d2;'"
64
+ bottom_f_text = bottom_item.format_string(bottom_item.text)
65
+ file_name = bottom_item.parent_doc.id
66
+ repository = bottom_item.parent_doc.repository
67
+
68
+ p = bottom_item.parent_doc.html_file_path.split('/build/source_files/').last
69
+ html_source_file_relative_path = "./../../source_files/#{p}"
70
+
71
+ s += "\t<tr>\n"
72
+ 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"
73
+ s += "\t\t<td class=\"item_text\" style='width: 28%;'>#{top_f_text}</td>\n"
74
+ s += "\t\t<td class=\"item_id\"><a href=\"#{html_source_file_relative_path}##{bottom_item.id}\" class=\"external\">#{bottom_item.id}</a></td>\n"
75
+ s += "\t\t<td class=\"item_text\" style='width: 16%;'>#{repository}</td>\n"
76
+ s += "\t\t<td class=\"item_text\" style='width: 16%;'>#{file_name}</td>\n"
77
+ s += "\t\t<td class=\"item_text\" style='width: 28%;'>#{bottom_f_text}</td>\n"
78
+ s += "\t</tr>\n"
79
+ top_item_rendered = true
80
+ @traced_items[top_item.id.to_s.downcase] = top_item
81
+ end
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: 28%;'>#{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: 16%;'></td>\n"
88
+ s += "\t\t<td class=\"item_text\" style='width: 16%;'></td>\n"
89
+ s += "\t\t<td class=\"item_text\" style='width: 28%;'></td>\n"
90
+ s += "\t</tr>\n"
91
+ end
92
+ else
93
+ s += "\t<tr>\n"
94
+ 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"
95
+ s += "\t\t<td class=\"item_text\" style='width: 28%;'>#{top_f_text}</td>\n"
96
+ s += "\t\t<td class=\"item_id\"></td>\n"
97
+ s += "\t\t<td class=\"item_text\" style='width: 16%;'></td>\n"
98
+ s += "\t\t<td class=\"item_text\" style='width: 16%;'></td>\n"
99
+ s += "\t\t<td class=\"item_text\" style='width: 28%;'></td>\n"
100
+ s += "\t</tr>\n"
103
101
  end
102
+ s
103
+ end
104
104
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  require_relative 'base_document'
4
4
 
5
- class Index < BaseDocument # rubocop:disable Metrics/ClassLength,Style/Documentation
5
+ class Index < BaseDocument
6
6
  attr_accessor :project
7
7
 
8
8
  def initialize(project)
@@ -16,11 +16,10 @@ class Index < BaseDocument # rubocop:disable Metrics/ClassLength,Style/Documenta
16
16
  puts "\e[36mIndex: #{@id}\e[0m"
17
17
  end
18
18
 
19
- def to_html(output_file_path) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
19
+ def to_html(output_file_path)
20
20
  html_rows = []
21
21
 
22
22
  html_rows.append('')
23
- s = "<h1>#{@title}</h1>\n"
24
23
 
25
24
  # Specifications
26
25
  s = "<h2>Specifications</h2>\n"
@@ -41,7 +40,7 @@ class Index < BaseDocument # rubocop:disable Metrics/ClassLength,Style/Documenta
41
40
 
42
41
  sorted_items = @project.project_data.specifications.sort_by(&:id)
43
42
 
44
- sorted_items.each do |doc| # rubocop:disable Metrics/BlockLength
43
+ sorted_items.each do |doc|
45
44
  s = "\t<tr>\n"
46
45
  s += "\t\t<td class=\"item_text\" style='padding: 5px;'><a href=\"./specifications/#{doc.id}/#{doc.id}.html\" class=\"external\"><i class=\"fa fa-file-text-o\" style='background-color: ##{doc.color};'> </i>&nbsp#{doc.title}</a></td>\n"
47
46
  s += "\t\t<td class=\"item_id\" style='width: 7%;'>#{doc.controlled_items.length}</td>\n"
@@ -121,11 +120,11 @@ class Index < BaseDocument # rubocop:disable Metrics/ClassLength,Style/Documenta
121
120
  s += "\t\t<td class=\"item_text\" style='padding: 5px;'><a href=\"./specifications/#{doc.id}/#{doc.id}.html\" class=\"external\">#{doc.title}</a></td>\n"
122
121
  s += "\t\t<td class=\"item_id\" style='width: 7%;'>#{'%.2f' % coverage}%</td>\n"
123
122
  s += "\t\t<td class=\"item_text\" style='width: 25%; padding: 5px;'><i class=\"fa fa-file-text-o\" style='background-color: ##{doc.top_doc.color};'> </i>&nbsp#{doc.top_doc.title}</td>\n"
124
- if doc.bottom_doc
125
- s += "\t\t<td class=\"item_text\" style='width: 25%; padding: 5px;'><i class=\"fa fa-file-text-o\" style='background-color: ##{doc.bottom_doc.color};'> </i>&nbsp#{doc.bottom_doc.title}</td>\n"
126
- else
127
- s += "\t\t<td class=\"item_text\" style='width: 25%; padding: 5px;'><i class=\"fa fa-circle-o\"'> </i>&nbspAll References</td>\n"
128
- end
123
+ s += if doc.bottom_doc
124
+ "\t\t<td class=\"item_text\" style='width: 25%; padding: 5px;'><i class=\"fa fa-file-text-o\" style='background-color: ##{doc.bottom_doc.color};'> </i>&nbsp#{doc.bottom_doc.title}</td>\n"
125
+ else
126
+ "\t\t<td class=\"item_text\" style='width: 25%; padding: 5px;'><i class=\"fa fa-circle-o\"'> </i>&nbspAll References</td>\n"
127
+ end
129
128
  s += "</tr>\n"
130
129
  html_rows.append s
131
130
  end
@@ -158,7 +157,7 @@ class Index < BaseDocument # rubocop:disable Metrics/ClassLength,Style/Documenta
158
157
  end
159
158
 
160
159
  s += "\t\t<td class=\"item_text\" style='padding: 5px;'><a href=\"./specifications/#{doc.id}/#{doc.id}.html\" class=\"external\">#{doc.title}</a></td>\n"
161
- s += "\t\t<td class=\"item_id\" style='width: 7%;'>#{'%.2f' % coverage}%</td>\n" # rubocop:disable Style/FormatString
160
+ s += "\t\t<td class=\"item_id\" style='width: 7%;'>#{'%.2f' % coverage}%</td>\n"
162
161
  s += "\t\t<td class=\"item_id\" style='width: 7%; #{test_step_color}' title='pass/fail'> #{doc.passed_steps_number}/#{doc.failed_steps_number} </td>\n"
163
162
  s += "\t\t<td class=\"item_text\" style='width: 25%; padding: 5px;'>\
164
163
  <i class=\"fa fa-file-text-o\" style='background-color: ##{doc.top_doc.color};'> </i>\
@@ -187,13 +186,13 @@ class Index < BaseDocument # rubocop:disable Metrics/ClassLength,Style/Documenta
187
186
 
188
187
  implemented_items = 0
189
188
  doc.top_doc.controlled_items.each do |i|
190
- implemented_items += 1 if i.source_code_links && i.source_code_links.length.positive? # rubocop:disable Style/SafeNavigation
189
+ implemented_items += 1 if i.source_code_links && i.source_code_links.length.positive?
191
190
  end
192
191
 
193
192
  coverage = implemented_items.to_f / doc.top_doc.controlled_items.length * 100.0
194
193
 
195
194
  s += "\t\t<td class=\"item_text\" style='padding: 5px;'><a href=\"./specifications/#{doc.id}/#{doc.id}.html\" class=\"external\">#{doc.title}</a></td>\n"
196
- s += "\t\t<td class=\"item_id\" style='width: 7%;'>#{'%.2f' % coverage}%</td>\n" # rubocop:disable Style/FormatString
195
+ s += "\t\t<td class=\"item_id\" style='width: 7%;'>#{'%.2f' % coverage}%</td>\n"
197
196
  s += "\t\t<td class=\"item_text\" style='width: 25%; padding: 5px;'>\
198
197
  <i class=\"fa fa-file-text-o\" style='background-color: ##{doc.top_doc.color};'> </i>\
199
198
  #{doc.top_doc.title}</td>\n"
@@ -2,7 +2,7 @@
2
2
 
3
3
  require_relative 'base_document'
4
4
 
5
- class PersistentDocument < BaseDocument # rubocop:disable Style/Documentation
5
+ class PersistentDocument < BaseDocument
6
6
  attr_accessor :path, :items, :controlled_items, :headings, :up_link_docs, :frontmatter
7
7
 
8
8
  def initialize(fele_path)
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'date'
4
+
5
+ # Shared date bucketing for the planning views (ADR-182): the recent Fridays a
6
+ # time-series trail is sampled on. Used by the overview's velocity chart and the
7
+ # Critical Chain page's fever-chart trail (ADR-196).
8
+ module PlanningDates
9
+ # `count` Fridays ending at the Friday on or before `reference_date`, oldest
10
+ # first.
11
+ def recent_fridays(reference_date, count)
12
+ friday_wday = 5
13
+ days_back = (reference_date.wday - friday_wday) % 7
14
+ most_recent = reference_date - days_back
15
+ (0...count).to_a.reverse.map { |i| most_recent - (7 * i) }
16
+ end
17
+ end
@@ -1,28 +1,24 @@
1
- require_relative "persistent_document"
1
+ require_relative 'persistent_document'
2
2
 
3
3
  class Protocol < PersistentDocument
4
+ attr_accessor :specifications_path
4
5
 
5
- attr_accessor :specifications_path
6
+ def initialize(fele_path)
7
+ super
8
+ @id = File.basename(fele_path, File.extname(fele_path)).downcase
9
+ @specifications_path = './../../../specifications/'
10
+ end
6
11
 
7
- def initialize(fele_path)
8
- super
9
- @id = File.basename(fele_path, File.extname(fele_path)).downcase
10
- @specifications_path = './../../../specifications/'
11
- end
12
-
13
- def to_html(nav_pane, output_file_path)
14
-
15
- html_rows = Array.new
16
-
17
- html_rows.append('')
12
+ def to_html(nav_pane, output_file_path)
13
+ html_rows = []
18
14
 
19
- @items.each do |item|
20
- a = item.to_html
21
- html_rows.append a
22
- end
15
+ html_rows.append('')
23
16
 
24
- self.save_html_to_file(html_rows, nav_pane, output_file_path)
25
-
17
+ @items.each do |item|
18
+ a = item.to_html
19
+ html_rows.append a
26
20
  end
27
21
 
28
- end
22
+ save_html_to_file(html_rows, nav_pane, output_file_path)
23
+ end
24
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'decision'
4
+ require_relative '../doc_items/controlled_table'
5
+
6
+ # A risk record (ADR-215): one Markdown file per risk, collected from the
7
+ # first-level subfolders of the project's risks/ folder, each subfolder being
8
+ # a risk registry. The record format is the decision-record format — filename
9
+ # letters-digits id, frontmatter title, Status table with a "*" current-state
10
+ # marker — so the type reuses Decision wholesale.
11
+ class RiskRecord < Decision
12
+ # The first-level risks/ subfolder the record was collected from.
13
+ attr_accessor :registry
14
+
15
+ def to_console
16
+ puts "\e[36mRisk Record: #{@id}\e[0m"
17
+ end
18
+
19
+ # The rendered HTML of the record section whose heading text equals
20
+ # `section_name` (ADR-216). Empty when there is no such section — the
21
+ # register renders it as an empty cell.
22
+ def section_html(section_name)
23
+ section_items(section_name).map(&:to_html).join
24
+ end
25
+
26
+ # The numeric value of the named section (ADR-217): its items' plain text
27
+ # parsed as a Float. nil when the section is missing, empty, or not numeric —
28
+ # the RPN cell renders blank rather than computing a broken record as safe.
29
+ def section_numeric(section_name)
30
+ texts = section_items(section_name).filter_map { |i| i.text if i.respond_to?(:text) }
31
+ Float(texts.join(' ').strip, exception: false)
32
+ end
33
+
34
+ # The record's value for an RPN group (ADR-217): the product of its numeric
35
+ # input sections. nil when any input is missing or not numeric — such a
36
+ # record renders a blank cell and is ignored by the summary aggregates.
37
+ def rpn_value(group)
38
+ factors = group[:inputs].map { |input| section_numeric(input) }
39
+ return nil if factors.any?(&:nil?)
40
+
41
+ factors.reduce(:*)
42
+ end
43
+
44
+ # The distinct controlled-paragraph IDs the record's Affected Documents
45
+ # Req-ID column links to (ADR-218), in the section's row order — the
46
+ # IDs-only content of the register cell. Empty when the record carries no
47
+ # Affected Documents section or its rows link nothing.
48
+ def affected_document_ids
49
+ table = section_items('Affected Documents').find { |i| i.is_a?(ControlledTable) }
50
+ return [] if table.nil?
51
+
52
+ table.rows.flat_map { |row| row.up_link_ids || [] }.uniq
53
+ end
54
+
55
+ private
56
+
57
+ # The items between the heading whose text equals `section_name` and the
58
+ # next heading of the same or a higher level; empty when no heading matches.
59
+ def section_items(section_name)
60
+ in_section = false
61
+ section_level = nil
62
+ collected = []
63
+ @items.each do |item|
64
+ if item.is_a?(Heading) && !in_section
65
+ next unless item.text.strip == section_name
66
+
67
+ in_section = true
68
+ section_level = item.level
69
+ elsif in_section
70
+ break if item.is_a?(Heading) && item.level <= section_level
71
+
72
+ collected.append item
73
+ end
74
+ end
75
+ collected
76
+ end
77
+ end
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_document'
4
+ require_relative 'rpn_rendering'
5
+ require_relative '../doc_items/heading'
6
+
7
+ # The registry page (ADR-216): build/risks/<registry>/overview.html holding
8
+ # the rendered registry preface (overview.md), when present, followed by the
9
+ # register table — one row per risk record with the implicit linked-ID and
10
+ # Title columns first, then the configured columns filled from each record's
11
+ # section matched by heading text. The Status column is reserved: it is filled
12
+ # from the record's current lifecycle status, never from a section.
13
+ class RiskRegistryPage < BaseDocument
14
+ include RpnRendering
15
+
16
+ STATUS_COLUMN = 'Status'
17
+ # The column rendered as bare linked IDs from the record's Affected
18
+ # Documents section (ADR-218), never as the section's prose.
19
+ AFFECTED_DOCUMENTS_COLUMN = 'Affected Documents'
20
+
21
+ attr_accessor :registry, :records, :preface, :columns, :rpn_groups
22
+
23
+ # `columns` is the configured per-registry list (ADR-216); nil means the
24
+ # registry is not configured and gets the implicit columns plus Status only.
25
+ # `rpn_groups` (ADR-217) appends one computed "<Name> RPN" column per group
26
+ # after the configured columns; empty appends nothing.
27
+ def initialize(registry, records, preface, columns, rpn_groups = [])
28
+ super()
29
+ @registry = registry
30
+ @records = records
31
+ @preface = preface
32
+ @columns = columns.nil? ? [STATUS_COLUMN] : columns
33
+ @rpn_groups = rpn_groups
34
+ @id = 'overview'
35
+ @title = preface_title || "Risk Registry: #{registry}"
36
+ end
37
+
38
+ def to_console
39
+ puts "\e[36mRisk Registry: #{@registry}\e[0m"
40
+ end
41
+
42
+ def to_html(output_file_path)
43
+ html_rows = ['']
44
+ html_rows.concat preface_html
45
+ html_rows.append render_register_table
46
+ save_html_to_file(html_rows, nil, output_file_path)
47
+ end
48
+
49
+ private
50
+
51
+ # The preface frontmatter title names the page; without it the registry does.
52
+ def preface_title
53
+ params = @preface&.frontmatter&.parameters
54
+ params && params['title']
55
+ end
56
+
57
+ # The rendered overview.md items. The parser-injected level-0 title heading
58
+ # is not authored preface content and is skipped.
59
+ def preface_html
60
+ return [] if @preface.nil?
61
+
62
+ @preface.items.reject { |i| i.is_a?(Heading) && i.level.zero? }.map(&:to_html)
63
+ end
64
+
65
+ def render_register_table
66
+ s = "<table class=\"controlled risk_register\">\n"
67
+ s += "\t<thead>\n"
68
+ s += "\t\t<th>#</th>\n"
69
+ s += "\t\t<th>Title</th>\n"
70
+ @columns.each { |c| s += "\t\t<th>#{c}</th>\n" }
71
+ @rpn_groups.each { |g| s += "\t\t<th>#{g[:name]} RPN</th>\n" }
72
+ s += "</thead>\n"
73
+ @records.each { |doc| s += render_record_row(doc) }
74
+ s + "</table>\n"
75
+ end
76
+
77
+ def render_record_row(doc)
78
+ href = "./#{record_href(doc)}"
79
+ s = "\t<tr>\n"
80
+ s += "\t\t<td class=\"item_id\">\n"
81
+ s += "\t\t\t<a name=\"#{doc.id}\" id=\"#{doc.id}\" href=\"#{href}\" title=\"Risk Record ID\">#{doc.id.upcase}</a>"
82
+ s += "\t\t</td>\n"
83
+ s += "\t\t<td class=\"item_text\" style='padding: 5px;'>\
84
+ <a href=\"#{href}\" class=\"external\">#{record_title(doc)}</a></td>\n"
85
+ @columns.each { |c| s += render_column_cell(doc, c) }
86
+ @rpn_groups.each { |g| s += render_rpn_cell(doc, g) }
87
+ s + "\t</tr>\n"
88
+ end
89
+
90
+ # The Title cell (ENH-221): the frontmatter title with the record's own
91
+ # leading "ID:" prefix removed, case-insensitively — the ID column already
92
+ # carries it. A title not starting with the record's ID stays unchanged, and
93
+ # the record page keeps the full title. Display-only, like the uppercased ID:
94
+ # anchors and hrefs keep the canonical lowercase id.
95
+ def record_title(doc)
96
+ doc.title.to_s.sub(/\A#{Regexp.escape(doc.id)}\s*:\s*/i, '')
97
+ end
98
+
99
+ # The record page path relative to the registry page, which sits at the
100
+ # registry root: the record's html_rel_path minus its registry segment.
101
+ def record_href(doc)
102
+ doc.html_rel_path.split('/', 2).last
103
+ end
104
+
105
+ def render_column_cell(doc, column)
106
+ return "\t\t<td class=\"item_status\">#{doc.current_status}</td>\n" if column == STATUS_COLUMN
107
+ return render_affected_documents_cell(doc) if column == AFFECTED_DOCUMENTS_COLUMN
108
+
109
+ "\t\t<td class=\"item_text\">#{doc.section_html(column)}</td>\n"
110
+ end
111
+
112
+ # IDs only (ADR-218): each distinct linked controlled-paragraph ID as a
113
+ # clickable link in row order; a dangling ID renders in the existing
114
+ # broken-link style rather than being dropped. The Proposed Text stays on
115
+ # the record page.
116
+ def render_affected_documents_cell(doc)
117
+ links = doc.affected_document_ids.map { |id| affected_document_link(doc, id) }
118
+ "\t\t<td class=\"item_id\">#{links.join(', ')}</td>\n"
119
+ end
120
+
121
+ def affected_document_link(doc, item_id)
122
+ if doc.wrong_links_hash.key?(item_id)
123
+ %(<span class="broken_link" title="Unresolved reference">#{item_id}</span>)
124
+ else
125
+ spec = /^([a-zA-Z]+)-\d+/.match(item_id)&.[](1)&.downcase
126
+ href = "./../../specifications/#{spec}/#{spec}.html##{item_id}"
127
+ %(<a href="#{href}" class="external" title="Affected document">#{item_id}</a>)
128
+ end
129
+ end
130
+
131
+ # The computed group cell (ADR-217): the record's group value, blank when
132
+ # any input is missing or not numeric, coloured by the group's thresholds
133
+ # when configured.
134
+ def render_rpn_cell(doc, group)
135
+ value = doc.rpn_value(group)
136
+ return "\t\t<td class=\"item_rpn\"></td>\n" if value.nil?
137
+
138
+ classes = ['item_rpn', rpn_threshold_class(value, group)].compact.join(' ')
139
+ "\t\t<td class=\"#{classes}\">#{format_rpn(value)}</td>\n"
140
+ end
141
+ end