Almirah 0.4.2 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/bin/almirah +2 -2
  3. data/lib/almirah/doc_fabric.rb +7 -0
  4. data/lib/almirah/doc_items/blockquote.rb +15 -16
  5. data/lib/almirah/doc_items/code_block.rb +19 -21
  6. data/lib/almirah/doc_items/controlled_paragraph.rb +2 -2
  7. data/lib/almirah/doc_items/controlled_table.rb +11 -11
  8. data/lib/almirah/doc_items/controlled_table_row.rb +15 -18
  9. data/lib/almirah/doc_items/doc_footer.rb +10 -13
  10. data/lib/almirah/doc_items/doc_item.rb +2 -2
  11. data/lib/almirah/doc_items/heading.rb +1 -1
  12. data/lib/almirah/doc_items/image.rb +25 -27
  13. data/lib/almirah/doc_items/markdown_list.rb +2 -2
  14. data/lib/almirah/doc_items/markdown_table.rb +9 -2
  15. data/lib/almirah/doc_items/scope_table.rb +188 -0
  16. data/lib/almirah/doc_items/text_line.rb +26 -22
  17. data/lib/almirah/doc_items/todo_block.rb +15 -16
  18. data/lib/almirah/doc_items/work_item.rb +129 -0
  19. data/lib/almirah/doc_parser.rb +14 -8
  20. data/lib/almirah/doc_types/base_document.rb +21 -5
  21. data/lib/almirah/doc_types/coverage.rb +3 -3
  22. data/lib/almirah/doc_types/critical_chain_page.rb +218 -0
  23. data/lib/almirah/doc_types/decision.rb +152 -7
  24. data/lib/almirah/doc_types/decision_grouping.rb +17 -0
  25. data/lib/almirah/doc_types/decisions_overview.rb +591 -31
  26. data/lib/almirah/doc_types/implementation.rb +98 -98
  27. data/lib/almirah/doc_types/index.rb +11 -12
  28. data/lib/almirah/doc_types/persistent_document.rb +1 -1
  29. data/lib/almirah/doc_types/planning_dates.rb +17 -0
  30. data/lib/almirah/doc_types/protocol.rb +16 -20
  31. data/lib/almirah/doc_types/source_file.rb +1 -1
  32. data/lib/almirah/doc_types/traceability.rb +124 -133
  33. data/lib/almirah/dom/doc_section.rb +1 -1
  34. data/lib/almirah/navigation_pane.rb +9 -13
  35. data/lib/almirah/project/critical_chain.rb +117 -0
  36. data/lib/almirah/project/doc_linker.rb +4 -4
  37. data/lib/almirah/project/fever_chart.rb +94 -0
  38. data/lib/almirah/project/project_data.rb +8 -2
  39. data/lib/almirah/project/work_item_scheduler.rb +166 -0
  40. data/lib/almirah/project/working_calendar.rb +112 -0
  41. data/lib/almirah/project.rb +146 -9
  42. data/lib/almirah/project_configuration.rb +126 -29
  43. data/lib/almirah/project_template.rb +6 -6
  44. data/lib/almirah/project_utility.rb +3 -5
  45. data/lib/almirah/search/specifications_db.rb +2 -2
  46. data/lib/almirah/source_file_parser.rb +2 -3
  47. data/lib/almirah/templates/css/main.css +160 -0
  48. data/lib/almirah/templates/scripts/main.js +3 -1
  49. data/lib/almirah.rb +1 -2
  50. metadata +11 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5221af43605bcaab2815ac5c940d53188394a92f8f8fadbbd2def016d1d52328
4
- data.tar.gz: dd1d271774fe527d93512134b70cca3471048ff2a0859327c813eb75ebd35b54
3
+ metadata.gz: 46e67646a16db801d9dbe0cd2c2873a38e6e35068c5cde855179b9f5beb00089
4
+ data.tar.gz: c4953874698c949e6711ebdfa0c84dc0b56d6291674a7127d6ccfbb31802cea4
5
5
  SHA512:
