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.
- checksums.yaml +4 -4
- data/bin/almirah +2 -2
- data/lib/almirah/doc_fabric.rb +25 -0
- data/lib/almirah/doc_items/blockquote.rb +15 -16
- data/lib/almirah/doc_items/code_block.rb +19 -21
- data/lib/almirah/doc_items/controlled_paragraph.rb +4 -4
- data/lib/almirah/doc_items/controlled_table.rb +11 -11
- data/lib/almirah/doc_items/controlled_table_row.rb +15 -18
- data/lib/almirah/doc_items/doc_footer.rb +10 -13
- data/lib/almirah/doc_items/doc_item.rb +2 -2
- data/lib/almirah/doc_items/heading.rb +1 -1
- data/lib/almirah/doc_items/image.rb +25 -27
- data/lib/almirah/doc_items/markdown_list.rb +2 -2
- data/lib/almirah/doc_items/markdown_table.rb +9 -2
- data/lib/almirah/doc_items/scope_table.rb +188 -0
- data/lib/almirah/doc_items/text_line.rb +26 -22
- data/lib/almirah/doc_items/todo_block.rb +15 -16
- data/lib/almirah/doc_items/work_item.rb +129 -0
- data/lib/almirah/doc_parser.rb +15 -8
- data/lib/almirah/doc_types/base_document.rb +31 -7
- data/lib/almirah/doc_types/coverage.rb +3 -3
- data/lib/almirah/doc_types/critical_chain_page.rb +218 -0
- data/lib/almirah/doc_types/decision.rb +152 -7
- data/lib/almirah/doc_types/decision_grouping.rb +17 -0
- data/lib/almirah/doc_types/decisions_overview.rb +591 -31
- data/lib/almirah/doc_types/implementation.rb +98 -98
- data/lib/almirah/doc_types/index.rb +11 -12
- data/lib/almirah/doc_types/persistent_document.rb +1 -1
- data/lib/almirah/doc_types/planning_dates.rb +17 -0
- data/lib/almirah/doc_types/protocol.rb +16 -20
- data/lib/almirah/doc_types/risk_record.rb +77 -0
- data/lib/almirah/doc_types/risk_registry_page.rb +141 -0
- data/lib/almirah/doc_types/risks_overview.rb +102 -0
- data/lib/almirah/doc_types/rpn_rendering.rb +23 -0
- data/lib/almirah/doc_types/source_file.rb +1 -1
- data/lib/almirah/doc_types/traceability.rb +124 -133
- data/lib/almirah/dom/doc_section.rb +1 -1
- data/lib/almirah/navigation_pane.rb +9 -13
- data/lib/almirah/project/critical_chain.rb +117 -0
- data/lib/almirah/project/doc_linker.rb +4 -4
- data/lib/almirah/project/fever_chart.rb +94 -0
- data/lib/almirah/project/project_data.rb +17 -2
- data/lib/almirah/project/work_item_scheduler.rb +167 -0
- data/lib/almirah/project/working_calendar.rb +112 -0
- data/lib/almirah/project.rb +307 -9
- data/lib/almirah/project_configuration.rb +176 -29
- data/lib/almirah/project_template.rb +6 -6
- data/lib/almirah/project_utility.rb +3 -5
- data/lib/almirah/search/specifications_db.rb +2 -2
- data/lib/almirah/source_file_parser.rb +2 -3
- data/lib/almirah/templates/css/main.css +173 -1
- data/lib/almirah/templates/scripts/main.js +3 -1
- data/lib/almirah.rb +1 -2
- 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)
|
|
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
|
|
1
|
+
require_relative 'base_document'
|
|
2
2
|
|
|
3
3
|
class Traceability < BaseDocument
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
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
|
|
@@ -1,17 +1,13 @@
|
|
|
1
|
-
|
|
2
1
|
class NavigationPane
|
|
2
|
+
attr_accessor :specifications
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
def initialize(specification)
|
|
5
|
+
@doc = specification
|
|
6
|
+
end
|
|
5
7
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
end
|
|
8
|
+
def to_html
|
|
9
|
+
return @doc.dom.section_tree_to_html if @doc.dom
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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
|