Almirah 0.3.1 → 0.4.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: be9e792ca43617f09611a801b8e8c8efde741ec531a97295de8d65378232c0d2
4
- data.tar.gz: 492000cb9aadaf0731e077b46ec3a32b573da9c8cd2acfaff6f282dec75c394d
3
+ metadata.gz: b3c6b1d68ee25c9b62690c4cdda45ec2d790960a6e75762576ddc60d6c3136e5
4
+ data.tar.gz: 334ef7251595119cd27bf980eeb53a8f02d913cde7d2fc5d0844a1d863b6c825
5
5
  SHA512:
6
- metadata.gz: 51e65415ee9d0a8c0f77d19d21ab525a01cbc4c025bc5572f92f2df13bc56ceed8b04be5e18d3c0308c468c43986dd267046d9b1c125e281ad01476b08a93569
7
- data.tar.gz: b64d9b5a0da5d254ecbc6dd861db05017b8cd81e13c0f58071413816d9b20d879187cbda5b2a703d5eae9f92ec044000010fc6b1b103fa30a9de04b265562cde
6
+ metadata.gz: 20383b85a3d59d35a3c6259095e7e8b6709055b6670e2b651fdecdd814eee54b71ebf7d2cc1958c85bc998a426361dfff470c1c10749044334ae7ec7f1a69616
7
+ data.tar.gz: d1d066ecfc0cfe644eaa28f42d793342e328f42d3eefc0e1fdeb2290c8ff281009bd59b99d23893604f89927ab698a922ef844af1e702c45a3a0a5d51926ff84
@@ -5,6 +5,8 @@ require_relative 'doc_types/protocol'
5
5
  require_relative 'doc_types/coverage'
6
6
  require_relative 'doc_types/implementation'
7
7
  require_relative 'doc_types/traceability'
8
+ require_relative 'doc_types/decision'
9
+ require_relative 'doc_types/decisions_overview'
8
10
  require_relative 'doc_parser'
9
11
  require_relative 'source_file_parser'
10
12
  require_relative 'dom/document'
@@ -64,6 +66,19 @@ class DocFabric
64
66
  doc
65
67
  end
66
68
 
69
+ def self.create_decision(path)
70
+ doc = Decision.new path
71
+ DocFabric.parse_document doc
72
+ doc.extract_current_status
73
+ doc.extract_start_date
74
+ doc.extract_target_release_version
75
+ doc
76
+ end
77
+
78
+ def self.create_decisions_overview(project)
79
+ DecisionsOverview.new project
80
+ end
81
+
67
82
  def self.create_source_file(repository_path, path, repository_name)
68
83
  doc = SourceFile.new repository_path, path, repository_name
69
84
  DocFabric.parse_source_file doc
@@ -78,7 +93,7 @@ class DocFabric
78
93
  DocParser.parse(doc, file_lines)
79
94
 
80
95
  # Build dom
81
- doc.dom = Document.new(doc.headings) if doc.is_a?(Specification) || doc.is_a?(Protocol)
96
+ doc.dom = Document.new(doc.headings) if doc.is_a?(Specification) || doc.is_a?(Protocol) || doc.is_a?(Decision)
82
97
  end
83
98
 
84
99
  def self.parse_source_file(doc)
@@ -2,7 +2,7 @@ require_relative 'paragraph'
2
2
 
3
3
  # <REQ> Implementa a controlled paragraph as a subclass of the DocItem >[SRS-001] </REQ>
4
4
  class ControlledParagraph < Paragraph
5
- attr_accessor :id, :up_link_ids, :down_links, :coverage_links, :source_code_links
5
+ attr_accessor :id, :up_link_ids, :down_links, :coverage_links, :source_code_links, :decision_record_links
6
6
 
7
7
  def initialize(doc, text, id)
8
8
  super(doc, text)
@@ -12,6 +12,7 @@ class ControlledParagraph < Paragraph
12
12
  @down_links = nil
13
13
  @coverage_links = nil
14
14
  @source_code_links = nil
15
+ @decision_record_links = nil
15
16
  end
16
17
 
17
18
  def to_html
@@ -19,7 +20,7 @@ class ControlledParagraph < Paragraph
19
20
  unless @@html_table_render_in_progress
20
21
  s += "<table class=\"controlled\">\n"
21
22
  s += "\t<thead> <th>#</th> <th></th> <th title=\"Up-links\">UL</th> <th title=\"Down-links\">DL</th> \
22
- <th title=\"Test Coverage\">COV</th> </thead>\n"
23
+ <th title=\"Test Coverage\">COV</th> <th title=\"Decision Record\">DR</th> </thead>\n"
23
24
  @@html_table_render_in_progress = true # rubocop:disable Style/ClassVars
24
25
  end
25
26
  f_text = format_string(@text)
@@ -108,6 +109,31 @@ class ControlledParagraph < Paragraph
108
109
  else
109
110
  s += "\t\t<td class=\"item_id\"></td>\n"
110
111
  end
