Class: Project

Inherits:
Object
  • Object
show all
Defined in:
lib/almirah/project.rb

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(configuration) ⇒ Project

Returns a new instance of Project.



19
20
21
22
23
24
25
26
27
# File 'lib/almirah/project.rb', line 19

def initialize(configuration)
  @configuration = configuration
  @project_data = ProjectData.new

  @index = nil
  @project = self
  FileUtils.remove_dir("#{@configuration.project_root_directory}/build", true)
  copy_resources
end

Instance Attribute Details

#configurationObject

Returns the value of attribute configuration.



17
18
19
# File 'lib/almirah/project.rb', line 17

def configuration
  @configuration
end

#indexObject

Returns the value of attribute index.



17
18
19
# File 'lib/almirah/project.rb', line 17

def index
  @index
end

#projectObject

Returns the value of attribute project.



17
18
19
# File 'lib/almirah/project.rb', line 17

def project
  @project
end

#project_dataObject

Returns the value of attribute project_data.



17
18
19
# File 'lib/almirah/project.rb', line 17

def project_data
  @project_data
end

Instance Method Details

#add_to_decision_group(doc, rel_dir) ⇒ Object

Add a decision record to its planning group, keyed on the first-level folder under decisions/ (a record directly under decisions/ has rel_dir '.', kept as its own '.' group rather than dropped). Groups are single-key hashes appended in folder-encounter order; the matching one is reused. See ADR-197.



302
303
304
305
306
307
308
309
310
# File 'lib/almirah/project.rb', line 302

def add_to_decision_group(doc, rel_dir)
  group_name = rel_dir.split('/').first
  group = @project_data.decision_groups.find { |g| g.key?(group_name) }
  if group.nil?
    @project_data.decision_groups.append({ group_name => [doc] })
  else
    group[group_name].append(doc)
  end
end

#aligned_work_item(target, activity) ⇒ Object

The target record's work item whose activity (Item) matches activity, falling back to the nearest earlier activity by canonical phase order, then (when the target has only later activities) to its earliest row. nil only when the target has no Scope rows.



178
179
180
181
182
183
184
185
186
187
188
189
190
# File 'lib/almirah/project.rb', line 178

def aligned_work_item(target, activity)
  items = target.scope_work_items
  return nil if items.empty?

  exact = items.select { |t| t.activity == activity }.min_by(&:step)
  return exact if exact

  rank = WorkItem::ACTIVITY_ORDER.index(activity) || WorkItem::ACTIVITY_ORDER.length
  earlier = items.select { |t| t.activity_rank <= rank }
  return items.min_by { |t| [t.activity_rank, t.step] } if earlier.empty?

  earlier.min_by { |t| [-t.activity_rank, t.step] }
end

Assigns each document its generated output path (relative to the build root) and registers it for cross-document link resolution (ADR-186). Runs after all documents are parsed and before any rendering, so link targets are known.



225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
# File 'lib/almirah/project.rb', line 225

def build_link_registry
  reg = @project_data.link_registry
  TextLine.link_registry = reg
  TextLine.reset_broken_links
  @project_data.specifications.each do |d|
    d.output_rel_path = "specifications/#{d.id}/#{d.id}.html"
    reg.register(d)
  end
  @project_data.protocols.each do |d|
    d.output_rel_path = "tests/protocols/#{d.id}/#{d.id}.html"
    reg.register(d)
  end
  @project_data.decisions.each do |d|
    d.output_rel_path = "decisions/#{d.html_rel_path}"
    reg.register(d)
  end
  @project_data.source_files.each do |d|
    rel = d.path.sub("#{d.root_path}/", '')
    d.output_rel_path = "source_files/#{d.repository}/#{rel}.html"
    reg.register(d)
  end
end

#check_wrong_specification_referencedObject



380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
# File 'lib/almirah/project.rb', line 380

def check_wrong_specification_referenced
  available_specification_ids = {}

  @project_data.specifications.each do |s|
    available_specification_ids[s.id.to_s.downcase] = s
  end

  @project_data.specifications.each do |s| # rubocop:disable Style/CombinableLoops
    s.up_link_docs.each do |key, _value|
      next if available_specification_ids.key?(key)

      # now key points to the doc_id that does not exist
      wrong_doc_id = key
      # find the item that reference to it
      s.controlled_items.each do |item|
        next if item.up_link_ids.nil?

        item.up_link_ids.each do |up_link_id|
          next unless tmp = /^([a-zA-Z]+)-\d+/.match(up_link_id) # SRS

          if tmp[1].downcase == wrong_doc_id
            # we got it finally!
            s.wrong_links_hash[up_link_id.to_s] = item
          end
        end
      end
    end
  end