6
- metadata.gz: 6cf6a8d5892855c7b8752a2a3d447fec79dcf89a6a796b931709b2207ceeaddaede5cf7ac262ce13ab8b47aa081ead25fd225f353fff3afb84bc01dfaf2dfbdd
7
- data.tar.gz: 9c0c6cffe8a41a19fc6778bcb0a3210f5a0ca3402b058d6b5f011b0434805fd11035be76a9ec6a742485839fe7523480d043923e582a2f0367c4fadb93922473
6
+ metadata.gz: 1401a30fbde34dd6c21b77685e3e2506753e7b302d528189e5d3d129eff6f9a15dd169f756be53ea1467dc5805fe36d0ce0678d2a446a866b18e95c66e70de64
7
+ data.tar.gz: 1db3dd4230d6b77154758bdbad3802fb121ba23c41a3ee33aeb8ba70ac0be4a7ac0d8e14ea05667869d2625fcc638ca2d900a47270f22ff9bac1077c932173ba
data/bin/almirah CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- require "almirah"
3
+ require 'almirah'
4
4
 
5
- CLI.start(ARGV)
5
+ CLI.start(ARGV)
@@ -7,6 +7,7 @@ require_relative 'doc_types/implementation'
7
7
  require_relative 'doc_types/traceability'
8
8
  require_relative 'doc_types/decision'
9
9
  require_relative 'doc_types/decisions_overview'
10
+ require_relative 'doc_types/critical_chain_page'
10
11
  require_relative 'doc_parser'
11
12
  require_relative 'source_file_parser'
12
13
  require_relative 'dom/document'
@@ -67,6 +68,8 @@ class DocFabric
67
68
  doc.extract_start_date
68
69
  doc.extract_target_date
69
70
  doc.extract_target_release_version
71
+ doc.extract_owners
72
+ doc.extract_scope_table
70
73
  doc
71
74
  end
72
75
 
@@ -74,6 +77,10 @@ class DocFabric
74
77
  DecisionsOverview.new project
75
78
  end
76
79
 
80
+ def self.create_critical_chain_page(project)
81
+ CriticalChainPage.new project
82
+ end
83
+
77
84
  def self.create_source_file(repository_path, path, repository_name)
78
85
  doc = SourceFile.new repository_path, path, repository_name
79
86
  DocFabric.parse_source_file doc
@@ -1,22 +1,21 @@
1
- require_relative "doc_item"
1
+ require_relative 'doc_item'
2
2
 
3
3
  class Blockquote < DocItem
4
+ attr_accessor :text
4
5
 
5
- attr_accessor :text
6
+ def initialize(text)
7
+ @text = text
8
+ end
6
9
 
7
- def initialize(text)
8
- @text = text
10
+ def to_html
11
+ s = ''
12
+ f_text = format_string(@text)
13
+ if @@html_table_render_in_progress
14
+ s += "</table>\n"
15
+ @@html_table_render_in_progress = false
9
16
  end
10
17
 
11
- def to_html
12
- s = ''
13
- f_text = format_string(@text)
14
- if @@html_table_render_in_progress
15
- s += "</table>\n"
16
- @@html_table_render_in_progress = false
17
- end
18
-
19
- s += "<div class=\"blockquote\"><p>#{f_text}</div>\n"
20
- return s
21
- end
22
- end
18
+ s += "<div class=\"blockquote\"><p>#{f_text}</div>\n"
19
+ s
20
+ end
21
+ end
@@ -1,27 +1,25 @@
1
- require_relative "doc_item"
1
+ require_relative 'doc_item'
2
2
 
3
3
  class CodeBlock < DocItem
4
+ attr_accessor :suggested_format, :code_lines
4
5
 
5
- attr_accessor :suggested_format
6
- attr_accessor :code_lines
6
+ def initialize(suggested_format)
7
+ @suggested_format = suggested_format
8
+ @code_lines = []
9
+ end
7
10
 
8
- def initialize(suggested_format)
9
- @suggested_format = suggested_format
10
- @code_lines = Array.new
11
- end
12
-
13
- def to_html
14
- s = ''
11
+ def to_html
12
+ s = ''
15
13
 