112
+
113
+ if @decision_record_links
114
+ if @decision_record_links.length == 1
115
+ dr_doc = @decision_record_links[0].parent_doc
116
+ s += "\t\t<td class=\"item_id\">\
117
+ <a href=\"./../../decisions/#{dr_doc.html_rel_path}\" \
118
+ class=\"external\" title=\"Decision Record\">#{dr_doc.id.upcase}</a></td>\n"
119
+ else
120
+ s += "\t\t<td class=\"item_id\">"
121
+ s += "<div id=\"DR_#{@id}\" style=\"display: block;\">"
122
+ s += "<a href=\"#\" onclick=\"decisionLink_OnClick(this.parentElement); return false;\" \
123
+ class=\"external\" title=\"Number of decision records\">#{@decision_record_links.length}</a>"
124
+ s += '</div>'
125
+ s += "<div id=\"DRS_#{@id}\" style=\"display: none;\">"
126
+ @decision_record_links.each do |lnk|
127
+ dr_doc = lnk.parent_doc
128
+ s += "\t\t\t<a href=\"./../../decisions/#{dr_doc.html_rel_path}\" \
129
+ class=\"external\" title=\"Referenced in\">#{dr_doc.id.upcase}</a>\n<br>"
130
+ end
131
+ s += '</div>'
132
+ s += "</td>\n"
133
+ end
134
+ else
135
+ s += "\t\t<td class=\"item_id\"></td>\n"
136
+ end
111
137
  s += "\t</tr>\n"
112
138
  s
113
139
  end
@@ -101,13 +101,14 @@ class TestStepReferenceColumn < ControlledTableColumn # rubocop:disable Style/Do
101
101
 
102
102
  def to_html # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
103
103
  s = ''
104
+ specifications_path = @parent_row.parent_doc.specifications_path
104
105
  if @up_link_ids
105
106
  if @up_link_ids.length == 1
106
107
  if tmp = /^([a-zA-Z]+)-\d+/.match(@up_link_ids[0])
107
108
  up_link_doc_name = tmp[1].downcase
108
109
  end
109
110
  s += "\t\t<td class=\"item_id\" style=\"text-align: center;\">\
110
- <a href=\"./../../../specifications/#{up_link_doc_name}/#{up_link_doc_name}.html##{@up_link_ids[0]}\" \
111
+ <a href=\"#{specifications_path}#{up_link_doc_name}/#{up_link_doc_name}.html##{@up_link_ids[0]}\" \
111
112
  class=\"external\" title=\"Linked to\">#{@up_link_ids[0]}</a></td>\n"
112
113
  else
113
114
  s += "\t\t<td class=\"item_id\" style=\"text-align: center;\">"
@@ -120,7 +121,7 @@ class TestStepReferenceColumn < ControlledTableColumn # rubocop:disable Style/Do
120
121
  if tmp = /^([a-zA-Z]+)-\d+/.match(lnk)
121
122
  up_link_doc_name = tmp[1].downcase
122
123
  end
123
- s += "\t\t\t<a href=\"./../../../specifications/#{up_link_doc_name}/#{up_link_doc_name}.html##{lnk}\" \
124
+ s += "\t\t\t<a href=\"#{specifications_path}#{up_link_doc_name}/#{up_link_doc_name}.html##{lnk}\" \
124
125
  class=\"external\" title=\"Verifies\">#{lnk}</a>\n<br>"
125
126
  end
126
127
  s += '</div>'
@@ -184,7 +185,7 @@ class ControlledTable < DocItem # rubocop:disable Style/Documentation
184
185
  end
185
186
  end
186
187
 
187
- elsif index + 2 == columns.length # it is expected that test step result is placed to the pre-last column only
188
+ elsif (index + 2 == columns.length) && @parent_doc.instance_of?(Protocol) # test step result column applies only to Protocols
188
189
 
189
190
  col = TestStepResultColumn.new element
190
191
  new_row.columns.append col
@@ -73,7 +73,7 @@ class MarkdownList < DocItem
73
73
  s.each_char do |c|
74
74
  case state
75
75
  when 'looking_for_list_item_marker'
76
- if c == '*'
76
+ if c == '*' || c == '-'
77
77
  state = 'looking_for_space'
78
78
  elsif numeric?(c)
79
79
  state = 'looking_for_dot'
@@ -106,7 +106,7 @@ class MarkdownList < DocItem
106
106
  end
107
107
 
108
108
  def self.unordered_list_item?(raw_text)
109
- res = /(\*\s?)(.*)/.match(raw_text)
109
+ res = /^\s*([*\-]\s+)(.*)/.match(raw_text)
110
110
  return true if res
111
111
 
112
112
  false
@@ -3,9 +3,10 @@
3
3
  require_relative 'doc_item'
4
4
 
5
5
  class MarkdownTable < DocItem
6
- attr_accessor :column_names, :rows, :heading_row, :is_separator_detected, :column_aligns
6
+ attr_accessor :column_names, :rows, :heading_row, :is_separator_detected, :column_aligns,
7
+ :is_decision_status_table
7
8
 
8
- def initialize(doc, heading_row)
9
+ def initialize(doc, heading_row) # rubocop:disable Metrics/MethodLength
9
10
  super(doc)
10
11
  @heading_row = heading_row
11
12
 
@@ -18,6 +19,7 @@ class MarkdownTable < DocItem
18
19
  @rows = []
19
20
  @is_separator_detected = false
20
21
  @column_aligns = []
22
+ @is_decision_status_table = false
21
23
  end
22
24
 
23
25
  def add_separator(line)
@@ -69,10 +71,17 @@ class MarkdownTable < DocItem
69
71
  s += " </thead>\n"
70
72
 
71
73
  @rows.each do |row|
72
- s += "\t<tr>\n"
74
+ tr_class = if @is_decision_status_table && row[0].to_s.strip == '*'
75
+ ' class="current_status"'
76
+ else
77
+ ''
78
+ end
79
+ s += "\t<tr#{tr_class}>\n"
73
80
  row.each_with_index do |col, index|