end

#copy_resourcesObject



29
30
31
32
33
34
35
36
37
38
39
40
41
# File 'lib/almirah/project.rb', line 29

def copy_resources
  # scripts
  gem_root = File.expand_path './../..', File.dirname(__FILE__)
  src_folder = "#{gem_root}/lib/almirah/templates/scripts"
  dst_folder = "#{@configuration.project_root_directory}/build/scripts"
  FileUtils.mkdir_p(dst_folder)
  FileUtils.copy_entry(src_folder, dst_folder)
  # css
  src_folder = "#{gem_root}/lib/almirah/templates/css"
  dst_folder = "#{@configuration.project_root_directory}/build/css"
  FileUtils.mkdir_p(dst_folder)
  FileUtils.copy_entry(src_folder, dst_folder)
end

#create_indexObject



447
448
449
# File 'lib/almirah/project.rb', line 447

def create_index
  @index = Index.new(@project)
end

#create_search_dataObject



538
539
540
541
542
543
# File 'lib/almirah/project.rb', line 538

def create_search_data
  db = SpecificationsDb.new @project_data.specifications
  data_path = "#{@configuration.project_root_directory}/build/data"
  FileUtils.mkdir_p(data_path)
  db.save(data_path)
end

#decision_group_name(doc) ⇒ Object

The planning-group name (first-level decisions/ folder) a record belongs to, read from the decision_groups collection (ADR-197).



194
195
196
197
# File 'lib/almirah/project.rb', line 194

def decision_group_name(doc)
  group = @project_data.decision_groups.find { |g| g.values.first.include?(doc) }
  group&.keys&.first
end


356
357
358
359
360
361
362
363
364
365
366
367
# File 'lib/almirah/project.rb', line 356

def link_all_decisions
  number_of_links = 0
  @project_data.decisions.each do |d|
    @project_data.specifications.each do |s|
      next unless d.up_link_docs.key?(s.id.to_s)

      DocLinker.link_decision_to_spec(d, s)
      number_of_links += 1
    end
  end
  ConsoleReporter.count('decision links', number_of_links)
end


339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
# File 'lib/almirah/project.rb', line 339

def link_all_protocols
  @project_data.protocols.each do |p|
    @project_data.specifications.each do |s|
      if p.up_link_docs.key?(s.id.to_s)
        DocLinker.link_protocol_to_spec(p, s)
        @project_data.covered_specifications_dictionary[s.id.to_s] = s
      end
    end
  end
  # create coverage documents
  @project_data.covered_specifications_dictionary.each do |_key, value|
    doc = DocFabric.create_coverage_matrix(value)
    @project_data.coverage_matrices.append doc
  end
  ConsoleReporter.count('coverage matrices', @project_data.coverage_matrices.length)
end


369
370
371
372
373
374
375
376
377
378
# File 'lib/almirah/project.rb', line 369

def link_all_source_files
  return unless DocLinker.link_all_source_files(@project_data)

  # create implementation documents
  @project_data.implemented_specifications_dictionary.each do |_key, value|
    doc = DocFabric.create_implementation_document(value)
    @project_data.implementation_matrices.append doc
  end
  ConsoleReporter.count('implementation matrices', @project_data.implementation_matrices.length)
end


320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
# File 'lib/almirah/project.rb', line 320

def link_all_specifications
  comb_list = @project_data.specifications.combination(2)
  comb_list.each do |c|
    link_two_specifications(c[0], c[1])
    # puts "Link: #{c[0].id} - #{c[1].id}"
  end
  # separatelly create design inputs treceability
  @configuration.get_design_inputs.each do |i|
    next unless @project_data.specifications_dictionary.key? i.to_s.downcase

    document = @project_data.specifications_dictionary[i.to_s.downcase]
    if document
      doc = DocFabric.create_traceability_document(document, nil)
      @project_data.traceability_matrices.append doc
    end
  end
  ConsoleReporter.count('traceability matrices', @project_data.traceability_matrices.length)
end


150
151
152
153
154
155
156
# File 'lib/almirah/project.rb', line 150

def link_cross_record_dependencies
  @project_data.decisions.each do |d|
    d.scope_work_items.each do |wi|
      wi.depends_on_refs.each { |ref| link_dependency(d, wi, ref) }
    end
  end
end


158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
# File 'lib/almirah/project.rb', line 158

