Almirah 0.3.1 → 0.4.1
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/lib/almirah/console_reporter.rb +46 -0
- data/lib/almirah/doc_fabric.rb +16 -7
- data/lib/almirah/doc_items/controlled_paragraph.rb +28 -2
- data/lib/almirah/doc_items/controlled_table.rb +5 -4
- data/lib/almirah/doc_items/doc_item.rb +12 -0
- data/lib/almirah/doc_items/markdown_list.rb +2 -2
- data/lib/almirah/doc_items/markdown_table.rb +17 -8
- data/lib/almirah/doc_items/text_line.rb +243 -41
- data/lib/almirah/doc_parser.rb +20 -3
- data/lib/almirah/doc_types/base_document.rb +41 -24
- data/lib/almirah/doc_types/decision.rb +161 -0
- data/lib/almirah/doc_types/decisions_overview.rb +202 -0
- data/lib/almirah/doc_types/protocol.rb +3 -0
- data/lib/almirah/doc_types/source_file.rb +4 -8
- data/lib/almirah/link_registry.rb +38 -0
- data/lib/almirah/project/doc_linker.rb +24 -0
- data/lib/almirah/project/project_data.rb +7 -2
- data/lib/almirah/project.rb +117 -14
- data/lib/almirah/project_configuration.rb +1 -1
- data/lib/almirah/project_template.rb +89 -1
- data/lib/almirah/relative_url.rb +22 -0
- data/lib/almirah/search/specifications_db.rb +4 -4
- data/lib/almirah/templates/css/main.css +41 -1
- data/lib/almirah/templates/scripts/main.js +7 -1
- data/lib/almirah/templates/scripts/orama_search.js +3 -3
- metadata +6 -1
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'date'
|
|
4
|
+
require 'json'
|
|
5
|
+
require_relative 'base_document'
|
|
6
|
+
|
|
7
|
+
class DecisionsOverview < BaseDocument # rubocop:disable Style/Documentation,Metrics/ClassLength
|
|
8
|
+
attr_accessor :project
|
|
9
|
+
|
|
10
|
+
def initialize(project)
|
|
11
|
+
super()
|
|
12
|
+
@project = project
|
|
13
|
+
@title = 'Decision Records Overview'
|
|
14
|
+
@id = 'overview'
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def to_console
|
|
18
|
+
puts "\e[36mDecisions Overview: #{@id}\e[0m"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def to_html(output_file_path) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize,Metrics/CyclomaticComplexity
|
|
22
|
+
html_rows = []
|
|
23
|
+
html_rows.append('')
|
|
24
|
+
html_rows.append "<h1>#{@title}</h1>\n"
|
|
25
|
+
|
|
26
|
+
html_rows.append render_charts_grid
|
|
27
|
+
|
|
28
|
+
html_rows.append "<table class=\"controlled decisions_overview\">\n"
|
|
29
|
+
html_rows.append "\t<thead>\n"
|
|
30
|
+
html_rows.append "\t\t<th>#</th>\n"
|
|
31
|
+
html_rows.append "\t\t<th>Type</th>\n"
|
|
32
|
+
html_rows.append "\t\t<th>Status</th>\n"
|
|
33
|
+
html_rows.append "\t\t<th>Title</th>\n"
|
|
34
|
+
html_rows.append "\t\t<th>Start Date</th>\n"
|
|
35
|
+
html_rows.append "\t\t<th>Target Date</th>\n"
|
|
36
|
+
html_rows.append "\t\t<th title=\"Target Release Version\">Release</th>\n"
|
|
37
|
+
html_rows.append "\t\t<th>Owner</th>\n"
|
|
38
|
+
html_rows.append "</thead>\n"
|
|
39
|
+
|
|
40
|
+
sorted_items = @project.project_data.decisions.sort_by do |d|
|
|
41
|
+
[d.sequence_number ? 0 : 1, d.sequence_number.to_i, d.id]
|
|
42
|
+
end
|
|
43
|
+
sorted_items.each do |doc|
|
|
44
|
+
s = "\t<tr>\n"
|
|
45
|
+
s += "\t\t<td class=\"item_id\">\n"
|
|
46
|
+
label = doc.sequence_number || doc.id
|
|
47
|
+
href = doc.html_rel_path ? "./#{doc.html_rel_path}" : "##{doc.id}"
|
|
48
|
+
anchor_attrs = %(name="#{doc.id}" id="#{doc.id}" href="#{href}" title="Decision Record ID")
|
|
49
|
+
s += "\t\t\t<a #{anchor_attrs}>#{label}</a>"
|
|
50
|
+
s += "\t\t</td>\n"
|
|
51
|
+
s += "\t\t<td class=\"item_type\">#{doc.record_type}</td>\n"
|
|
52
|
+
s += "\t\t<td class=\"item_status\">#{doc.current_status}</td>\n"
|
|
53
|
+
title_html = doc.html_rel_path ? %(<a href="./#{doc.html_rel_path}" class="external">#{doc.title}</a>) : doc.title
|
|
54
|
+
s += "\t\t<td class=\"item_text\" style='padding: 5px;'>#{title_html}</td>\n"
|
|
55
|
+
start_date_html = doc.start_date ? doc.start_date.strftime('%d-%m-%Y') : ''
|
|
56
|
+
s += "\t\t<td class=\"item_meta\">#{start_date_html}</td>\n"
|
|
57
|
+
s += "\t\t<td class=\"item_meta\"></td>\n"
|
|
58
|
+
s += "\t\t<td class=\"item_meta\">#{doc.target_release_version}</td>\n"
|
|
59
|
+
s += "\t\t<td class=\"item_meta\"></td>\n"
|
|
60
|
+
s += "</tr>\n"
|
|
61
|
+
html_rows.append s
|
|
62
|
+
end
|
|
63
|
+
html_rows.append "</table>\n"
|
|
64
|
+
|
|
65
|
+
save_html_to_file(html_rows, nil, output_file_path)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
CHART_PALETTE = [
|
|
71
|
+
[54, 162, 235], [255, 99, 132], [255, 159, 64], [255, 205, 86],
|
|
72
|
+
[75, 192, 192], [153, 102, 255], [201, 203, 207]
|
|
73
|
+
].freeze
|
|
74
|
+
|
|
75
|
+
# Records with a missing or ambiguous current-status marker are surfaced under
|
|
76
|
+
# this category as a data-quality indicator rather than being silently dropped.
|
|
77
|
+
UNDEFINED_STATUS_LABEL = 'Undefined'
|
|
78
|
+
|
|
79
|
+
def render_charts_grid # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
|
|
80
|
+
counts = @project.project_data.decisions.each_with_object(Hash.new(0)) do |item, cntr|
|
|
81
|
+
cntr[item.record_type] += 1 if item.record_type
|
|
82
|
+
end
|
|
83
|
+
labels = counts.keys.sort
|
|
84
|
+
data = labels.map { |k| counts[k] }
|
|
85
|
+
pie_colors = labels.each_with_index.map { |_, i| palette_rgba(i, 0.5) }
|
|
86
|
+
|
|
87
|
+
velocity = velocity_chart_data
|
|
88
|
+
status_dist = status_distribution_chart_data
|
|
89
|
+
|
|
90
|
+
<<~HTML
|
|
91
|
+
<div class="decisions_overview_charts">
|
|
92
|
+
\t<div class="chart_cell">
|
|
93
|
+
\t\t<canvas id="decisions_type_pie"></canvas>
|
|
94
|
+
\t\t<script>
|
|
95
|
+
\t\t\tnew Chart(document.getElementById('decisions_type_pie'), {
|
|
96
|
+
\t\t\t\ttype: 'pie',
|
|
97
|
+
\t\t\t\tdata: {
|
|
98
|
+
\t\t\t\t\tlabels: #{labels.to_json},
|
|
99
|
+
\t\t\t\t\tdatasets: [{
|
|
100
|
+
\t\t\t\t\t\tlabel: 'Decision records',
|
|
101
|
+
\t\t\t\t\t\tdata: #{data.to_json},
|
|
102
|
+
\t\t\t\t\t\tbackgroundColor: #{pie_colors.to_json},
|
|
103
|
+
\t\t\t\t\t\tborderWidth: 0
|
|
104
|
+
\t\t\t\t\t}]
|
|
105
|
+
\t\t\t\t},
|
|
106
|
+
\t\t\t\toptions: { plugins: { title: { display: true, text: 'Decision Records by Type' } } }
|
|
107
|
+
\t\t\t});
|
|
108
|
+
\t\t</script>
|
|
109
|
+
\t</div>
|
|
110
|
+
\t<div class="chart_cell">
|
|
111
|
+
\t\t<canvas id="decisions_velocity_bar"></canvas>
|
|
112
|
+
\t\t<script>
|
|
113
|
+
\t\t\tnew Chart(document.getElementById('decisions_velocity_bar'), {
|
|
114
|
+
\t\t\t\ttype: 'bar',
|
|
115
|
+
\t\t\t\tdata: #{velocity.to_json},
|
|
116
|
+
\t\t\t\toptions: {
|
|
117
|
+
\t\t\t\t\tplugins: { title: { display: true, text: 'Decision Records by Status Over Time' } },
|
|
118
|
+
\t\t\t\t\tscales: { x: { stacked: true }, y: { stacked: true, ticks: { precision: 0 } } }
|
|
119
|
+
\t\t\t\t}
|
|
120
|
+
\t\t\t});
|
|
121
|
+
\t\t</script>
|
|
122
|
+
\t</div>
|
|
123
|
+
\t<div class="chart_cell">
|
|
124
|
+
\t\t<canvas id="decisions_status_bar"></canvas>
|
|
125
|
+
\t\t<script>
|
|
126
|
+
\t\t\tnew Chart(document.getElementById('decisions_status_bar'), {
|
|
127
|
+
\t\t\t\ttype: 'bar',
|
|
128
|
+
\t\t\t\tdata: #{status_dist.to_json},
|
|
129
|
+
\t\t\t\toptions: {
|
|
130
|
+
\t\t\t\t\tindexAxis: 'y',
|
|
131
|
+
\t\t\t\t\tplugins: { title: { display: true, text: 'Decision Records by Current Status' }, legend: { display: false } },
|
|
132
|
+
\t\t\t\t\tscales: { x: { beginAtZero: true, ticks: { precision: 0 } } }
|
|
133
|
+
\t\t\t\t}
|
|
134
|
+
\t\t\t});
|
|
135
|
+
\t\t</script>
|
|
136
|
+
\t</div>
|
|
137
|
+
</div>
|
|
138
|
+
HTML
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def velocity_chart_data(reference_date: Date.today, weeks: 6) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
|
|
142
|
+
fridays = recent_fridays(reference_date, weeks)
|
|
143
|
+
segments = []
|
|
144
|
+
counts = {}
|
|
145
|
+
|
|
146
|
+
fridays.each_with_index do |friday, i|
|
|
147
|
+
@project.project_data.decisions.each do |doc|
|
|
148
|
+
status = doc.effective_status_on(friday)
|
|
149
|
+
next if status.nil?
|
|
150
|
+
|
|
151
|
+
unless counts.key?(status)
|
|
152
|
+
counts[status] = Array.new(fridays.length, 0)
|
|
153
|
+
segments << status
|
|
154
|
+
end
|
|
155
|
+
counts[status][i] += 1
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
{
|
|
160
|
+
labels: fridays.map { |f| f.strftime('%d-%m-%Y') },
|
|
161
|
+
datasets: segments.map { |s| { label: s, data: counts[s] } }
|
|
162
|
+
}
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Tally each decision record under its current status (the "*"-marked Status
|
|
166
|
+
# row). Records whose current status is undefined fall under "Undefined", which
|
|
167
|
+
# is ordered last; real statuses keep first-seen order. The count is baked into
|
|
168
|
+
# each label so small categories stay legible on a linear axis next to a
|
|
169
|
+
# dominant one.
|
|
170
|
+
def status_distribution_chart_data # rubocop:disable Metrics/MethodLength,Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
|
|
171
|
+
counts = Hash.new(0)
|
|
172
|
+
keys = []
|
|
173
|
+
@project.project_data.decisions.each do |doc|
|
|
174
|
+
status = doc.current_status
|
|
175
|
+
key = status.nil? || status.strip.empty? ? UNDEFINED_STATUS_LABEL : status
|
|
176
|
+
keys << key unless counts.key?(key)
|
|
177
|
+
counts[key] += 1
|
|
178
|
+
end
|
|
179
|
+
keys << UNDEFINED_STATUS_LABEL if keys.delete(UNDEFINED_STATUS_LABEL)
|
|
180
|
+
|
|
181
|
+
colors = keys.each_with_index.map do |k, i|
|
|
182
|
+
k == UNDEFINED_STATUS_LABEL ? palette_rgba(CHART_PALETTE.length - 1, 0.5) : palette_rgba(i, 0.5)
|
|
183
|
+
end
|
|
184
|
+
{
|
|
185
|
+
labels: keys.map { |k| "#{k} (#{counts[k]})" },
|
|
186
|
+
datasets: [{ label: 'Decision records', data: keys.map { |k| counts[k] },
|
|
187
|
+
backgroundColor: colors, borderWidth: 0 }]
|
|
188
|
+
}
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def palette_rgba(index, alpha)
|
|
192
|
+
r, g, b = CHART_PALETTE[index % CHART_PALETTE.length]
|
|
193
|
+
"rgba(#{r}, #{g}, #{b}, #{alpha})"
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def recent_fridays(reference_date, count)
|
|
197
|
+
friday_wday = 5
|
|
198
|
+
days_back = (reference_date.wday - friday_wday) % 7
|
|
199
|
+
most_recent = reference_date - days_back
|
|
200
|
+
(0...count).to_a.reverse.map { |i| most_recent - (7 * i) }
|
|
201
|
+
end
|
|
202
|
+
end
|
|
@@ -2,9 +2,12 @@ require_relative "persistent_document"
|
|
|
2
2
|
|
|
3
3
|
class Protocol < PersistentDocument
|
|
4
4
|
|
|
5
|
+
attr_accessor :specifications_path
|
|
6
|
+
|
|
5
7
|
def initialize(fele_path)
|
|
6
8
|
super
|
|
7
9
|
@id = File.basename(fele_path, File.extname(fele_path)).downcase
|
|
10
|
+
@specifications_path = './../../../specifications/'
|
|
8
11
|
end
|
|
9
12
|
|
|
10
13
|
def to_html(nav_pane, output_file_path)
|
|
@@ -25,7 +25,6 @@ class SourceFile < PersistentDocument
|
|
|
25
25
|
depth = relative_path.count('/') + 1 # +1 for the repository folder
|
|
26
26
|
depth += 1 # for the source_files folder
|
|
27
27
|
@specifications_path = "./#{'../' * depth}specifications/"
|
|
28
|
-
puts @specifications_path
|
|
29
28
|
end
|
|
30
29
|
|
|
31
30
|
def to_console
|
|
@@ -78,13 +77,10 @@ class SourceFile < PersistentDocument
|
|
|
78
77
|
@html_file_path = output_file_path
|
|
79
78
|
FileUtils.mkdir_p(File.dirname(output_file_path))
|
|
80
79
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
css_path = "#{'../' * depth}css/main.css"
|
|
86
|
-
js_path = "#{'../' * depth}scripts/main.js"
|
|
87
|
-
index_path = "#{'../' * depth}index.html"
|
|
80
|
+
@output_rel_path = output_file_path.split('/build/', 2).last
|
|
81
|
+
css_path = rel_to('css/main.css')
|
|
82
|
+
js_path = rel_to('scripts/main.js')
|
|
83
|
+
index_path = rel_to('index.html')
|
|
88
84
|
|
|
89
85
|
file = File.open(output_file_path, 'w')
|
|
90
86
|
file_data.each do |s|
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Project-wide registry of managed documents, used to resolve cross-document
|
|
4
|
+
# links (ADR-186) to their generated output pages. Documents are indexed by
|
|
5
|
+
# their id / filename stem (case-insensitive, folder-independent) and, when they
|
|
6
|
+
# originate from a source file, by their absolute source path (for native
|
|
7
|
+
# Markdown relative links).
|
|
8
|
+
class LinkRegistry
|
|
9
|
+
attr_reader :collisions
|
|
10
|
+
|
|
11
|
+
def initialize
|
|
12
|
+
@by_id = {}
|
|
13
|
+
@by_source = {}
|
|
14
|
+
@collisions = []
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Each registered document is expected to already carry an output_rel_path
|
|
18
|
+
# (its generated page, relative to the build root).
|
|
19
|
+
def register(doc)
|
|
20
|
+
key = doc.id.to_s.downcase
|
|
21
|
+
if @by_id.key?(key) && !@by_id[key].equal?(doc)
|
|
22
|
+
@collisions << key
|
|
23
|
+
else
|
|
24
|
+
@by_id[key] = doc
|
|
25
|
+
end
|
|
26
|
+
return unless doc.respond_to?(:path) && doc.path
|
|
27
|
+
|
|
28
|
+
@by_source[File.expand_path(doc.path)] = doc
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def find_by_id(id)
|
|
32
|
+
@by_id[id.to_s.downcase]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def find_by_source(source_path)
|
|
36
|
+
@by_source[File.expand_path(source_path)]
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -44,6 +44,30 @@ 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
|
|
48
|
+
top_document = specification
|
|
49
|
+
bottom_document = decision
|
|
50
|
+
|
|
51
|
+
bottom_document.controlled_items.each do |item|
|
|
52
|
+
next unless item.up_link_ids
|
|
53
|
+
|
|
54
|
+
item.up_link_ids.each do |up_lnk|
|
|
55
|
+
if top_document.dictionary.key?(up_lnk.to_s)
|
|
56
|
+
|
|
57
|
+
top_item = top_document.dictionary[up_lnk.to_s]
|
|
58
|
+
|
|
59
|
+
top_item.decision_record_links = [] unless top_item.decision_record_links
|
|
60
|
+
top_item.decision_record_links.append(item)
|
|
61
|
+
elsif tmp = /^([a-zA-Z]+)-\d+/.match(up_lnk)
|
|
62
|
+
# check if there is a non existing link with the right doc_id
|
|
63
|
+
if tmp[1].downcase == top_document.id.downcase
|
|
64
|
+
bottom_document.wrong_links_hash[up_lnk] = item
|
|
65
|
+
end # SRS
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
47
71
|
def self.link_source_file_to_spec(source_file, specification) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
|
|
48
72
|
top_document = specification
|
|
49
73
|
bottom_document = source_file
|
|
@@ -1,18 +1,23 @@
|
|
|
1
|
+
require_relative '../link_registry'
|
|
2
|
+
|
|
1
3
|
class ProjectData
|
|
2
4
|
attr_reader :specifications, :protocols, :traceability_matrices, :coverage_matrices, :source_files,
|
|
3
5
|
:specifications_dictionary, :covered_specifications_dictionary, :implemented_specifications_dictionary,
|
|
4
|
-
:implementation_matrices
|
|
6
|
+
:implementation_matrices, :decisions, :link_registry
|
|
5
7
|
|
|
6
|
-
def initialize
|
|
8
|
+
def initialize # rubocop:disable Metrics/MethodLength
|
|
7
9
|
@specifications = []
|
|
8
10
|
@protocols = []
|
|
9
11
|
@traceability_matrices = []
|
|
10
12
|
@coverage_matrices = []
|
|
11
13
|
@source_files = []
|
|
12
14
|
@implementation_matrices = []
|
|
15
|
+
@decisions = []
|
|
13
16
|
|
|
14
17
|
@specifications_dictionary = {}
|
|
15
18
|
@covered_specifications_dictionary = {}
|
|
16
19
|
@implemented_specifications_dictionary = {}
|
|
20
|
+
|
|
21
|
+
@link_registry = LinkRegistry.new
|
|
17
22
|
end
|
|
18
23
|
end
|
data/lib/almirah/project.rb
CHANGED
|
@@ -9,6 +9,8 @@ require_relative 'search/specifications_db'
|
|
|
9
9
|
require_relative 'project/doc_linker'
|
|
10
10
|
require_relative 'project_configuration'
|
|
11
11
|
require_relative 'project/project_data'
|
|
12
|
+
require_relative 'console_reporter'
|
|
13
|
+
require_relative 'relative_url'
|
|
12
14
|
|
|
13
15
|
class Project # rubocop:disable Metrics/ClassLength,Style/Documentation
|
|
14
16
|
attr_accessor :index, :project, :configuration, :project_data
|
|
@@ -37,14 +39,17 @@ class Project # rubocop:disable Metrics/ClassLength,Style/Documentation
|
|
|
37
39
|
FileUtils.copy_entry(src_folder, dst_folder)
|
|
38
40
|
end
|
|
39
41
|
|
|
40
|
-
def specifications_and_protocols # rubocop:disable Metrics/MethodLength
|
|
42
|
+
def specifications_and_protocols # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
|
|
41
43
|
parse_all_specifications
|
|
42
44
|
parse_all_protocols
|
|
43
45
|
parse_all_source_files
|
|
46
|
+
parse_decisions
|
|
44
47
|
link_all_specifications
|
|
45
48
|
link_all_protocols
|
|
46
49
|
link_all_source_files
|
|
50
|
+
link_all_decisions
|
|
47
51
|
check_wrong_specification_referenced
|
|
52
|
+
build_link_registry
|
|
48
53
|
create_index
|
|
49
54
|
render_all_specifications(@project_data.specifications)
|
|
50
55
|
render_all_specifications(@project_data.traceability_matrices)
|
|
@@ -52,18 +57,25 @@ class Project # rubocop:disable Metrics/ClassLength,Style/Documentation
|
|
|
52
57
|
render_all_protocols
|
|
53
58
|
render_all_source_files
|
|
54
59
|
render_all_specifications(@project_data.implementation_matrices) # intentionally after source file rendering
|
|
60
|
+
render_decisions_overview
|
|
61
|
+
render_all_decisions
|
|
55
62
|
render_index
|
|
56
63
|
create_search_data
|
|
64
|
+
report_broken_links
|
|
65
|
+
report_rendered
|
|
57
66
|
end
|
|
58
67
|
|
|
59
|
-
def specifications_and_results(test_run) # rubocop:disable Metrics/MethodLength
|
|
68
|
+
def specifications_and_results(test_run) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
|
|
60
69
|
parse_all_specifications
|
|
61
70
|
parse_test_run test_run
|
|
62
71
|
parse_all_source_files
|
|
72
|
+
parse_decisions
|
|
63
73
|
link_all_specifications
|
|
64
74
|
link_all_protocols
|
|
65
75
|
link_all_source_files
|
|
76
|
+
link_all_decisions
|
|
66
77
|
check_wrong_specification_referenced
|
|
78
|
+
build_link_registry
|
|
67
79
|
create_index
|
|
68
80
|
render_all_specifications(@project_data.specifications)
|
|
69
81
|
render_all_specifications(@project_data.traceability_matrices)
|
|
@@ -71,22 +83,64 @@ class Project # rubocop:disable Metrics/ClassLength,Style/Documentation
|
|
|
71
83
|
render_all_protocols
|
|
72
84
|
render_all_source_files
|
|
73
85
|
render_all_specifications(@project_data.implementation_matrices) # intentionally after source file rendering
|
|
86
|
+
render_decisions_overview
|
|
87
|
+
render_all_decisions
|
|
74
88
|
render_index
|
|
75
89
|
create_search_data
|
|
90
|
+
report_broken_links
|
|
91
|
+
report_rendered
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def report_rendered
|
|
95
|
+
root = @configuration.project_root_directory
|
|
96
|
+
base = root == Dir.pwd ? '.' : root
|
|
97
|
+
ConsoleReporter.result('rendering HTML', File.join(base, 'build', 'index.html'))
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Reports cross-document links that could not be resolved (ADR-186, SRS-094),
|
|
101
|
+
# naming the linking document. The build still completes.
|
|
102
|
+
def report_broken_links
|
|
103
|
+
broken = TextLine.broken_links
|
|
104
|
+
return if broken.empty?
|
|
105
|
+
|
|
106
|
+
ConsoleReporter.warn('broken links', broken.length)
|
|
107
|
+
broken.each { |b| puts ConsoleReporter.warn_detail(" #{b[:document] || '?'}: #{b[:target]}") }
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Assigns each document its generated output path (relative to the build root)
|
|
111
|
+
# and registers it for cross-document link resolution (ADR-186). Runs after all
|
|
112
|
+
# documents are parsed and before any rendering, so link targets are known.
|
|
113
|
+
def build_link_registry # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
|
|
114
|
+
reg = @project_data.link_registry
|
|
115
|
+
TextLine.link_registry = reg
|
|
116
|
+
TextLine.reset_broken_links
|
|
117
|
+
@project_data.specifications.each do |d|
|
|
118
|
+
d.output_rel_path = "specifications/#{d.id}/#{d.id}.html"
|
|
119
|
+
reg.register(d)
|
|
120
|
+
end
|
|
121
|
+
@project_data.protocols.each do |d|
|
|
122
|
+
d.output_rel_path = "tests/protocols/#{d.id}/#{d.id}.html"
|
|
123
|
+
reg.register(d)
|
|
124
|
+
end
|
|
125
|
+
@project_data.decisions.each do |d|
|
|
126
|
+
d.output_rel_path = "decisions/#{d.html_rel_path}"
|
|
127
|
+
reg.register(d)
|
|
128
|
+
end
|
|
129
|
+
@project_data.source_files.each do |d|
|
|
130
|
+
rel = d.path.sub("#{d.root_path}/", '')
|
|
131
|
+
d.output_rel_path = "source_files/#{d.repository}/#{rel}.html"
|
|
132
|
+
reg.register(d)
|
|
133
|
+
end
|
|
76
134
|
end
|
|
77
135
|
|
|
78
136
|
def parse_all_specifications
|
|
79
137
|
path = @configuration.project_root_directory
|
|
80
|
-
# do a lasy pass first to get the list of documents id
|
|
81
138
|
Dir.glob("#{path}/specifications/**/*.md").each do |f|
|
|
82
|
-
DocFabric.add_lazy_doc_id(f)
|
|
83
|
-
end
|
|
84
|
-
# parse documents in the second pass
|
|
85
|
-
Dir.glob("#{path}/specifications/**/*.md").each do |f| # rubocop:disable Style/CombinableLoops
|
|
86
139
|
doc = DocFabric.create_specification(f)
|
|
87
140
|
@project_data.specifications.append(doc)
|
|
88
141
|
@project_data.specifications_dictionary[doc.id.to_s.downcase] = doc
|
|
89
142
|
end
|
|
143
|
+
ConsoleReporter.count('parsing specifications', @project_data.specifications.length)
|
|
90
144
|
end
|
|
91
145
|
|
|
92
146
|
def parse_all_protocols
|
|
@@ -95,6 +149,7 @@ class Project # rubocop:disable Metrics/ClassLength,Style/Documentation
|
|
|
95
149
|
doc = DocFabric.create_protocol(f)
|
|
96
150
|
@project_data.protocols.append(doc)
|
|
97
151
|
end
|
|
152
|
+
ConsoleReporter.count('parsing test protocols', @project_data.protocols.length)
|
|
98
153
|
end
|
|
99
154
|
|
|
100
155
|
def parse_all_source_files
|
|
@@ -114,6 +169,19 @@ class Project # rubocop:disable Metrics/ClassLength,Style/Documentation
|
|
|
114
169
|
end
|
|
115
170
|
end
|
|
116
171
|
|
|
172
|
+
def parse_decisions
|
|
173
|
+
path = @configuration.project_root_directory
|
|
174
|
+
decisions_root = "#{path}/decisions"
|
|
175
|
+
Dir.glob("#{decisions_root}/**/*.md").each do |f|
|
|
176
|
+
doc = DocFabric.create_decision(f)
|
|
177
|
+
rel_dir = File.dirname(f.sub("#{decisions_root}/", ''))
|
|
178
|
+
doc.html_rel_path = rel_dir == '.' ? "#{doc.id}.html" : "#{rel_dir}/#{doc.id}.html"
|
|
179
|
+
@project_data.decisions.append(doc)
|
|
180
|
+
end
|
|
181
|
+
BaseDocument.show_decisions_link = @project_data.decisions.any?
|
|
182
|
+
ConsoleReporter.count('parsing decisions', @project_data.decisions.length)
|
|
183
|
+
end
|
|
184
|
+
|
|
117
185
|
def parse_test_run(test_run)
|
|
118
186
|
path = @configuration.project_root_directory
|
|
119
187
|
Dir.glob("#{path}/tests/runs/#{test_run}/**/*.md").each do |f|
|
|
@@ -138,6 +206,7 @@ class Project # rubocop:disable Metrics/ClassLength,Style/Documentation
|
|
|
138
206
|
@project_data.traceability_matrices.append doc
|
|
139
207
|
end
|
|
140
208
|
end
|
|
209
|
+
ConsoleReporter.count('traceability matrices', @project_data.traceability_matrices.length)
|
|
141
210
|
end
|
|
142
211
|
|
|
143
212
|
def link_all_protocols # rubocop:disable Metrics/MethodLength
|
|
@@ -154,6 +223,20 @@ class Project # rubocop:disable Metrics/ClassLength,Style/Documentation
|
|
|
154
223
|
doc = DocFabric.create_coverage_matrix(value)
|
|
155
224
|
@project_data.coverage_matrices.append doc
|
|
156
225
|
end
|
|
226
|
+
ConsoleReporter.count('coverage matrices', @project_data.coverage_matrices.length)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def link_all_decisions
|
|
230
|
+
number_of_links = 0
|
|
231
|
+
@project_data.decisions.each do |d|
|
|
232
|
+
@project_data.specifications.each do |s|
|
|
233
|
+
next unless d.up_link_docs.key?(s.id.to_s)
|
|
234
|
+
|
|
235
|
+
DocLinker.link_decision_to_spec(d, s)
|
|
236
|
+
number_of_links += 1
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
ConsoleReporter.count('decision links', number_of_links)
|
|
157
240
|
end
|
|
158
241
|
|
|
159
242
|
def link_all_source_files
|
|
@@ -164,6 +247,7 @@ class Project # rubocop:disable Metrics/ClassLength,Style/Documentation
|
|
|
164
247
|
doc = DocFabric.create_implementation_document(value)
|
|
165
248
|
@project_data.implementation_matrices.append doc
|
|
166
249
|
end
|
|
250
|
+
ConsoleReporter.count('implementation matrices', @project_data.implementation_matrices.length)
|
|
167
251
|
end
|
|
168
252
|
|
|
169
253
|
def check_wrong_specification_referenced # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
|
|
@@ -237,14 +321,12 @@ class Project # rubocop:disable Metrics/ClassLength,Style/Documentation
|
|
|
237
321
|
@index = Index.new(@project)
|
|
238
322
|
end
|
|
239
323
|
|
|
240
|
-
def render_all_specifications(spec_list)
|
|
324
|
+
def render_all_specifications(spec_list)
|
|
241
325
|
path = @configuration.project_root_directory
|
|
242
326
|
|
|
243
327
|
FileUtils.mkdir_p("#{path}/build/specifications")
|
|
244
328
|
|
|
245
329
|
spec_list.each do |doc|
|
|
246
|
-
doc.to_console
|
|
247
|
-
|
|
248
330
|
img_src_dir = "#{path}/specifications/#{doc.id}/img"
|
|
249
331
|
img_dst_dir = "#{path}/build/specifications/#{doc.id}/img"
|
|
250
332
|
|
|
@@ -280,8 +362,6 @@ class Project # rubocop:disable Metrics/ClassLength,Style/Documentation
|
|
|
280
362
|
FileUtils.mkdir_p("#{path}/build/source_files")
|
|
281
363
|
|
|
282
364
|
@project_data.source_files.each do |doc|
|
|
283
|
-
doc.to_console
|
|
284
|
-
|
|
285
365
|
doc.to_html("#{path}/build/source_files/")
|
|
286
366
|
end
|
|
287
367
|
end
|
|
@@ -290,11 +370,34 @@ class Project # rubocop:disable Metrics/ClassLength,Style/Documentation
|
|
|
290
370
|
path = @configuration.project_root_directory
|
|
291
371
|
|
|
292
372
|
doc = @index
|
|
293
|
-
doc.to_console
|
|
294
|
-
|
|
295
373
|
doc.to_html("#{path}/build/")
|
|
296
374
|
end
|
|
297
375
|
|
|
376
|
+
def render_decisions_overview
|
|
377
|
+
return if @project_data.decisions.empty?
|
|
378
|
+
|
|
379
|
+
path = @configuration.project_root_directory
|
|
380
|
+
FileUtils.mkdir_p("#{path}/build/decisions")
|
|
381
|
+
|
|
382
|
+
doc = DocFabric.create_decisions_overview(@project)
|
|
383
|
+
doc.to_html("#{path}/build/decisions/")
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
def render_all_decisions # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
|
|
387
|
+
return if @project_data.decisions.empty?
|
|
388
|
+
|
|
389
|
+
build_decisions_root = "#{@configuration.project_root_directory}/build/decisions"
|
|
390
|
+
@project_data.decisions.each do |doc|
|
|
391
|
+
out_dir_rel = File.dirname(doc.html_rel_path)
|
|
392
|
+
out_dir = out_dir_rel == '.' ? build_decisions_root : "#{build_decisions_root}/#{out_dir_rel}"
|
|
393
|
+
FileUtils.mkdir_p(out_dir)
|
|
394
|
+
depth = 1 + (out_dir_rel == '.' ? 0 : out_dir_rel.split('/').size)
|
|
395
|
+
doc.root_prefix = '../' * depth
|
|
396
|
+
doc.specifications_path = "./#{doc.root_prefix}specifications/"
|
|
397
|
+
doc.to_html(NavigationPane.new(doc), "#{out_dir}/")
|
|
398
|
+
end
|
|
399
|
+
end
|
|
400
|
+
|
|
298
401
|
def create_search_data
|
|
299
402
|
db = SpecificationsDb.new @project_data.specifications
|
|
300
403
|
data_path = "#{@configuration.project_root_directory}/build/data"
|