16
- if @@html_table_render_in_progress
17
- s += "</table>\n"
18
- @@html_table_render_in_progress = false
19
- end
20
- s += "<code>"
21
- @code_lines.each do |l|
22
- s += escape_text(l) + " </br>" # ADR-188/SRS-096: code content is inert text
23
- end
24
- s += "</code>\n"
25
- return s
14
+ if @@html_table_render_in_progress
15
+ s += "</table>\n"
16
+ @@html_table_render_in_progress = false
17
+ end
18
+ s += '<code>'
19
+ @code_lines.each do |l|
20
+ s += escape_text(l) + ' </br>' # ADR-188/SRS-096: code content is inert text
26
21
  end
27
- end
22
+ s += "</code>\n"
23
+ s
24
+ end
25
+ end
@@ -21,7 +21,7 @@ class ControlledParagraph < Paragraph
21
21
  s += "<table class=\"controlled\">\n"
22
22
  s += "\t<thead> <th>#</th> <th></th> <th title=\"Up-links\">UL</th> <th title=\"Down-links\">DL</th> \
23
23
  <th title=\"Test Coverage\">COV</th> <th title=\"Decision Record\">DR</th> </thead>\n"
24
- @@html_table_render_in_progress = true # rubocop:disable Style/ClassVars
24
+ @@html_table_render_in_progress = true
25
25
  end
26
26
  f_text = format_string(@text)
27
27
  s += "\t<tr>\n"
@@ -85,7 +85,7 @@ class ControlledParagraph < Paragraph
85
85
  end
86
86
 
87
87
  if @coverage_links
88
- if tmp = /^(.+)[.]\d+/.match(@coverage_links[0].id) # guessing that all the links refer to one document
88
+ if tmp = /^(.+)[.]\d+/.match(@coverage_links[0].id) # guessing that all the links refer to one document
89
89
  cov_link_doc_name = tmp[1].downcase
90
90
  end
91
91
  if @coverage_links.length == 1
@@ -1,10 +1,10 @@
1
1
  require_relative 'controlled_table_row'
2
2
  require_relative 'text_line'
3
3
 
4
- class ControlledTableColumn < TextLine # rubocop:disable Style/Documentation
4
+ class ControlledTableColumn < TextLine
5
5
  attr_accessor :text
6
6
 
7
- def initialize(text) # rubocop:disable Lint/MissingSuper
7
+ def initialize(text)
8
8
  @text = text.strip
9
9
  end
10
10
 
@@ -17,7 +17,7 @@ end
17
17
  class RegualrColumn < ControlledTableColumn
18
18
  end
19
19
 
20
- class TestStepNumberColumn < ControlledTableColumn # rubocop:disable Style/Documentation
20
+ class TestStepNumberColumn < ControlledTableColumn
21
21
  attr_accessor :step_number, :row_id
22
22
 
23
23
  def initialize(text)
@@ -32,7 +32,7 @@ class TestStepNumberColumn < ControlledTableColumn # rubocop:disable Style/Docum
32
32
  end
33
33
  end
34
34
 
35
- class TestStepResultColumn < ControlledTableColumn # rubocop:disable Style/Documentation
35
+ class TestStepResultColumn < ControlledTableColumn
36
36
  def to_html
37
37
  f_text = format_string(@text)
38
38
  case @text.downcase
@@ -46,7 +46,7 @@ class TestStepResultColumn < ControlledTableColumn # rubocop:disable Style/Docum
46
46
  end
47
47
  end
48
48
 
49
- class TestStepReferenceColumn < ControlledTableColumn # rubocop:disable Style/Documentation
49
+ class TestStepReferenceColumn < ControlledTableColumn
50
50
  attr_accessor :up_link_ids, :up_link_doc_ids, :parent_row
51
51
 
52
52
  def initialize(parent_row, text)
@@ -99,7 +99,7 @@ class TestStepReferenceColumn < ControlledTableColumn # rubocop:disable Style/Do
99
99
  end
100
100
  end
101
101
 
102
- def to_html # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
102
+ def to_html
103
103
  s = ''
104
104
  specifications_path = @parent_row.parent_doc.specifications_path
105
105
  if @up_link_ids
@@ -133,10 +133,10 @@ class TestStepReferenceColumn < ControlledTableColumn # rubocop:disable Style/Do
133
133
  end
134
134
  end
135
135
 
136
- class ControlledTable < DocItem # rubocop:disable Style/Documentation
136
+ class ControlledTable < DocItem
137
137
  attr_accessor :column_names, :rows, :is_separator_detected
138
138
 