def link_dependency(record, work_item, ref)
  target = @project_data.link_registry.find_by_id(ref)
  unless target.is_a?(Decision)
    @kit_unresolved << { record: record.id, target: ref }
    return
  end
  prereq = aligned_work_item(target, work_item.activity)
  return if prereq.nil? || prereq.equal?(work_item)

  cross = decision_group_name(record) != decision_group_name(target)
  work_item.add_predecessor(prereq, cross_group: cross)
  prereq.add_successor(work_item)
  anchor = target.scope_table&.step_column? ? prereq.row_anchor : nil
  work_item.add_resolved_dependency(ref, target, anchor, prereq.id)
end

Each row's lower-numbered same-record steps are its predecessors; equal step numbers are concurrent (no edge). These edges are in-group by definition.



136
137
138
139
140
141
142
143
144
145
146
147
148
# File 'lib/almirah/project.rb', line 136

def link_intra_record_steps
  @project_data.decisions.each do |d|
    items = d.scope_work_items
    items.each do |wi|
      items.each do |other|
        next if other.equal?(wi) || other.step >= wi.step

        wi.add_predecessor(other, cross_group: false)
        other.add_successor(wi)
      end
    end
  end
end


410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
# File 'lib/almirah/project.rb', line 410

def link_two_specifications(doc_a, doc_b)
  if doc_b.up_link_docs.key?(doc_a.id.to_s)
    top_document = doc_a
    bottom_document = doc_b
  elsif doc_a.up_link_docs.key?(doc_b.id.to_s)
    top_document = doc_b
    bottom_document = doc_a
  else
    return # no links
  end
  # puts "Link: #{doc_a.id} - #{doc_b.id}"
  bottom_document.controlled_items.each do |item|
    next unless item.up_link_ids

    item.up_link_ids.each do |up_lnk|
      if top_document.dictionary.key?(up_lnk.to_s)

        top_item = top_document.dictionary[up_lnk.to_s]

        unless top_item.down_links
          top_item.down_links = []
          top_document.items_with_downlinks_number += 1 # for statistics
        end
        top_item.down_links.append(item)
      elsif tmp = /^([a-zA-Z]+)-\d+/.match(up_lnk)
        # check if there is a non existing link with the right doc_id
        if tmp[1].downcase == top_document.id.downcase
          bottom_document.wrong_links_hash[up_lnk] = item
        end # SRS
      end
    end
  end
  # create treceability document
  doc = DocFabric.create_traceability_document(top_document, bottom_document)
  @project_data.traceability_matrices.append doc
end

Builds the per-row WorkItem dependency network (ADR-194): registers every Scope-row work item, fills the intra-record step-order edges, then resolves each Depends On reference globally (LinkRegistry) to the activity-type-aligned work item of the target record, tagging each cross-record edge in-group or cross-group against the decision_groups boundary (ADR-197). Unresolved references are collected for report_kit_violations. Runs after build_link_registry (so every record resolves) and before rendering (so the overview Kit column is ready).



125
126
127
128
129
130
131
132
# File 'lib/almirah/project.rb', line 125

def link_work_items
  @kit_unresolved = []
  @project_data.decisions.each do |d|
    d.scope_work_items.each { |wi| @project_data.work_items[wi.id] = wi }
  end
  link_intra_record_steps
  link_cross_record_dependencies
end

#parse_all_protocolsObject



258
259
260
261
262
263
264
265
# File 'lib/almirah/project.rb', line 258

def parse_all_protocols
  path = @configuration.project_root_directory
  Dir.glob("#{path}/tests/protocols/**/*.md").each do |f|
    doc = DocFabric.create_protocol(f)
    @project_data.protocols.append(doc)
  end
  ConsoleReporter.count('parsing test protocols', @project_data.protocols.length)
end

#parse_all_source_filesObject



267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
# File 'lib/almirah/project.rb', line 267

def parse_all_source_files
  @configuration.get_repositories.each do |repos|
    # puts "Processing repository: #{repos['name']}, #{repos['path']}"
    next unless repos['path'] && Dir.exist?(repos['path'])

    file_path = repos['path']
    Dir.glob("#{repos['path']}/**/*.*").each do |f|
      next unless File.file?(f) && f.end_with?('.c', '.cpp', '.h', '.hpp', '.py', '.java', '.rb', '.js', '.ts', '.go',
                                               '.rs')

      doc = DocFabric.create_source_file(file_path, f, repos['name'])
      # puts "Source file: #{doc.id}"
      @project_data.source_files.append(doc)
    end
  end
end

#parse_all_specificationsObject



248
249
250
251
252
253
254
255
256
# File 'lib/almirah/project.rb', line 248