74
- if col.to_i.positive? && col.to_i.to_s == col # autoalign cells with numbers
75
- s += "\t\t<td style=\"text-align: center;\">#{col}</td>\n"
81
+ cell = col
82
+ cell = '▶' if @is_decision_status_table && index.zero? && col.strip == '*'
83
+ if cell.to_i.positive? && cell.to_i.to_s == cell # autoalign cells with numbers
84
+ s += "\t\t<td style=\"text-align: center;\">#{cell}</td>\n"
76
85
  else
77
86
  align = ''
78
87
  case @column_aligns[index]
@@ -83,7 +92,7 @@ class MarkdownTable < DocItem
83
92
  when 'center'
84
93
  align = 'style="text-align: center;"'
85
94
  end
86
- f_text = format_string(col)
95
+ f_text = format_string(cell)
87
96
  s += "\t\t<td #{align}>#{f_text}</td>\n"
88
97
  end
89
98
  end
@@ -1,3 +1,5 @@
1
+ require 'cgi'
2
+
1
3
  class TextLineToken
2
4
  attr_accessor :value
3
5
 
@@ -54,14 +56,27 @@ class SquareBracketRightAndParentheseLeft < TextLineToken
54
56
  end
55
57
  end
56
58
 
59
+ class BacktickToken < TextLineToken
60
+ def initialize # rubocop:disable Lint/MissingSuper
61
+ @value = '`'
62
+ end
63
+ end
64
+
65
+ class InlineCodeToken < TextLineToken
66
+ def initialize(raw) # rubocop:disable Lint/MissingSuper
67
+ @value = raw
68
+ end
69
+ end
70
+
57
71
  class TextLineParser
58
72
  attr_accessor :supported_tokens
59
73
 
60
- def initialize # rubocop:disable Metrics/AbcSize
74
+ def initialize # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
61
75
  @supported_tokens = []
62
76
  @supported_tokens.append(BoldAndItalicToken.new)
63
77
  @supported_tokens.append(BoldToken.new)
64
78
  @supported_tokens.append(ItalicToken.new)
79
+ @supported_tokens.append(BacktickToken.new)
65
80
  @supported_tokens.append(SquareBracketRightAndParentheseLeft.new)
66
81
  @supported_tokens.append(ParentheseLeft.new)
67
82
  @supported_tokens.append(ParentheseRight.new)
@@ -70,7 +85,7 @@ class TextLineParser
70
85
  @supported_tokens.append(TextLineToken.new)
71
86
  end
72
87
 
73
- def tokenize(str)
88
+ def tokenize(str) # rubocop:disable Metrics/MethodLength,Metrics/CyclomaticComplexity,Metrics/AbcSize,Metrics/PerceivedComplexity
74
89
  result = []
75
90
  sl = str.length
76
91
  si = 0
@@ -79,29 +94,110 @@ class TextLineParser
79
94
  tl = t.value.length
80
95
  if tl != 0 # literal is the last supported token in the list
81
96
  projected_end_position = si + tl - 1
82
- next if projected_end_position > sl
97
+ next if projected_end_position >= sl
83
98
 
84
99
  buf = str[si..projected_end_position]
85
- if buf == t.value
100
+ next unless buf == t.value
101
+
102
+ if emphasis_token?(t) && !can_flank?(str, si, projected_end_position)
103
+ append_literal(result, buf)
104
+ else
86
105
  result.append(t)
87
- si = projected_end_position + 1
88
- break
89
106
  end
107
+ si = projected_end_position + 1
108
+ break
90
109
  else
91
- if result.length.positive? && (result[-1].instance_of? TextLineToken)
92
- literal = result[-1]
93
- literal.value += str[si]
94
- else
95
- literal = TextLineToken.new
96
- literal.value = str[si]
97
- result.append(literal)
98
- end
110
+ append_literal(result, str[si])
99
111
  si += 1
100
112
  end
101
113
  end
102
114
  end
115
+ fuse_backticks(result)
116
+ end
117
+
118
+ private
119
+
120
+ def fuse_backticks(tokens) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
121
+ result = []
122
+ i = 0
123
+ while i < tokens.length
124
+ if tokens[i].instance_of?(BacktickToken)
125
+ closer = next_backtick_index(tokens, i + 1)
126
+ if closer
127
+ raw = tokens[(i + 1)..(closer - 1)].map(&:value).join
128
+ result.append(InlineCodeToken.new(raw))
129
+ i = closer + 1
130
+ else
131
+ append_literal(result, '`')
132
+ i += 1
133
+ end
134
+ else
135
+ result.append(tokens[i])
136
+ i += 1
137
+ end
138
+ end
103
139
  result
104
140
  end
141
+
142
+ def next_backtick_index(tokens, start_idx)
143
+ idx = start_idx
144
+ while idx < tokens.length
145
+ return idx if tokens[idx].instance_of?(BacktickToken)
146
+
147
+ idx += 1
148
+ end
149
+ nil
150
+ end
151
+
152
+ def emphasis_token?(token)
153
+ token.is_a?(ItalicToken) || token.is_a?(BoldToken) || token.is_a?(BoldAndItalicToken)
154
+ end
155
+
156
+ def append_literal(result, text)
157
+ if !result.empty? && result[-1].instance_of?(TextLineToken)
158
+ result[-1].value += text
159
+ else
160
+ literal = TextLineToken.new
161
+ literal.value = text.dup
162
+ result.append(literal)
163
+ end
164
+ end
165
+
166
+ def can_flank?(str, start_idx, end_idx)
167
+ left_flanking?(str, start_idx, end_idx) || right_flanking?(str, start_idx, end_idx)
168
+ end
169
+
170
+ def left_flanking?(str, start_idx, end_idx)
171
+ after = char_at(str, end_idx + 1)
172
+ return false if after.nil? || whitespace?(after)
173
+ return true unless punctuation?(after)
174
+
175
+ before = char_at(str, start_idx - 1)
176
+ before.nil? || whitespace?(before)
177
+ end
178
+
179
+ def right_flanking?(str, start_idx, end_idx)
180
+ before = char_at(str, start_idx - 1)
181
+ return false if before.nil? || whitespace?(before)
182
+ return true unless punctuation?(before)
183
+
184
+ after = char_at(str, end_idx + 1)
185
+ after.nil? || whitespace?(after)
186
+ end
187
+
188
+ def char_at(str, idx)
189
+ return nil if idx.negative? || idx >= str.length
190
+
191
+ str[idx]
192
+ end
193
+
194
+ def whitespace?(char)
195
+ char.match?(/\s/)
196
+ end
197
+
198
+ def punctuation?(char)
199
+ char.match?(/[[:punct:]]/)
200
+ end
105
201
  end