139
- def initialize(doc, markdown_table) # rubocop:disable Lint/MissingSuper
139
+ def initialize(doc, markdown_table)
140
140
  @parent_doc = doc
141
141
  @parent_heading = doc.headings[-1]
142
142
 
@@ -158,7 +158,7 @@ class ControlledTable < DocItem # rubocop:disable Style/Documentation
158
158
  true
159
159
  end
160
160
 
161
- def format_columns(columns) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
161
+ def format_columns(columns)
162
162
  new_row = ControlledTableRow.new
163
163
  new_row.parent_doc = @parent_doc
164
164
 
@@ -198,11 +198,11 @@ class ControlledTable < DocItem # rubocop:disable Style/Documentation
198
198
  new_row
199
199
  end
200
200
 
201
- def to_html # rubocop:disable Metrics/MethodLength
201
+ def to_html
202
202
  s = ''
203
203
  if @@html_table_render_in_progress
204
204
  s += "</table>\n"
205
- @@html_table_render_in_progress = false # rubocop:disable Style/ClassVars
205
+ @@html_table_render_in_progress = false
206
206
  end
207
207
 
208
208
  s += "<table class=\"markdown_table\">\n"
@@ -1,23 +1,20 @@
1
- require_relative "doc_item"
1
+ require_relative 'doc_item'
2
2
 
3
3
  class ControlledTableRow < DocItem
4
+ attr_accessor :id, :up_link_ids, :columns
4
5
 
5
- attr_accessor :id
6
- attr_accessor :up_link_ids
7
- attr_accessor :columns
8
-
9
- def initialize
10
- @id = ""
11
- @up_link_ids = nil
12
- @columns = []
13
- end
6
+ def initialize
7
+ @id = ''
8
+ @up_link_ids = nil
9
+ @columns = []
10
+ end
14
11
 
15
- def to_html
16
- s = ""
17
- s += "\t<tr>\n"
18
- @columns.each do |col|
19
- s += col.to_html # "\t\t<td>#{col}</td>\n\r"
20
- end
21
- s += "\t</tr>\n"
12
+ def to_html
13
+ s = ''
14
+ s += "\t<tr>\n"
15
+ @columns.each do |col|
16
+ s += col.to_html # "\t\t<td>#{col}</td>\n\r"
22
17
  end
23
- end
18
+ s += "\t</tr>\n"
19
+ end
20
+ end
@@ -1,17 +1,14 @@
1
- require_relative "doc_item"
1
+ require_relative 'doc_item'
2
2
 
3
3
  class DocFooter < DocItem
4
+ def initialize; end
4
5
 
5
- def initialize
6
+ def to_html
7
+ s = ''
8
+ if @@html_table_render_in_progress
9
+ s += "</table>\n"
10
+ @@html_table_render_in_progress = false
6
11
  end
7
-
8
- def to_html
9
- s = ''
10
- if @@html_table_render_in_progress
11
- s += "</table>\n"
12
- @@html_table_render_in_progress = false
13
- end
14
- return s
15
- end
16
-
17
- end
12
+ s
13
+ end
14
+ end
@@ -2,13 +2,13 @@
2
2
 
3
3
  require_relative 'text_line'
4
4
 
5
- class DocItem < TextLine # rubocop:disable Style/Documentation
5
+ class DocItem < TextLine
6
6
  attr_accessor :parent_doc, :parent_heading
7
7
 
8
8
  @parent_doc = nil
9
9
  @parent_heading = nil
10
10
 
11
- @@html_table_render_in_progress = false # rubocop:disable Style/ClassVars
11
+ @@html_table_render_in_progress = false
12
12
 
13
13
  def initialize(doc)
14
14
  super()
@@ -92,7 +92,7 @@ class Heading < Paragraph
92
92
  heading_level = level.to_s
93
93
  heading_text = get_section_info_html
94
94
  if level.zero?
95
- heading_level = 1.to_s # Render Doc Title as a regular h1
95
+ heading_level = 1.to_s # Render Doc Title as a regular h1
96
96
  heading_text = escape_text(@text) # Doc Title does not have a section number
97
97
  end
98
98
  s += "<a name=\"#{@anchor_id}\"></a>\n"
