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.
@@ -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
- # Calculate the relative path depth to determine correct number of parent directory symbols
82
- relative_path = @path.sub("#{@root_path}/", '')
83
- depth = relative_path.count('/') + 1 # +1 for the repository folder
84
- depth += 1 # for the source_files folder
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
@@ -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) # rubocop:disable Metrics/MethodLength
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"
@@ -4,7 +4,7 @@ class ProjectConfiguration
4
4
  attr_accessor :project_root_directory, :parameters
5
5
 
6
6
  def initialize(path)
7
- @project_root_directory = path
7
+ @project_root_directory = File.expand_path(path)
8
8
  @parameters = {}
9
9
  load_project_file
10
10
  end