106
202
 
107
203
  class TextLineBuilderContext
@@ -117,6 +213,10 @@ class TextLineBuilderContext
117
213
  str
118
214
  end
119
215
 
216
+ def inline_code(str)
217
+ str
218
+ end
219
+
120
220
  def link(_link_text, link_url)
121
221
  link_url
122
222
  end
@@ -230,6 +330,9 @@ class TextLineBuilder
230
330
  ti = ti_starting_position + 1
231
331
  end
232
332
 
333
+ when 'InlineCodeToken'
334
+ result += @builder_context.inline_code(token_list[ti].value)
335
+ ti += 1
233
336
  when 'TextLineToken', 'ParentheseLeft', 'ParentheseRight', 'SquareBracketRight'
234
337
  result += token_list[ti].value
235
338
  ti += 1
@@ -267,6 +370,10 @@ class TextLine < TextLineBuilderContext
267
370
  "<b><i>#{str}</i></b>"
268
371
  end
269
372
 
373
+ def inline_code(str)
374
+ "<code class=\"inline\">#{CGI.escapeHTML(str)}</code>"
375
+ end
376
+
270
377
  def link(link_text, link_url)
271
378
  # define default result first
272
379
  result = "<a target=\"_blank\" rel=\"noopener\" href=\"#{link_url}\" class=\"external\">#{link_text}</a>"
@@ -174,7 +174,7 @@ class DocParser # rubocop:disable Metrics/ClassLength,Style/Documentation
174
174
 
175
175
  doc.items.append(item)
176
176
 
177
- elsif res = /^(\*\s+)(.*)/.match(s) # check if unordered list start
177
+ elsif res = /^([*\-]\s+)(.*)/.match(s) # check if unordered list start
178
178
 
179
179
  if doc.title == ''
180
180
  # dummy section if root is not a Document Title (level 0)
@@ -248,8 +248,9 @@ class DocParser # rubocop:disable Metrics/ClassLength,Style/Documentation
248
248
 
249
249
  if temp_md_table
250
250
  if temp_md_table.is_separator_detected # if there is a separator
251
- # check if parent doc is a Protocol
252
- if doc.instance_of? Protocol
251
+ # check if parent doc is a Protocol, or a Decision Record inside an "Affected Documents" section
252
+ if doc.instance_of?(Protocol) ||
253
+ (doc.instance_of?(Decision) && in_section?(doc, 'Affected Documents'))
253
254
  # check if it is a controlled table
254
255
  tmp = /(.*)\s+>\[(\S*)\]/.match(row)
255
256
  if tmp && (temp_md_table.instance_of? MarkdownTable)
@@ -379,4 +380,20 @@ class DocParser # rubocop:disable Metrics/ClassLength,Style/Documentation
379
380
  end
380
381
  temp_md_table
381
382
  end
383
+
384
+ # Returns true when the most recently parsed heading is inside (or equal to) a section
385
+ # whose heading text matches `section_name`. Walks back through the heading list
386
+ # accumulating the ancestor chain (strictly decreasing levels).
387
+ def self.in_section?(doc, section_name)
388
+ return false if doc.headings.empty?
389
+
390
+ last_level = doc.headings[-1].level + 1
391
+ doc.headings.reverse_each do |h|
392
+ next if h.level >= last_level
393
+
394
+ last_level = h.level
395
+ return true if h.text.strip == section_name
396
+ end
397
+ false
398
+ end
382
399
  end
@@ -3,6 +3,10 @@
3
3
  class BaseDocument # rubocop:disable Style/Documentation
4
4
  attr_accessor :title, :id, :dom, :headings
5
5
 
6
+ class << self
7
+ attr_accessor :show_decisions_link
8
+ end
9
+
6
10
  def initialize
7
11
  @items = []
8
12
  @headings = []
@@ -21,6 +25,10 @@ class BaseDocument # rubocop:disable Style/Documentation
21
25
 
22
26
  output_file_path += if @id == 'index'
23
27
  "#{@id}.html"
28
+ elsif instance_of? DecisionsOverview
29
+ 'overview.html'
30
+ elsif instance_of? Decision
31
+ "#{@id}.html"
24
32
  else
25
33
  "#{@id}/#{@id}.html"
26
34
  end
@@ -55,14 +63,30 @@ class BaseDocument # rubocop:disable Style/Documentation
55
63
  elsif instance_of? Protocol
56
64
  file.puts '<link rel="stylesheet" href="../../../css/main.css">'
57
65
  file.puts '<script src="../../../scripts/main.js"></script>'