@@ -1,35 +1,33 @@
1
- require_relative "doc_item"
1
+ require_relative 'doc_item'
2
2
 
3
3
  class Image < DocItem
4
+ attr_accessor :text, :path
4
5
 
5
- attr_accessor :text
6
- attr_accessor :path
6
+ def initialize(text, path)
7
+ @text = text
8
+ @path = path
9
+ end
7
10
 
8
- def initialize(text, path)
9
- @text = text
10
- @path = path
11
- end
11
+ def getTextWithoutSpaces
12
+ @text.split.join('-')
13
+ end
12
14
 
13
- def getTextWithoutSpaces
14
- return @text.split.join('-')
15
+ def to_html
16
+ s = ''
17
+ if @@html_table_render_in_progress
18
+ s += "</table>\n"
19
+ @@html_table_render_in_progress = false
15
20
  end
16
21
 
17
- def to_html
18
- s = ''
19
- if @@html_table_render_in_progress
20
- s += "</table>\n"
21
- @@html_table_render_in_progress = false
22
- end
23
-
24
- alt = escape_attr(@text)
25
- src = safe_url(@path)
26
- if src.nil? # disallowed scheme: render inert rather than emitting it (ADR-188, SRS-098)
27
- s += "<p style=\"margin-top: 15px;\">[image: #{alt}]"
28
- return s
29
- end
30
-
31
- s += "<p style=\"margin-top: 15px;\"><img src=\"#{escape_attr(src)}\" alt=\"#{alt}\" "
32
- s += "href=\"javascript:void(0)\" onclick=\"image_OnClick(this)\">"
33
- return s
22
+ alt = escape_attr(@text)
23
+ src = safe_url(@path)
24
+ if src.nil? # disallowed scheme: render inert rather than emitting it (ADR-188, SRS-098)
25
+ s += "<p style=\"margin-top: 15px;\">[image: #{alt}]"
26
+ return s
34
27
  end