def parse_all_specifications
  path = @configuration.project_root_directory
  Dir.glob("#{path}/specifications/**/*.md").each do |f|
    doc = DocFabric.create_specification(f)
    @project_data.specifications.append(doc)
    @project_data.specifications_dictionary[doc.id.to_s.downcase] = doc
  end
  ConsoleReporter.count('parsing specifications', @project_data.specifications.length)
end

#parse_decisionsObject



284
285
286
287
288
289
290
291
292
293
294
295
296
# File 'lib/almirah/project.rb', line 284

def parse_decisions
  path = @configuration.project_root_directory
  decisions_root = "#{path}/decisions"
  Dir.glob("#{decisions_root}/**/*.md").each do |f|
    doc = DocFabric.create_decision(f)
    rel_dir = File.dirname(f.sub("#{decisions_root}/", ''))
    doc.html_rel_path = rel_dir == '.' ? "#{doc.id}.html" : "#{rel_dir}/#{doc.id}.html"
    @project_data.decisions.append(doc)
    add_to_decision_group(doc, rel_dir)
  end
  BaseDocument.show_decisions_link = @project_data.decisions.any?
  ConsoleReporter.count('parsing decisions', @project_data.decisions.length)
end

#parse_test_run(test_run) ⇒ Object



312
313
314
315
316
317
318
# File 'lib/almirah/project.rb', line 312

def parse_test_run(test_run)
  path = @configuration.project_root_directory
  Dir.glob("#{path}/tests/runs/#{test_run}/**/*.md").each do |f|
    doc = DocFabric.create_protocol(f)
    @project_data.protocols.append(doc)
  end
end

#render_all_decisionsObject



523
524
525
526
527
528
529
530
531
532
533
534
535
536
# File 'lib/almirah/project.rb', line 523

def render_all_decisions
  return if @project_data.decisions.empty?

  build_decisions_root = "#{@configuration.project_root_directory}/build/decisions"
  @project_data.decisions.each do |doc|
    out_dir_rel = File.dirname(doc.html_rel_path)
    out_dir = out_dir_rel == '.' ? build_decisions_root : "#{build_decisions_root}/#{out_dir_rel}"
    FileUtils.mkdir_p(out_dir)
    depth = 1 + (out_dir_rel == '.' ? 0 : out_dir_rel.split('/').size)
    doc.root_prefix = '../' * depth
    doc.specifications_path = "./#{doc.root_prefix}specifications/"
    doc.to_html(NavigationPane.new(doc), "#{out_dir}/")
  end
end

#render_all_protocolsObject



469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
# File 'lib/almirah/project.rb', line 469

def render_all_protocols
  path = @configuration.project_root_directory

  FileUtils.mkdir_p("#{path}/build/tests/protocols")

  @project_data.protocols.each do |doc|
    img_src_dir = "#{path}/tests/protocols/#{doc.id}/img"
    img_dst_dir = "#{path}/build/tests/protocols/#{doc.id}/img"

    FileUtils.mkdir_p(img_dst_dir)

    FileUtils.copy_entry(img_src_dir, img_dst_dir) if File.directory?(img_src_dir)

    nav_pane = NavigationPane.new(doc)
    doc.to_html(nav_pane, "#{path}/build/tests/protocols/")
  end
end

#render_all_source_filesObject



487
488
489
490
491
492
493
494
# File 'lib/almirah/project.rb', line 487

def render_all_source_files
  path = @configuration.project_root_directory
  FileUtils.mkdir_p("#{path}/build/source_files")

  @project_data.source_files.each do |doc|
    doc.to_html("#{path}/build/source_files/")
  end
end

#render_all_specifications(spec_list) ⇒ Object



451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
# File 'lib/almirah/project.rb', line 451

def render_all_specifications(spec_list)
  path = @configuration.project_root_directory

  FileUtils.mkdir_p("#{path}/build/specifications")

  spec_list.each do |doc|
    img_src_dir = "#{path}/specifications/#{doc.id}/img"
    img_dst_dir = "#{path}/build/specifications/#{doc.id}/img"

    FileUtils.mkdir_p(img_dst_dir)

    FileUtils.copy_entry(img_src_dir, img_dst_dir) if File.directory?(img_src_dir)

    nav_pane = NavigationPane.new(doc)
    doc.to_html(nav_pane, "#{path}/build/specifications/")
  end
end

#render_critical_chain_pageObject



513
514
515
516
517
518
519
520
521
# File 'lib/almirah/project.rb', line 513

