Almirah 0.4.1 → 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.
- checksums.yaml +4 -4
- data/bin/almirah +2 -2
- data/lib/almirah/doc_fabric.rb +8 -0
- data/lib/almirah/doc_items/blockquote.rb +15 -16
- data/lib/almirah/doc_items/code_block.rb +19 -21
- data/lib/almirah/doc_items/controlled_paragraph.rb +2 -2
- data/lib/almirah/doc_items/controlled_table.rb +11 -11
- data/lib/almirah/doc_items/controlled_table_row.rb +15 -18
- data/lib/almirah/doc_items/doc_footer.rb +10 -13
- data/lib/almirah/doc_items/doc_item.rb +2 -2
- data/lib/almirah/doc_items/heading.rb +21 -3
- data/lib/almirah/doc_items/image.rb +25 -20
- data/lib/almirah/doc_items/markdown_list.rb +2 -2
- data/lib/almirah/doc_items/markdown_table.rb +10 -3
- data/lib/almirah/doc_items/scope_table.rb +188 -0
- data/lib/almirah/doc_items/text_line.rb +52 -27
- data/lib/almirah/doc_items/todo_block.rb +15 -16
- data/lib/almirah/doc_items/work_item.rb +129 -0
- data/lib/almirah/doc_parser.rb +14 -8
- data/lib/almirah/doc_types/base_document.rb +21 -5
- data/lib/almirah/doc_types/coverage.rb +3 -3
- data/lib/almirah/doc_types/critical_chain_page.rb +218 -0
- data/lib/almirah/doc_types/decision.rb +158 -7
- data/lib/almirah/doc_types/decision_grouping.rb +17 -0
- data/lib/almirah/doc_types/decisions_overview.rb +593 -32
- data/lib/almirah/doc_types/implementation.rb +98 -98
- data/lib/almirah/doc_types/index.rb +11 -12
- data/lib/almirah/doc_types/persistent_document.rb +1 -1
- data/lib/almirah/doc_types/planning_dates.rb +17 -0
- data/lib/almirah/doc_types/protocol.rb +16 -20
- data/lib/almirah/doc_types/source_file.rb +1 -1
- data/lib/almirah/doc_types/traceability.rb +124 -133
- data/lib/almirah/dom/doc_section.rb +2 -2
- data/lib/almirah/dom/document.rb +1 -1
- data/lib/almirah/html_safe.rb +39 -0
- data/lib/almirah/link_registry.rb +21 -6
- data/lib/almirah/navigation_pane.rb +9 -13
- data/lib/almirah/project/critical_chain.rb +117 -0
- data/lib/almirah/project/doc_linker.rb +4 -4
- data/lib/almirah/project/fever_chart.rb +94 -0
- data/lib/almirah/project/project_data.rb +8 -2
- data/lib/almirah/project/work_item_scheduler.rb +166 -0
- data/lib/almirah/project/working_calendar.rb +112 -0
- data/lib/almirah/project.rb +146 -9
- data/lib/almirah/project_configuration.rb +126 -29
- data/lib/almirah/project_template.rb +6 -6
- data/lib/almirah/project_utility.rb +3 -5
- data/lib/almirah/search/specifications_db.rb +2 -2
- data/lib/almirah/source_file_parser.rb +2 -3
- data/lib/almirah/templates/css/main.css +168 -1
- data/lib/almirah/templates/scripts/main.js +7 -2
- data/lib/almirah/templates/scripts/orama_search.js +17 -4
- data/lib/almirah.rb +1 -2
- metadata +12 -2
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
require 'cgi'
|
|
2
2
|
require 'uri'
|
|
3
3
|
require_relative '../relative_url'
|
|
4
|
+
require_relative '../html_safe'
|
|
4
5
|
|
|
5
6
|
class TextLineToken
|
|
6
7
|
attr_accessor :value
|
|
@@ -11,25 +12,25 @@ class TextLineToken
|
|
|
11
12
|
end
|
|
12
13
|
|
|
13
14
|
class ItalicToken < TextLineToken
|
|
14
|
-
def initialize
|
|
15
|
+
def initialize
|
|
15
16
|
@value = '*'
|
|
16
17
|
end
|
|
17
18
|
end
|
|
18
19
|
|
|
19
20
|
class BoldToken < TextLineToken
|
|
20
|
-
def initialize
|
|
21
|
+
def initialize
|
|
21
22
|
@value = '**'
|
|
22
23
|
end
|
|
23
24
|
end
|
|
24
25
|
|
|
25
26
|
class BoldAndItalicToken < TextLineToken
|
|
26
|
-
def initialize
|
|
27
|
+
def initialize
|
|
27
28
|
@value = '***'
|
|
28
29
|
end
|
|
29
30
|
end
|
|
30
31
|
|
|
31
32
|
class ParentheseLeft < TextLineToken
|
|
32
|
-
def initialize
|
|
33
|
+
def initialize
|
|
33
34
|
@value = '('
|
|
34
35
|
end
|
|
35
36
|
end
|
|
@@ -41,7 +42,7 @@ class ParentheseRight < TextLineToken
|
|
|
41
42
|
end
|
|
42
43
|
|
|
43
44
|
class SquareBracketLeft < TextLineToken
|
|
44
|
-
def initialize
|
|
45
|
+
def initialize
|
|
45
46
|
@value = '['
|
|
46
47
|
end
|
|
47
48
|
end
|
|
@@ -53,13 +54,13 @@ class SquareBracketRight < TextLineToken
|
|
|
53
54
|
end
|
|
54
55
|
|
|
55
56
|
class DoubleSquareBracketLeft < TextLineToken
|
|
56
|
-
def initialize
|
|
57
|
+
def initialize
|
|
57
58
|
@value = '[['
|
|
58
59
|
end
|
|
59
60
|
end
|
|
60
61
|
|
|
61
62
|
class DoubleSquareBracketRight < TextLineToken
|
|
62
|
-
def initialize
|
|
63
|
+
def initialize
|
|
63
64
|
@value = ']]'
|
|
64
65
|
end
|
|
65
66
|
end
|
|
@@ -71,13 +72,13 @@ class SquareBracketRightAndParentheseLeft < TextLineToken
|
|
|
71
72
|
end
|
|
72
73
|
|
|
73
74
|
class BacktickToken < TextLineToken
|
|
74
|
-
def initialize
|
|
75
|
+
def initialize
|
|
75
76
|
@value = '`'
|
|
76
77
|
end
|
|
77
78
|
end
|
|
78
79
|
|
|
79
80
|
class InlineCodeToken < TextLineToken
|
|
80
|
-
def initialize(raw)
|
|
81
|
+
def initialize(raw)
|
|
81
82
|
@value = raw
|
|
82
83
|
end
|
|
83
84
|
end
|
|
@@ -85,7 +86,7 @@ end
|
|
|
85
86
|
class TextLineParser
|
|
86
87
|
attr_accessor :supported_tokens
|
|
87
88
|
|
|
88
|
-
def initialize
|
|
89
|
+
def initialize
|
|
89
90
|
@supported_tokens = []
|
|
90
91
|
@supported_tokens.append(BoldAndItalicToken.new)
|
|
91
92
|
@supported_tokens.append(BoldToken.new)
|
|
@@ -101,7 +102,7 @@ class TextLineParser
|
|
|
101
102
|
@supported_tokens.append(TextLineToken.new)
|
|
102
103
|
end
|
|
103
104
|
|
|
104
|
-
def tokenize(str)
|
|
105
|
+
def tokenize(str)
|
|
105
106
|
result = []
|
|
106
107
|
sl = str.length
|
|
107
108
|
si = 0
|
|
@@ -133,7 +134,7 @@ class TextLineParser
|
|
|
133
134
|
|
|
134
135
|
private
|
|
135
136
|
|
|
136
|
-
def fuse_backticks(tokens)
|
|
137
|
+
def fuse_backticks(tokens)
|
|
137
138
|
result = []
|
|
138
139
|
i = 0
|
|
139
140
|
while i < tokens.length
|
|
@@ -217,6 +218,12 @@ class TextLineParser
|
|
|
217
218
|
end
|
|
218
219
|
|
|
219
220
|
class TextLineBuilderContext
|
|
221
|
+
# Literal (non-markup) text run. Subclasses encode it for the HTML context;
|
|
222
|
+
# the base context leaves it untouched for plain reconstruction/unit tests.
|
|
223
|
+
def literal_text(str)
|
|
224
|
+
str
|
|
225
|
+
end
|
|
226
|
+
|
|
220
227
|
def italic(str)
|
|
221
228
|
str
|
|
222
229
|
end
|
|
@@ -249,7 +256,7 @@ class TextLineBuilder
|
|
|
249
256
|
@builder_context = builder_context
|
|
250
257
|
end
|
|
251
258
|
|
|
252
|
-
def restore(token_list)
|
|
259
|
+
def restore(token_list)
|
|
253
260
|
result = ''
|
|
254
261
|
return '' if token_list.nil?
|
|
255
262
|
|
|
@@ -363,7 +370,11 @@ class TextLineBuilder
|
|
|
363
370
|
tii += 1
|
|
364
371
|
end
|
|
365
372
|
if is_found
|
|
366
|
-
|
|
373
|
+
# URL is reconstructed raw (not via restore) so scheme classification
|
|
374
|
+
# and file-path resolution see the original characters; link() applies
|
|
375
|
+
# attribute escaping and the scheme allow-list (ADR-188).
|
|
376
|
+
raw_url = (sub_list_url_address || []).map(&:value).join
|
|
377
|
+
result += @builder_context.link(restore(sub_list_url_text), raw_url)
|
|
367
378
|
else
|
|
368
379
|
result += '['
|
|
369
380
|
ti = ti_starting_position + 1
|
|
@@ -373,7 +384,7 @@ class TextLineBuilder
|
|
|
373
384
|
result += @builder_context.inline_code(token_list[ti].value)
|
|
374
385
|
ti += 1
|
|
375
386
|
when 'TextLineToken', 'ParentheseLeft', 'ParentheseRight', 'SquareBracketRight', 'DoubleSquareBracketRight'
|
|
376
|
-
result += token_list[ti].value
|
|
387
|
+
result += @builder_context.literal_text(token_list[ti].value)
|
|
377
388
|
ti += 1
|
|
378
389
|
else
|
|
379
390
|
ti += 1
|
|
@@ -384,12 +395,14 @@ class TextLineBuilder
|
|
|
384
395
|
end
|
|
385
396
|
|
|
386
397
|
class TextLine < TextLineBuilderContext
|
|
387
|
-
|
|
388
|
-
|
|
398
|
+
include HtmlSafe
|
|
399
|
+
|
|
400
|
+
@@link_registry = nil
|
|
401
|
+
@@broken_links = []
|
|
389
402
|
|
|
390
403
|
class << self
|
|
391
404
|
def link_registry=(registry)
|
|
392
|
-
@@link_registry = registry
|
|
405
|
+
@@link_registry = registry
|
|
393
406
|
end
|
|
394
407
|
|
|
395
408
|
def link_registry
|
|
@@ -403,7 +416,7 @@ class TextLine < TextLineBuilderContext
|
|
|
403
416
|
end
|
|
404
417
|
|
|
405
418
|
def reset_broken_links
|
|
406
|
-
@@broken_links = []
|
|
419
|
+
@@broken_links = []
|
|
407
420
|
end
|
|
408
421
|
|
|
409
422
|
def record_broken_link(document, target)
|
|
@@ -423,6 +436,11 @@ class TextLine < TextLineBuilderContext
|
|
|
423
436
|
tlb.restore(tlp.tokenize(str))
|
|
424
437
|
end
|
|
425
438
|
|
|
439
|
+
# Literal text run, HTML-escaped for element content (ADR-188, SRS-096).
|
|
440
|
+
def literal_text(str)
|
|
441
|
+
escape_text(str)
|
|
442
|
+
end
|
|
443
|
+
|
|
426
444
|
def italic(str)
|
|
427
445
|
"<i>#{str}</i>"
|
|
428
446
|
end
|
|
@@ -439,24 +457,27 @@ class TextLine < TextLineBuilderContext
|
|
|
439
457
|
"<code class=\"inline\">#{CGI.escapeHTML(str)}</code>"
|
|
440
458
|
end
|
|
441
459
|
|
|
442
|
-
def link(link_text, link_url)
|
|
460
|
+
def link(link_text, link_url)
|
|
443
461
|
raw = link_url.to_s
|
|
444
462
|
kind, target, fragment = classify_markdown_link(raw)
|
|
445
463
|
case kind
|
|
446
464
|
when :internal
|
|
447
465
|
href = RelativeUrl.between(owner_document.output_rel_path, target.output_rel_path, fragment: fragment)
|
|
448
|
-
"<a href=\"#{href}\" class=\"external\">#{link_text}</a>"
|
|
466
|
+
"<a href=\"#{escape_attr(href)}\" class=\"external\">#{link_text}</a>"
|
|
449
467
|
when :broken
|
|
450
468
|
TextLine.record_broken_link(owner_document, raw)
|
|
451
|
-
"<a href=\"#{raw}\" class=\"broken_link\" title=\"Unresolved cross-document link\">#{link_text}</a>"
|
|
469
|
+
"<a href=\"#{escape_attr(raw)}\" class=\"broken_link\" title=\"Unresolved cross-document link\">#{link_text}</a>"
|
|
452
470
|
else
|
|
453
|
-
|
|
471
|
+
url = safe_url(raw)
|
|
472
|
+
return link_text if url.nil? # disallowed scheme: render inert (ADR-188, SRS-098)
|
|
473
|
+
|
|
474
|
+
"<a target=\"_blank\" rel=\"noopener\" href=\"#{escape_attr(url)}\" class=\"external\">#{link_text}</a>"
|
|
454
475
|
end
|
|
455
476
|
end
|
|
456
477
|
|
|
457
478
|
# Resolves an Obsidian/wiki link "[[target#fragment|alias]]" to a managed
|
|
458
479
|
# document by its unique id/filename, independent of folder (ADR-186).
|
|
459
|
-
def wiki_link(inner)
|
|
480
|
+
def wiki_link(inner)
|
|
460
481
|
link_part, sep, alias_text = inner.partition('|')
|
|
461
482
|
target, _hash, fragment = link_part.partition('#')
|
|
462
483
|
display = (sep.empty? ? link_part : alias_text).strip
|
|
@@ -480,7 +501,7 @@ class TextLine < TextLineBuilderContext
|
|
|
480
501
|
# Classifies a Markdown link target as :internal (a managed document, resolved
|
|
481
502
|
# against the owning document's source directory), :broken (a local .md path
|
|
482
503
|
# that does not resolve), or :external (everything else). ADR-186.
|
|
483
|
-
def classify_markdown_link(raw)
|
|
504
|
+
def classify_markdown_link(raw)
|
|
484
505
|
path_part, _sep, fragment = raw.partition('#')
|
|
485
506
|
path_part = URI::DEFAULT_PARSER.unescape(path_part) # decode %20 etc. to match the real file path
|
|
486
507
|
return [:external] unless local_markdown_path?(path_part)
|
|
@@ -489,10 +510,14 @@ class TextLine < TextLineBuilderContext
|
|
|
489
510
|
return [:external] unless doc&.output_rel_path && doc.respond_to?(:path) && doc.path && TextLine.link_registry
|
|
490
511
|
|
|
491
512
|
target = TextLine.link_registry.find_by_source(File.expand_path(path_part, File.dirname(doc.path)))
|
|
492
|
-
|
|
513
|
+
if target
|
|
514
|
+
[:internal, target, fragment.empty? ? nil : fragment]
|
|
515
|
+
else
|
|
516
|
+
[:broken]
|
|
517
|
+
end
|
|
493
518
|
end
|
|
494
519
|
|
|
495
520
|
def local_markdown_path?(path_part)
|
|
496
|
-
path_part.match?(/\.(md|markdown)\z/i) && !path_part.match?(%r{\A[a-z][a-z0-9
|
|
521
|
+
path_part.match?(/\.(md|markdown)\z/i) && !path_part.match?(%r{\A[a-z][a-z0-9+.-]*://}i)
|
|
497
522
|
end
|
|
498
523
|
end
|
|
@@ -1,22 +1,21 @@
|
|
|
1
|
-
require_relative
|
|
1
|
+
require_relative 'doc_item'
|
|
2
2
|
|
|
3
3
|
class TodoBlock < DocItem
|
|
4
|
+
attr_accessor :text
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
def initialize(text)
|
|
7
|
+
@text = text
|
|
8
|
+
end
|
|
6
9
|
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
s += "</table>\n"
|
|
16
|
-
@@html_table_render_in_progress = false
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
s += "<div class=\"todoblock\"><p>#{f_text}</div>\n"
|
|
20
|
-
return s
|
|
21
|
-
end
|
|
22
|
-
end
|
|
18
|
+
s += "<div class=\"todoblock\"><p>#{f_text}</div>\n"
|
|
19
|
+
s
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# A single Scope-table row of a Decision Record, modelled as a node in the
|
|
4
|
+
# per-row dependency network (ADR-194). Each work item carries its canonical
|
|
5
|
+
# identity (`<record>.<step>.<activity>`), the bounded per-row Status it reads
|
|
6
|
+
# its readiness from (ADR-193), and predecessor / successor edges spanning both
|
|
7
|
+
# intra-record (step order) and cross-record (Depends On) links.
|
|
8
|
+
#
|
|
9
|
+
# `predecessors` / `successors` follow the house list-of-single-key-hashes shape
|
|
10
|
+
# (ADR-197): each entry maps a readable `record.step.activity` label to the
|
|
11
|
+
# resolved WorkItem. Every cross-record edge additionally records whether it
|
|
12
|
+
# stays within a decision_groups folder (in-group) or crosses to another
|
|
13
|
+
# (cross-group); that tag is inert here and consumed by the critical-chain step
|
|
14
|
+
# (ADR-195). Readiness ignores it — a cross-group predecessor blocks exactly
|
|
15
|
+
# like an in-group one.
|
|
16
|
+
class WorkItem
|
|
17
|
+
# Canonical phase order used only as the fallback for activity-type-aligned
|
|
18
|
+
# resolution when the prerequisite has no row of the dependent's exact Item.
|
|
19
|
+
ACTIVITY_ORDER = %w[Analysis Requirements Code Tests].freeze
|
|
20
|
+
|
|
21
|
+
attr_reader :record_id, :step, :activity, :owner, :status, :depends_on_refs,
|
|
22
|
+
:focused_estimate, :safe_estimate, :record_sequence, :start_date, :target_date
|
|
23
|
+
attr_accessor :predecessors, :successors, :cross_group_predecessor_labels, :resolved_dependencies
|
|
24
|
+
|
|
25
|
+
def initialize(record_id:, step:, activity:, owner:, status:, depends_on_refs:, # rubocop:disable Metrics/ParameterLists
|
|
26
|
+
focused_estimate: 0.0, safe_estimate: 0.0, record_sequence: 0,
|
|
27
|
+
start_date: nil, target_date: nil)
|
|
28
|
+
@record_id = record_id
|
|
29
|
+
@step = step
|
|
30
|
+
@activity = activity.to_s.strip
|
|
31
|
+
@owner = owner.to_s.strip
|
|
32
|
+
@status = status.to_s.strip
|
|
33
|
+
@depends_on_refs = depends_on_refs # array of record ref strings, e.g. ["ADR-193"]
|
|
34
|
+
# The row's authored committed window (ADR-213): the Scope Start Date and
|
|
35
|
+
# Target Date as parsed Dates (nil when absent/unparseable). Distinct from the
|
|
36
|
+
# computed schedule -- these are the author's intent, drawn on the tracking lane.
|
|
37
|
+
@start_date = start_date
|
|
38
|
+
@target_date = target_date
|
|
39
|
+
# Per-row scheduling estimates in working days (ADR-195); the focused estimate
|
|
40
|
+
# is the scheduling duration, safe - focused the safety aggregated into the
|
|
41
|
+
# project buffer. record_sequence is the owning record's number, used only as
|
|
42
|
+
# a deterministic priority tiebreak in the critical-chain scheduler.
|
|
43
|
+
@focused_estimate = focused_estimate
|
|
44
|
+
@safe_estimate = safe_estimate
|
|
45
|
+
@record_sequence = record_sequence
|
|
46
|
+
@predecessors = []
|
|
47
|
+
@successors = []
|
|
48
|
+
@cross_group_predecessor_labels = []
|
|
49
|
+
# One entry per resolved Depends On reference, keyed by the authored ref:
|
|
50
|
+
# { ref => { doc:, anchor:, label: } }. `anchor` is the target's resolved
|
|
51
|
+
# work-item row anchor (nil when the target has no `#` step column, so the
|
|
52
|
+
# link opens the record page); `label` is the resolved work item's id, for
|
|
53
|
+
# the link tooltip. Used to render the Depends On cell as deep links.
|
|
54
|
+
@resolved_dependencies = {}
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def id
|
|
58
|
+
"#{@record_id}.#{@step}.#{@activity}"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# The anchor of this work item's row in its rendered Scope table — the deep-link
|
|
62
|
+
# target a dependent record points at. Namespaced with `.scope.` so it never
|
|
63
|
+
# collides with the Affected Documents controlled table, whose rows anchor on
|
|
64
|
+
# the same `<record>.<step>` scheme on the same page.
|
|
65
|
+
def row_anchor
|
|
66
|
+
"#{@record_id}.scope.#{@step}"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def add_resolved_dependency(ref, doc, anchor, label)
|
|
70
|
+
@resolved_dependencies[ref] = { doc: doc, anchor: anchor, label: label }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def activity_rank
|
|
74
|
+
ACTIVITY_ORDER.index(@activity) || ACTIVITY_ORDER.length
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# A row is "started" once it leaves To Do; both In-Progress and Done count, so
|
|
78
|
+
# a Done row whose predecessor never finished is still flagged as a violation.
|
|
79
|
+
def started?
|
|
80
|
+
@status == 'In-Progress' || @status == 'Done'
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def done?
|
|
84
|
+
@status == 'Done'
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def add_predecessor(work_item, cross_group:)
|
|
88
|
+
@predecessors << { work_item.id => work_item }
|
|
89
|
+
@cross_group_predecessor_labels << work_item.id if cross_group
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def add_successor(work_item)
|
|
93
|
+
@successors << { work_item.id => work_item }
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def predecessor_items
|
|
97
|
+
@predecessors.map { |edge| edge.values.first }
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def successor_items
|
|
101
|
+
@successors.map { |edge| edge.values.first }
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Same-record, lower-numbered steps — the intra-record (phase-order) edges.
|
|
105
|
+
def intra_record_predecessors
|
|
106
|
+
predecessor_items.select { |p| p.record_id == @record_id }
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Resolved Depends On edges into other records (in-group or cross-group alike).
|
|
110
|
+
def cross_record_predecessors
|
|
111
|
+
predecessor_items.reject { |p| p.record_id == @record_id }
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Kitted when every predecessor — intra- and cross-record, regardless of the
|
|
115
|
+
# in-group / cross-group tag — is Done; trivially kitted when it has none.
|
|
116
|
+
def fully_kitted?
|
|
117
|
+
predecessor_items.all?(&:done?)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# A started row with an unfinished lower-numbered step in the same record.
|
|
121
|
+
def phase_order_violation?
|
|
122
|
+
started? && intra_record_predecessors.any? { |p| !p.done? }
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# A started row whose resolved cross-record predecessor is not yet Done.
|
|
126
|
+
def cross_record_violation?
|
|
127
|
+
started? && cross_record_predecessors.any? { |p| !p.done? }
|
|
128
|
+
end
|
|
129
|
+
end
|
data/lib/almirah/doc_parser.rb
CHANGED
|
@@ -8,13 +8,14 @@ require_relative 'doc_items/todo_block'
|
|
|
8
8
|
require_relative 'doc_items/controlled_paragraph'
|
|
9
9
|
require_relative 'doc_items/markdown_table'
|
|
10
10
|
require_relative 'doc_items/controlled_table'
|
|
11
|
+
require_relative 'doc_items/scope_table'
|
|
11
12
|
require_relative 'doc_items/image'
|
|
12
13
|
require_relative 'doc_items/markdown_list'
|
|
13
14
|
require_relative 'doc_items/doc_footer'
|
|
14
15
|
require_relative 'doc_items/frontmatter'
|
|
15
16
|
|
|
16
|
-
class DocParser
|
|
17
|
-
def self.try_to_extract_frontmatter(doc, text_lines)
|
|
17
|
+
class DocParser
|
|
18
|
+
def self.try_to_extract_frontmatter(doc, text_lines)
|
|
18
19
|
lines_to_remove = 0
|
|
19
20
|
frontmatter_lines = ''
|
|
20
21
|
if /^(-{3,})/.match(text_lines[0])
|
|
@@ -174,7 +175,7 @@ class DocParser # rubocop:disable Metrics/ClassLength,Style/Documentation
|
|
|
174
175
|
|
|
175
176
|
doc.items.append(item)
|
|
176
177
|
|
|
177
|
-
elsif res = /^([
|
|
178
|
+
elsif res = /^([*-]\s+)(.*)/.match(s) # check if unordered list start
|
|
178
179
|
|
|
179
180
|
if doc.title == ''
|
|
180
181
|
# dummy section if root is not a Document Title (level 0)
|
|
@@ -187,7 +188,7 @@ class DocParser # rubocop:disable Metrics/ClassLength,Style/Documentation
|
|
|
187
188
|
|
|
188
189
|
temp_md_table = process_temp_table(doc, temp_md_table)
|
|
189
190
|
|
|
190
|
-
|
|
191
|
+
res[2]
|
|
191
192
|
|
|
192
193
|
if temp_md_list
|
|
193
194
|
temp_md_list.add_row(s)
|
|
@@ -201,7 +202,7 @@ class DocParser # rubocop:disable Metrics/ClassLength,Style/Documentation
|
|
|
201
202
|
|
|
202
203
|
temp_md_table = process_temp_table(doc, temp_md_table)
|
|
203
204
|
|
|
204
|
-
|
|
205
|
+
res[1]
|
|
205
206
|
|
|
206
207
|
if temp_md_list
|
|
207
208
|
temp_md_list.add_row(s)
|
|
@@ -248,9 +249,14 @@ class DocParser # rubocop:disable Metrics/ClassLength,Style/Documentation
|
|
|
248
249
|
|
|
249
250
|
if temp_md_table
|
|
250
251
|
if temp_md_table.is_separator_detected # if there is a separator
|
|
251
|
-
#
|
|
252
|
-
|
|
253
|
-
|
|
252
|
+
# A Decision Record's Scope table is parsed into a purpose-built
|
|
253
|
+
# ScopeTable (ADR-194); a Protocol, or a Decision inside an
|
|
254
|
+
# "Affected Documents" section, into a ControlledTable.
|
|
255
|
+
if doc.instance_of?(Decision) && in_section?(doc, 'Scope') &&
|
|
256
|
+
temp_md_table.instance_of?(MarkdownTable)
|
|
257
|
+
temp_md_table = ScopeTable.new(doc, temp_md_table)
|
|
258
|
+
elsif doc.instance_of?(Protocol) ||
|
|
259
|
+
(doc.instance_of?(Decision) && in_section?(doc, 'Affected Documents'))
|
|
254
260
|
# check if it is a controlled table
|
|
255
261
|
tmp = /(.*)\s+>\[(\S*)\]/.match(row)
|
|
256
262
|
if tmp && (temp_md_table.instance_of? MarkdownTable)
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative '../relative_url'
|
|
4
4
|
|
|
5
|
-
class BaseDocument
|
|
5
|
+
class BaseDocument
|
|
6
6
|
attr_accessor :title, :id, :dom, :headings, :output_rel_path
|
|
7
7
|
|
|
8
8
|
class << self
|
|
@@ -17,7 +17,13 @@ class BaseDocument # rubocop:disable Style/Documentation
|
|
|
17
17
|
@dom = nil
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
# Whether this page needs the Chart.js library loaded (overridden by the
|
|
21
|
+
# planning pages that render charts).
|
|
22
|
+
def needs_chartjs?
|
|
23
|
+
false
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def save_html_to_file(html_rows, nav_pane, output_file_path)
|
|
21
27
|
gem_root = File.expand_path './../../..', File.dirname(__FILE__)
|
|
22
28
|
template_file = "#{gem_root}/lib/almirah/templates/page.html"
|
|
23
29
|
|
|
@@ -29,6 +35,8 @@ class BaseDocument # rubocop:disable Style/Documentation
|
|
|
29
35
|
"#{@id}.html"
|
|
30
36
|
elsif instance_of? DecisionsOverview
|
|
31
37
|
'overview.html'
|
|
38
|
+
elsif instance_of? CriticalChainPage
|
|
39
|
+
'critical-chain.html'
|
|
32
40
|
elsif instance_of? Decision
|
|
33
41
|
"#{@id}.html"
|
|
34
42
|
else
|
|
@@ -36,7 +44,7 @@ class BaseDocument # rubocop:disable Style/Documentation
|
|
|
36
44
|
end
|
|
37
45
|
@output_rel_path = output_file_path.split('/build/', 2).last
|
|
38
46
|
file = File.open(output_file_path, 'w')
|
|
39
|
-
file_data.each do |s|
|
|
47
|
+
file_data.each do |s|
|
|
40
48
|
if s.include?('{{CONTENT}}')
|
|
41
49
|
html_rows.each do |r|
|
|
42
50
|
file.puts r
|
|
@@ -51,7 +59,7 @@ class BaseDocument # rubocop:disable Style/Documentation
|
|
|
51
59
|
if @id == 'index'
|
|
52
60
|
file.puts "<script type=\"module\" src=\"#{rel_to('scripts/orama_search.js')}\"></script>"
|
|
53
61
|
file.puts "<link rel=\"stylesheet\" href=\"#{rel_to('css/search.css')}\">"
|
|
54
|
-
elsif
|
|
62
|
+
elsif needs_chartjs?
|
|
55
63
|
file.puts '<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>'
|
|
56
64
|
end
|
|
57
65
|
elsif s.include?('{{HOME_BUTTON}}')
|
|
@@ -60,7 +68,10 @@ class BaseDocument # rubocop:disable Style/Documentation
|
|
|
60
68
|
else
|
|
61
69
|
file.puts index_link(rel_to('index.html'))
|
|
62
70
|
end
|
|
63
|
-
|
|
71
|
+
if BaseDocument.show_decisions_link
|
|
72
|
+
file.puts decisions_link(rel_to('decisions/overview.html'))
|
|
73
|
+
file.puts critical_chain_link(rel_to('decisions/critical-chain.html'))
|
|
74
|
+
end
|
|
64
75
|
elsif s.include?('{{GEM_VERSION}}')
|
|
65
76
|
file.puts "(#{Gem.loaded_specs['Almirah'].version.version})"
|
|
66
77
|
else
|
|
@@ -75,6 +86,11 @@ class BaseDocument # rubocop:disable Style/Documentation
|
|
|
75
86
|
%(<a id="decisions_menu_item" href="#{href}">#{icon} Decision Records</a>)
|
|
76
87
|
end
|
|
77
88
|
|
|
89
|
+
def critical_chain_link(href)
|
|
90
|
+
icon = '<span><i class="fa fa-link" aria-hidden="true"></i></span>'
|
|
91
|
+
%(<a id="critical_chain_menu_item" href="#{href}">#{icon} Critical Chain</a>)
|
|
92
|
+
end
|
|
93
|
+
|
|
78
94
|
def index_link(href)
|
|
79
95
|
icon = '<span><i class="fa fa-info" aria-hidden="true"></i></span>'
|
|
80
96
|
%(<a id="index_menu_item" href="#{href}">#{icon} Index</a>)
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative 'base_document'
|
|
4
4
|
|
|
5
|
-
class Coverage < BaseDocument
|
|
5
|
+
class Coverage < BaseDocument
|
|
6
6
|
attr_accessor :top_doc, :bottom_doc, :covered_items, :passed_steps_number, :failed_steps_number
|
|
7
7
|
|
|
8
8
|
def initialize(top_doc)
|
|
@@ -21,7 +21,7 @@ class Coverage < BaseDocument # rubocop:disable Style/Documentation
|
|
|
21
21
|
puts "\e[35mTraceability: #{@id}\e[0m"
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
-
def to_html(nav_pane, output_file_path)
|
|
24
|
+
def to_html(nav_pane, output_file_path)
|
|
25
25
|
html_rows = []
|
|
26
26
|
|
|
27
27
|
html_rows.append('')
|
|
@@ -43,7 +43,7 @@ class Coverage < BaseDocument # rubocop:disable Style/Documentation
|
|
|
43
43
|
save_html_to_file(html_rows, nav_pane, output_file_path)
|
|
44
44
|
end
|
|
45
45
|
|
|
46
|
-
def render_table_row(top_item)
|
|
46
|
+
def render_table_row(top_item)
|
|
47
47
|
s = ''
|
|
48
48
|
if top_item.coverage_links
|
|
49
49
|
id_color = if top_item.coverage_links.length > 1
|