35
- end
28
+
29
+ s += "<p style=\"margin-top: 15px;\"><img src=\"#{escape_attr(src)}\" alt=\"#{alt}\" "
30
+ s += 'href="javascript:void(0)" onclick="image_OnClick(this)">'
31
+ s
32
+ end
33
+ end
@@ -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 == '*' || c == '-'
76
+ if ['*', '-'].include?(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*([*\-]\s+)(.*)/.match(raw_text)
109
+ res = /^\s*([*-]\s+)(.*)/.match(raw_text)
110
110
  return true if res
111
111
 
112
112
  false
@@ -6,7 +6,7 @@ class MarkdownTable < DocItem
6
6
  attr_accessor :column_names, :rows, :heading_row, :is_separator_detected, :column_aligns,
7
7
  :is_decision_status_table
8
8
 
9
- def initialize(doc, heading_row) # rubocop:disable Metrics/MethodLength
9
+ def initialize(doc, heading_row)
10
10
  super(doc)
11
11
  @heading_row = heading_row
12
12
 
@@ -54,6 +54,13 @@ class MarkdownTable < DocItem
54
54
  true
55
55
  end
56
56
 
57
+ # Header-addressed cell grid (each row an array of stripped cell strings).
58
+ # Named alongside ScopeTable#cells so Decision's Scope/Status/version readers
59
+ # can treat either table type uniformly (ADR-194).
60
+ def cells
61
+ @rows
62
+ end
63
+
57
64
  def to_html
58
65
  s = ''
59
66
  if @@html_table_render_in_progress
@@ -80,7 +87,7 @@ class MarkdownTable < DocItem
80
87
  row.each_with_index do |col, index|
81
88
  cell = col
82
89
  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
90
+ if cell.to_i.positive? && cell.to_i.to_s == cell # autoalign cells with numbers
84
91
  s += "\t\t<td style=\"text-align: center;\">#{cell}</td>\n"
85
92
  else
86
93
  align = ''
@@ -0,0 +1,188 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'date'
4
+ require_relative 'controlled_table'
5
+ require_relative 'work_item'
6
+
7
+ # Purpose-built controlled table for a Decision Record's `# Scope` section
8
+ # (ADR-194). It is modelled on ControlledTable but addresses its columns by
9
+ # header text (not position) and has no fixed link column: it locates `#`,
10
+ # `Item`, `Owner`, `Depends On`, and `Status` wherever the author placed them.
11
+ #
12
+ # Each data row becomes a WorkItem (the dependency-network node). The table also
13
+ # keeps a plain header-addressed cell grid (`cells`) so the ADR-193 Scope
14
+ # readers (owners / WIP / dates) keep working unchanged after the Scope section
15
+ # stopped being a plain MarkdownTable.
16
+ class ScopeTable < ControlledTable
17
+ attr_reader :cells, :work_items
18
+
19
+ def initialize(doc, markdown_table)
20
+ @parent_doc = doc
21
+ @parent_heading = doc.headings[-1]
22
+ @column_names = markdown_table.column_names
23
+ @is_separator_detected = markdown_table.is_separator_detected
24
+ @cells = []
25
+ @work_items = []
26
+ @col = locate_columns(@column_names)
27
+ # The switch to ScopeTable happens on the first data row, so the source
28
+ # MarkdownTable carries none yet; any it did are re-read for safety.
29
+ markdown_table.rows.each { |cells| append_cells(cells) }
30
+ end
31
+
32
+ def add_row(row)
33
+ append_cells(split_table_cells(row).map(&:strip))
34
+ true
35
+ end
36
+
37
+ # True when the table has a leading `#` step column, hence a per-row anchor a
38
+ # dependent record can deep-link to (ADR-194).
39
+ def step_column?
40
+ !@col[:step].nil?
41
+ end
42
+
43
+ # Render as a controlled table: an anchored, centered step number in the `#`
44
+ # column (so rows are linkable, like the protocol table) and formatted text
45
+ # for every other cell.
46
+ def to_html
47
+ s = +''
48
+ if @@html_table_render_in_progress
49
+ s << "</table>\n"
50
+ @@html_table_render_in_progress = false
51
+ end
52
+ s << "<table class=\"markdown_table\">\n\t<thead>"
53
+ @column_names.each { |h| s << " <th>#{format_string(h.strip)}</th>" }
54
+ s << " </thead>\n"
55
+ @cells.each_with_index { |cells, i| s << row_html(cells, @work_items[i]) }
56
+ s << "</table>\n"
57
+ s
58
+ end
59
+
60
+ private
61
+
62
+ def locate_columns(column_names)
63
+ index_of = lambda do |name|
64
+ column_names.each_with_index { |h, i| return i if h.to_s.strip == name }
65
+ nil
66
+ end
67
+ { step: index_of.call('#'), item: index_of.call('Item'), owner: index_of.call('Owner'),
68
+ depends_on: index_of.call('Depends On'), status: index_of.call('Status'),
69
+ est_focused: index_of.call('Est (focused)'), est_safe: index_of.call('Est (safe)'),
70
+ start_date: index_of.call('Start Date'), target_date: index_of.call('Target Date') }
71
+ end
72
+
73
+ def append_cells(cells)
74
+ @cells << cells
75
+ @work_items << build_work_item(cells)
76
+ end
77
+
78
+ def build_work_item(cells)
79
+ WorkItem.new(
80
+ record_id: @parent_doc.id,
81
+ step: step_number(cells, @cells.length), # 1-based row order fallback
82
+ activity: cell(cells, @col[:item]),
83
+ owner: cell(cells, @col[:owner]),
84
+ status: cell(cells, @col[:status]),
85
+ depends_on_refs: parse_depends_on(cell(cells, @col[:depends_on])),
86
+ start_date: parse_date(cell(cells, @col[:start_date])),
87
+ target_date: parse_date(cell(cells, @col[:target_date])),
88
+ **estimate_attrs(cells)
89
+ )
90
+ end
91
+
92
+ # A Scope Start/Target Date cell as a Date (ADR-213), or nil when blank or not
93
+ # in the DD-MM-YYYY form the rest of the planning views read (ADR-205).
94
+ def parse_date(text)
95
+ match = /\A(\d{2})-(\d{2})-(\d{4})\z/.match(text.to_s.strip)
96
+ return nil unless match
97
+
98
+ Date.new(match[3].to_i, match[2].to_i, match[1].to_i)
99
+ rescue ArgumentError
100
+ nil
101
+ end
102
+
103
+ # The ADR-195 estimate-derived attributes: focused and safe working-day
104
+ # estimates (blank/unparseable -> 0) and the owning record's sequence number
105
+ # (the critical-chain priority tiebreak).
106
+ def estimate_attrs(cells)
107
+ {
108
+ focused_estimate: parse_estimate(cell(cells, @col[:est_focused])),
109
+ safe_estimate: parse_estimate(cell(cells, @col[:est_safe])),
110
+ record_sequence: @parent_doc.sequence_number.to_i
111
+ }
112
+ end
113
+
114
+ # A Scope estimate cell as a non-negative number of working days (decimals
115
+ # allowed); blank, unparseable, or negative values count as 0 (ADR-195).
116
+ def parse_estimate(text)
117
+ value = Float(text.to_s.strip, exception: false)
118
+ value&.positive? ? value : 0.0
119
+ end
120
+
121
+ # The `#` column's integer when present; otherwise the row's 1-based position,
122
+ # so a record without the column keeps each row as its own sequential step.
123
+ def step_number(cells, row_position)
124
+ return row_position if @col[:step].nil?
125
+
126
+ value = cell(cells, @col[:step]).to_s.strip
127
+ value.match?(/\A\d+\z/) ? value.to_i : row_position
128
+ end
129
+
130
+ def parse_depends_on(text)
131
+ text.to_s.scan(/>\[([^\]]+)\]/).flatten.map(&:strip)
132
+ end
133
+
134
+ def cell(cells, index)
135
+ return '' if index.nil?
136
+
137
+ cells[index].to_s.strip
138
+ end
139
+
140
+ def row_html(cells, work_item)
141
+ s = +"\t<tr>\n"
142
+ cells.each_with_index { |value, index| s << cell_html(value, index, work_item) }
143
+ s << "\t</tr>\n"
144
+ s
145
+ end
146
+
147
+ def cell_html(value, index, work_item)
148
+ return depends_on_html(work_item) if index == @col[:depends_on]
149
+ return step_cell_html(value, work_item) if index == @col[:step]
150
+ return date_cell_html(value) if [@col[:start_date], @col[:target_date]].include?(index)
151
+
152
+ "\t\t<td>#{format_string(value.strip)}</td>\n"
153
+ end
154
+
155
+ # The Start/Target Date cells are kept on one line so a DD-MM-YYYY date never
156
+ # wraps at its hyphens in a narrow column (ADR-213).
157
+ def date_cell_html(value)
158
+ "\t\t<td class=\"scope_date\">#{format_string(value.strip)}</td>\n"
159
+ end
160
+
161
+ # The `#` cell is a named anchor (id + name), mirroring the test-step number
162
+ # column, so a dependent record's Depends On link can target this row.
163
+ def step_cell_html(value, work_item)
164
+ anchor = work_item.row_anchor
165
+ "\t\t<td style=\"text-align: center;\">" \
166
+ "<a name=\"#{anchor}\" id=\"#{anchor}\" href=\"##{anchor}\">#{format_string(value.strip)}</a></td>\n"
167
+ end
168
+
169
+ # Render each authored Depends On reference as a link to the resolved target
170
+ # record. When the target record has a `#` step column, the link deep-jumps to
171
+ # the aligned activity row (its step anchor); otherwise it opens the record
172
+ # page (no such anchor exists). Unresolved references render as broken-link
173
+ # spans (ADR-194 also reports them on the console).
174
+ def depends_on_html(work_item)
175
+ return "\t\t<td></td>\n" if work_item.depends_on_refs.empty?
176
+
177
+ links = work_item.depends_on_refs.map { |ref| dependency_ref_html(ref, work_item) }
178
+ "\t\t<td>#{links.join(', ')}</td>\n"
179
+ end
180
+
181
+ def dependency_ref_html(ref, work_item)
182
+ dep = work_item.resolved_dependencies[ref]
183
+ return "<span class=\"broken_link\" title=\"Unresolved Depends On\">#{ref}</span>" if dep.nil?
184
+
185
+ href = RelativeUrl.between(@parent_doc.output_rel_path, dep[:doc].output_rel_path, fragment: dep[:anchor])
186
+ "<a href=\"#{href}\" class=\"external\" title=\"Depends on #{dep[:label]}\">#{ref}</a>"
187
+ end
188
+ end