66
+ elsif instance_of? DecisionsOverview
67
+ file.puts '<link rel="stylesheet" href="../css/main.css">'
68
+ file.puts '<script src="../scripts/main.js"></script>'
69
+ file.puts '<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>'
70
+ elsif instance_of? Decision
71
+ file.puts "<link rel=\"stylesheet\" href=\"#{root_prefix}css/main.css\">"
72
+ file.puts "<script src=\"#{root_prefix}scripts/main.js\"></script>"
58
73
  end
59
74
  elsif s.include?('{{HOME_BUTTON}}')
60
75
  if @id == 'index'
61
76
  file.puts '<a id="home_menu_item" href="./index.html"><span><i class="fa fa-home" aria-hidden="true"></i></span>&nbsp;Home</a>'
77
+ file.puts decisions_link('./decisions/overview.html') if BaseDocument.show_decisions_link
62
78
  elsif instance_of? Protocol
63
79
  file.puts '<a id="index_menu_item" href="./../../../index.html"><span><i class="fa fa-info" aria-hidden="true"></i></span>&nbsp;Index</a>'
80
+ file.puts decisions_link('./../../../decisions/overview.html') if BaseDocument.show_decisions_link
81
+ elsif instance_of? DecisionsOverview
82
+ file.puts index_link('./../index.html')
83
+ file.puts decisions_link('./overview.html')
84
+ elsif instance_of? Decision
85
+ file.puts index_link("#{root_prefix}index.html")
86
+ file.puts decisions_link("#{root_prefix}decisions/overview.html")
64
87
  else
65
- file.puts '<a id="index_menu_item" href="./../../index.html"><span><i class="fa fa-info" aria-hidden="true"></i></span>&nbsp;Index</a>'
88
+ file.puts '<a id="index_menu_item" href="./../../index.html"><span><i class="fa fa-info" aria-hidden="true"></i></span>&nbsp;Index</a>'
89
+ file.puts decisions_link('./../../decisions/overview.html') if BaseDocument.show_decisions_link
66
90
  end
67
91
  elsif s.include?('{{GEM_VERSION}}')
68
92
  file.puts "(#{Gem.loaded_specs['Almirah'].version.version})"
@@ -72,4 +96,14 @@ class BaseDocument # rubocop:disable Style/Documentation
72
96
  end
73
97
  file.close
74
98
  end