def render_critical_chain_page
  return if @project_data.decisions.empty?

  path = @configuration.project_root_directory
  FileUtils.mkdir_p("#{path}/build/decisions")

  doc = DocFabric.create_critical_chain_page(@project)
  doc.to_html("#{path}/build/decisions/")
end

#render_decisions_overviewObject



503
504
505
506
507
508
509
510
511
# File 'lib/almirah/project.rb', line 503

def render_decisions_overview
  return if @project_data.decisions.empty?

  path = @configuration.project_root_directory
  FileUtils.mkdir_p("#{path}/build/decisions")

  doc = DocFabric.create_decisions_overview(@project)
  doc.to_html("#{path}/build/decisions/")
end

#render_indexObject



496
497
498
499
500
501
# File 'lib/almirah/project.rb', line 496

def render_index
  path = @configuration.project_root_directory

  doc = @index
  doc.to_html("#{path}/build/")
end

Reports cross-document links that could not be resolved (ADR-186, SRS-094), naming the linking document. The build still completes.



109
110
111
112
113
114
115
# File 'lib/almirah/project.rb', line 109

def report_broken_links
  broken = TextLine.broken_links
  return if broken.empty?

  ConsoleReporter.warn('broken links', broken.length)
  broken.each { |b| puts ConsoleReporter.warn_detail("  #{b[:document] || '?'}: #{b[:target]}") }
end

#report_kit_violationsObject

Reports the two kit gates and unresolved Depends On references (ADR-194), all as non-failing console warnings alongside report_broken_links.



201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
# File 'lib/almirah/project.rb', line 201

def report_kit_violations
  @kit_unresolved ||= []
  phase = @project_data.work_items.each_value.select(&:phase_order_violation?)
  cross = @project_data.work_items.each_value.select(&:cross_record_violation?)
  total = phase.length + cross.length + @kit_unresolved.length
  return if total.zero?

  ConsoleReporter.warn('kit violations', total)
  phase.each do |wi|
    blocking = wi.intra_record_predecessors.reject(&:done?).map(&:id).join(', ')
    puts ConsoleReporter.warn_detail("  phase order: #{wi.id} started before #{blocking}")
  end
  cross.each do |wi|
    blocking = wi.cross_record_predecessors.reject(&:done?).map(&:id).join(', ')
    puts ConsoleReporter.warn_detail("  not kitted: #{wi.id} needs #{blocking}")
  end
  @kit_unresolved.each do |u|
    puts ConsoleReporter.warn_detail("  unresolved Depends On: #{u[:record]} -> #{u[:target]}")
  end
end

#report_renderedObject



101
102
103
104
105
# File 'lib/almirah/project.rb', line 101

def report_rendered
  root = @configuration.project_root_directory
  base = root == Dir.pwd ? '.' : root
  ConsoleReporter.result('rendering HTML', File.join(base, 'build', 'index.html'))
end

#specifications_and_protocolsObject



43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
# File 'lib/almirah/project.rb', line 43

def specifications_and_protocols
  parse_all_specifications
  parse_all_protocols
  parse_all_source_files
  parse_decisions
  link_all_specifications
  link_all_protocols
  link_all_source_files
  link_all_decisions
  check_wrong_specification_referenced
  build_link_registry
  link_work_items
  create_index
  render_all_specifications(@project_data.specifications)
  render_all_specifications(@project_data.traceability_matrices)
  render_all_specifications(@project_data.coverage_matrices)
  render_all_protocols
  render_all_source_files
  render_all_specifications(@project_data.implementation_matrices) # intentionally after source file rendering
  render_decisions_overview
  render_critical_chain_page
  render_all_decisions
  render_index
  create_search_data
  report_broken_links
  report_kit_violations
  report_rendered
end

#specifications_and_results(test_run) ⇒ Object



72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
# File 'lib/almirah/project.rb', line 72

def specifications_and_results(test_run)
  parse_all_specifications
  parse_test_run test_run
  parse_all_source_files
  parse_decisions
  link_all_specifications
  link_all_protocols
  link_all_source_files
  link_all_decisions
  check_wrong_specification_referenced
  build_link_registry
  link_work_items
  create_index
  render_all_specifications(@project_data.specifications)
  render_all_specifications(@project_data.traceability_matrices)
  render_all_specifications(@project_data.coverage_matrices)
  render_all_protocols
  render_all_source_files
  render_all_specifications(@project_data.implementation_matrices) # intentionally after source file rendering
  render_decisions_overview
  render_critical_chain_page
  render_all_decisions
  render_index
  create_search_data
  report_broken_links
  report_kit_violations
  report_rendered
end