99
+
100
+ def decisions_link(href)
101
+ icon = '<span><i class="fa fa-gavel" aria-hidden="true"></i></span>'
102
+ %(<a id="decisions_menu_item" href="#{href}">#{icon}&nbsp;Decision Records</a>)
103
+ end
104
+
105
+ def index_link(href)
106
+ icon = '<span><i class="fa fa-info" aria-hidden="true"></i></span>'
107
+ %(<a id="index_menu_item" href="#{href}">#{icon}&nbsp;Index</a>)
108
+ end
75
109
  end
@@ -0,0 +1,161 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'date'
4
+ require_relative 'persistent_document'
5
+ require_relative '../doc_items/heading'
6
+ require_relative '../doc_items/markdown_table'
7
+
8
+ class Decision < PersistentDocument # rubocop:disable Style/Documentation,Metrics/ClassLength
9
+ attr_accessor :path, :sequence_number, :record_type, :html_rel_path, :root_prefix, :current_status,
10
+ :start_date, :target_release_version, :specifications_path, :wrong_links_hash
11
+
12
+ def initialize(file_path)
13
+ super
14
+ @path = file_path
15
+ stem = File.basename(file_path, File.extname(file_path))
16
+ assign_id_parts(stem)
17
+ @current_status = nil
18
+ @start_date = nil
19
+ @target_release_version = nil
20
+ @wrong_links_hash = {}
21
+ end
22
+
23
+ def to_console
24
+ puts "\e[36mDecision: #{@id}\e[0m"
25
+ end
26
+
27
+ def to_html(nav_pane, output_file_path)
28
+ html_rows = []
29
+ html_rows.append('')
30
+
31
+ @items.each do |item|
32
+ html_rows.append item.to_html
33
+ end
34
+
35
+ save_html_to_file(html_rows, nav_pane, output_file_path)
36
+ end
37
+
38
+ def extract_current_status
39
+ status_table = find_section_table('Status')
40
+ return if status_table.nil?
41
+
42
+ status_table.is_decision_status_table = true
43
+ marker_rows = status_table.rows.select { |row| row[0].to_s.strip == '*' }
44
+ @current_status = marker_rows.length == 1 ? marker_rows[0][-1].to_s.strip : nil
45
+ end
46
+
47
+ def extract_start_date
48
+ dates = collect_dates('Status', 'Date') + collect_dates('Scope', 'Start Date')
49
+ @start_date = dates.min
50
+ end
51
+
52
+ def extract_target_release_version
53
+ @target_release_version = lookup_cell(
54
+ section_name: 'Software Versions',
55
+ key_column: 'Software Version Category',
56
+ value_column: 'Software Version ID',
57
+ key: 'Target Release Version'
58
+ )
59
+ end
60
+
61
+ def effective_status_on(date) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
62
+ table = find_section_table('Status')
63
+ return nil if table.nil?
64
+
65
+ date_idx = column_index(table, 'Date')
66
+ status_idx = column_index(table, 'Status')
67
+ return nil if date_idx.nil? || status_idx.nil?
68
+
69
+ best_idx = nil
70
+ best_date = nil
71
+ table.rows.each_with_index do |row, i|
72
+ parsed = parse_dd_mm_yyyy(row[date_idx])
73
+ next if parsed.nil? || parsed > date
74
+
75
+ if best_date.nil? || parsed > best_date || (parsed == best_date && i > best_idx)
76
+ best_date = parsed
77
+ best_idx = i
78
+ end
79
+ end
80
+ return nil if best_idx.nil?
81
+
82
+ status = table.rows[best_idx][status_idx].to_s.strip
83
+ status.empty? ? nil : status
84
+ end
85
+
86
+ private
87
+
88
+ def lookup_cell(section_name:, key_column:, value_column:, key:) # rubocop:disable Metrics/AbcSize
89
+ table = find_section_table(section_name)
90
+ return nil if table.nil?
91
+
92
+ key_idx = column_index(table, key_column)
93
+ value_idx = column_index(table, value_column)
94
+ return nil if key_idx.nil? || value_idx.nil?
95
+
96
+ row = table.rows.find { |r| r[key_idx].to_s.strip == key }
97
+ return nil if row.nil?
98
+
99
+ cell = row[value_idx].to_s.strip
100
+ cell.empty? ? nil : cell
101
+ end
102
+
103
+ def find_section_table(section_name) # rubocop:disable Metrics/MethodLength,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
104
+ in_section = false
105
+ section_level = nil
106
+ @items.each do |item|
107
+ if item.is_a?(Heading)
108
+ if !in_section && item.text.strip == section_name
109
+ in_section = true
110
+ section_level = item.level
111
+ elsif in_section && item.level <= section_level
112
+ return nil
113
+ end
114
+ elsif in_section && item.is_a?(MarkdownTable)
115
+ return item
116
+ end
117
+ end
118
+ nil
119
+ end
120
+
121
+ def collect_dates(section_name, column_name)
122
+ table = find_section_table(section_name)
123
+ return [] if table.nil?
124
+
125
+ col_index = column_index(table, column_name)
126
+ return [] if col_index.nil?
127
+
128
+ table.rows.filter_map { |row| parse_dd_mm_yyyy(row[col_index]) }
129
+ end
130
+
131
+ def column_index(table, column_name)
132
+ table.column_names.each_with_index do |name, idx|
133
+ return idx if name.to_s.strip == column_name
134
+ end
135
+ nil
136
+ end
137
+
138
+ def parse_dd_mm_yyyy(value)
139
+ return nil if value.nil?
140
+
141
+ match = /\A(\d{2})-(\d{2})-(\d{4})\z/.match(value.to_s.strip)
142
+ return nil unless match
143
+
144
+ Date.new(match[3].to_i, match[2].to_i, match[1].to_i)
145
+ rescue ArgumentError
146
+ nil
147
+ end
148
+
149
+ def assign_id_parts(stem)
150
+ match = stem.match(/\A([A-Za-z]+)-(\d+)/)
151
+ if match
152
+ @id = "#{match[1]}-#{match[2]}".downcase
153
+ @record_type = match[1].upcase
154
+ @sequence_number = match[2]
155
+ else
156
+ @id = stem.downcase
157
+ @record_type = nil
158
+ @sequence_number = nil
159
+ end
160
+ end
161
+ end
@@ -0,0 +1,158 @@
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
+ def render_charts_grid # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
76
+ counts = @project.project_data.decisions.each_with_object(Hash.new(0)) do |item, cntr|
77
+ cntr[item.record_type] += 1 if item.record_type
78
+ end
79
+ labels = counts.keys.sort
80
+ data = labels.map { |k| counts[k] }
81
+ pie_colors = labels.each_with_index.map { |_, i| palette_rgba(i, 0.5) }
82
+
83
+ velocity = velocity_chart_data
84
+
85
+ <<~HTML
86
+ <div class="decisions_overview_charts">
87
+ \t<div class="chart_cell">
88
+ \t\t<canvas id="decisions_type_pie"></canvas>
89
+ \t\t<script>
90
+ \t\t\tnew Chart(document.getElementById('decisions_type_pie'), {
91
+ \t\t\t\ttype: 'pie',
92
+ \t\t\t\tdata: {
93
+ \t\t\t\t\tlabels: #{labels.to_json},
94
+ \t\t\t\t\tdatasets: [{
95
+ \t\t\t\t\t\tlabel: 'Decision records',
96
+ \t\t\t\t\t\tdata: #{data.to_json},
97
+ \t\t\t\t\t\tbackgroundColor: #{pie_colors.to_json},
98
+ \t\t\t\t\t\tborderWidth: 0
99
+ \t\t\t\t\t}]
100
+ \t\t\t\t},
101
+ \t\t\t\toptions: { plugins: { title: { display: true, text: 'Decision Records by Type' } } }
102
+ \t\t\t});
103
+ \t\t</script>
104
+ \t</div>
105
+ \t<div class="chart_cell">
106
+ \t\t<canvas id="decisions_velocity_bar"></canvas>
107
+ \t\t<script>
108
+ \t\t\tnew Chart(document.getElementById('decisions_velocity_bar'), {
109
+ \t\t\t\ttype: 'bar',
110
+ \t\t\t\tdata: #{velocity.to_json},
111
+ \t\t\t\toptions: {
112
+ \t\t\t\t\tplugins: { title: { display: true, text: 'Decision Records by Status Over Time' } },
113
+ \t\t\t\t\tscales: { x: { stacked: true }, y: { stacked: true, ticks: { precision: 0 } } }
114
+ \t\t\t\t}
115
+ \t\t\t});
116
+ \t\t</script>
117
+ \t</div>
118
+ \t<div class="chart_cell"></div>
119
+ </div>
120
+ HTML
121
+ end
122
+
123
+ def velocity_chart_data(reference_date: Date.today, weeks: 6) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
124
+ fridays = recent_fridays(reference_date, weeks)
125
+ segments = []
126
+ counts = {}
127
+
128
+ fridays.each_with_index do |friday, i|
129
+ @project.project_data.decisions.each do |doc|
130
+ status = doc.effective_status_on(friday)
131
+ next if status.nil?
132
+
133
+ unless counts.key?(status)
134
+ counts[status] = Array.new(fridays.length, 0)
135
+ segments << status
136
+ end
137
+ counts[status][i] += 1
138
+ end
139
+ end
140
+
141
+ {
142
+ labels: fridays.map { |f| f.strftime('%d-%m-%Y') },
143
+ datasets: segments.map { |s| { label: s, data: counts[s] } }
144
+ }
145
+ end
146
+
147
+ def palette_rgba(index, alpha)
148
+ r, g, b = CHART_PALETTE[index % CHART_PALETTE.length]
149
+ "rgba(#{r}, #{g}, #{b}, #{alpha})"
150
+ end
151
+
152
+ def recent_fridays(reference_date, count)
153
+ friday_wday = 5
154
+ days_back = (reference_date.wday - friday_wday) % 7
155
+ most_recent = reference_date - days_back
156
+ (0...count).to_a.reverse.map { |i| most_recent - (7 * i) }
157
+ end
158
+ 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)
@@ -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,7 +1,7 @@
1
1
  class ProjectData
2
2
  attr_reader :specifications, :protocols, :traceability_matrices, :coverage_matrices, :source_files,
3
3
  :specifications_dictionary, :covered_specifications_dictionary, :implemented_specifications_dictionary,
4
- :implementation_matrices
4
+ :implementation_matrices, :decisions
5
5
 
6
6
  def initialize
7
7
  @specifications = []
@@ -10,6 +10,7 @@ class ProjectData
10
10
  @coverage_matrices = []
11
11
  @source_files = []
12
12
  @implementation_matrices = []
13
+ @decisions = []
13
14
 
14
15
  @specifications_dictionary = {}
15
16
  @covered_specifications_dictionary = {}
@@ -37,13 +37,15 @@ class Project # rubocop:disable Metrics/ClassLength,Style/Documentation
37
37
  FileUtils.copy_entry(src_folder, dst_folder)
38
38
  end
39
39
 
40
- def specifications_and_protocols # rubocop:disable Metrics/MethodLength
40
+ def specifications_and_protocols # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
41
41
  parse_all_specifications
42
42
  parse_all_protocols
43
43
  parse_all_source_files
44
+ parse_decisions
44
45
  link_all_specifications
45
46
  link_all_protocols
46
47
  link_all_source_files
48
+ link_all_decisions
47
49
  check_wrong_specification_referenced
48
50
  create_index
49
51
  render_all_specifications(@project_data.specifications)
@@ -52,17 +54,21 @@ class Project # rubocop:disable Metrics/ClassLength,Style/Documentation
52
54
  render_all_protocols
53
55
  render_all_source_files
54
56
  render_all_specifications(@project_data.implementation_matrices) # intentionally after source file rendering
57
+ render_decisions_overview
58
+ render_all_decisions
55
59
  render_index
56
60
  create_search_data
57
61
  end
58
62
 
59
- def specifications_and_results(test_run) # rubocop:disable Metrics/MethodLength
63
+ def specifications_and_results(test_run) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
60
64
  parse_all_specifications
61
65
  parse_test_run test_run
62
66
  parse_all_source_files
67
+ parse_decisions
63
68
  link_all_specifications
64
69
  link_all_protocols
65
70
  link_all_source_files
71
+ link_all_decisions
66
72
  check_wrong_specification_referenced
67
73
  create_index
68
74
  render_all_specifications(@project_data.specifications)
@@ -71,6 +77,8 @@ class Project # rubocop:disable Metrics/ClassLength,Style/Documentation
71
77
  render_all_protocols
72
78
  render_all_source_files
73
79
  render_all_specifications(@project_data.implementation_matrices) # intentionally after source file rendering
80
+ render_decisions_overview
81
+ render_all_decisions
74
82
  render_index
75
83
  create_search_data
76
84
  end
@@ -114,6 +122,18 @@ class Project # rubocop:disable Metrics/ClassLength,Style/Documentation
114
122
  end
115
123
  end
116
124
 
125
+ def parse_decisions
126
+ path = @configuration.project_root_directory
127
+ decisions_root = "#{path}/decisions"
128
+ Dir.glob("#{decisions_root}/**/*.md").each do |f|
129
+ doc = DocFabric.create_decision(f)
130
+ rel_dir = File.dirname(f.sub("#{decisions_root}/", ''))
131
+ doc.html_rel_path = rel_dir == '.' ? "#{doc.id}.html" : "#{rel_dir}/#{doc.id}.html"
132
+ @project_data.decisions.append(doc)
133
+ end
134
+ BaseDocument.show_decisions_link = @project_data.decisions.any?
135
+ end
136
+
117
137
  def parse_test_run(test_run)
118
138
  path = @configuration.project_root_directory
119
139
  Dir.glob("#{path}/tests/runs/#{test_run}/**/*.md").each do |f|
@@ -156,6 +176,16 @@ class Project # rubocop:disable Metrics/ClassLength,Style/Documentation
156
176
  end
157
177
  end
158
178
 
179
+ def link_all_decisions
180
+ @project_data.decisions.each do |d|
181
+ @project_data.specifications.each do |s|
182
+ next unless d.up_link_docs.key?(s.id.to_s)
183
+
184
+ DocLinker.link_decision_to_spec(d, s)
185
+ end
186
+ end
187
+ end
188
+
159
189
  def link_all_source_files
160
190
  return unless DocLinker.link_all_source_files(@project_data)
161
191
 
@@ -295,6 +325,33 @@ class Project # rubocop:disable Metrics/ClassLength,Style/Documentation
295
325
  doc.to_html("#{path}/build/")
296
326
  end
297
327
 
328
+ def render_decisions_overview
329
+ return if @project_data.decisions.empty?
330
+
331
+ path = @configuration.project_root_directory
332
+ FileUtils.mkdir_p("#{path}/build/decisions")
333
+
334
+ doc = DocFabric.create_decisions_overview(@project)
335
+ doc.to_console
336
+ doc.to_html("#{path}/build/decisions/")
337
+ end
338
+
339
+ def render_all_decisions # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
340
+ return if @project_data.decisions.empty?
341
+
342
+ build_decisions_root = "#{@configuration.project_root_directory}/build/decisions"
343
+ @project_data.decisions.each do |doc|
344
+ out_dir_rel = File.dirname(doc.html_rel_path)
345
+ out_dir = out_dir_rel == '.' ? build_decisions_root : "#{build_decisions_root}/#{out_dir_rel}"
346
+ FileUtils.mkdir_p(out_dir)
347
+ depth = 1 + (out_dir_rel == '.' ? 0 : out_dir_rel.split('/').size)
348
+ doc.root_prefix = '../' * depth
349
+ doc.specifications_path = "./#{doc.root_prefix}specifications/"
350
+ doc.to_console
351
+ doc.to_html(NavigationPane.new(doc), "#{out_dir}/")
352
+ end
353
+ end
354
+
298
355
  def create_search_data
299
356
  db = SpecificationsDb.new @project_data.specifications
300
357
  data_path = "#{@configuration.project_root_directory}/build/data"
@@ -64,11 +64,14 @@ table.markdown_table th{
64
64
  background-color:#EEEEEE;
65
65
  }
66
66
  table.markdown_table td{
67
- border: 1px solid #bbb;
67
+ border: 1px solid #bbb;
68
68
  padding: 4px;
69
69
  display: table-cell;
70
70
  vertical-align: inherit;
71
71
  }
72
+ table.markdown_table tr.current_status td {
73
+ background-color: #ffffee;
74
+ }
72
75
  table.controlled{
73
76
  border: 1px solid #e4e4e4;
74
77
  border-collapse: collapse;
@@ -110,9 +113,32 @@ table.controlled td.item_id {
110
113
  width: 3%;
111
114
  text-align: center;
112
115
  }
116
+ table.controlled td.item_type {
117
+ width: 5%;
118
+ text-align: center;
119
+ font-weight: normal;
120
+ }
121
+ table.controlled td.item_status {
122
+ width: 5%;
123
+ text-align: center;
124
+ white-space: nowrap;
125
+ }
113
126
  table.controlled td.item_text{
114
127
  text-align: left;
115
128
  }
129
+ .decisions_overview_charts {
130
+ display: grid;
131
+ grid-template-columns: repeat(3, 1fr);
132
+ gap: 16px;
133
+ margin-bottom: 16px;
134
+ }
135
+ .decisions_overview_charts .chart_cell {
136
+ min-height: 240px;
137
+ }
138
+ .decisions_overview_charts .chart_cell canvas {
139
+ max-width: 100%;
140
+ max-height: 300px;
141
+ }
116
142
  table.controlled:not(.odd-even) tbody tr:nth-child(odd) { background-color:#f6f7f8; }
117
143
  table.controlled:not(.odd-even) tbody tr:nth-child(even) { background-color: #fff; }
118
144
  table.controlled:not(.odd-even) tbody tr:nth-child(odd):hover,
@@ -146,6 +172,15 @@ code {
146
172
  margin-top: 4px;
147
173
  margin-bottom: 4px;
148
174
  }
175
+ code.inline {
176
+ display: inline;
177
+ background:#f4f4f4;
178
+ border: 1px solid #ddd;
179
+ border-left: 1px solid #ddd;
180
+ border-radius: 3px;
181
+ padding: 0 4px;
182
+ margin: 0;
183
+ }
149
184
  div.todoblock {
150
185
  display: block;
151
186
  background:#fcc;
@@ -50,7 +50,13 @@ function openNav() {
50
50
  clicked.style.display = 'none';
51
51
  id_parts = clicked.id.split("_");
52
52
  required_id = "COVS_" + id_parts[1];
53
- document.getElementById(required_id).style.display = 'block';
53
+ document.getElementById(required_id).style.display = 'block';
54
+ }
55
+ function decisionLink_OnClick(clicked){
56
+ clicked.style.display = 'none';
57
+ id_parts = clicked.id.split("_");
58
+ required_id = "DRS_" + id_parts[1];
59
+ document.getElementById(required_id).style.display = 'block';
54
60
  }
55
61
  function upLink_OnClick(clicked){
56
62
  clicked.style.display = 'none';
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: Almirah
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Oleksandr Ivanov
@@ -78,6 +78,8 @@ files:
78
78
  - lib/almirah/doc_parser.rb
79
79
  - lib/almirah/doc_types/base_document.rb
80
80
  - lib/almirah/doc_types/coverage.rb
81
+ - lib/almirah/doc_types/decision.rb
82
+ - lib/almirah/doc_types/decisions_overview.rb
81
83
  - lib/almirah/doc_types/implementation.rb
82
84
  - lib/almirah/doc_types/index.rb
83
85
  - lib/almirah/doc_types/persistent_document.rb