Index [Almirah.Spec] : decisions_spec.rb

[Almirah.Spec] : decisions_spec.rb

1.1 References

# UL DL COV DR
SC-AABV Provide a Decision Records Overview page with Sequence Number, Type, Title columns. SRS-041
SC-AABW ID derived from filename letters-digits prefix; full stem when no match. SRS-040
SC-AABX Provide a Decision Records Overview page with Sequence Number, Type, Title columns. SRS-041
SC-AABY Sequence Number is the digits portion of the ID. SRS-044
SC-AABZ Type is the letters portion of the ID, in upper case. SRS-045
SC-AACA Title comes from the YAML frontmatter title field. SRS-046
SC-AACB Top-nav Decision Records link on every rendered page, when at least one record exists. SRS-048
SC-AACC Top-nav Decision Records link on every rendered page, when at least one record exists. SRS-048
SC-AACD Top-nav Decision Records link on every rendered page, when at least one record exists. SRS-048
SC-AACE Render each decision record to an HTML page named after the Decision Record ID. SRS-047
SC-AACF Render each decision record to an HTML page named after the Decision Record ID. SRS-047
SC-AACG Top-nav Decision Records link on every rendered page, when at least one record exists. SRS-048
SC-AACH Title click in the overview navigates to the rendered decision page. SRS-042
SC-AACI Accept decision records placed in the decisions/ folder, including nested subfolders. SRS-043
SC-AACJ Render each decision record to an HTML page named after the Decision Record ID. SRS-047
SC-AACK Accept decision records placed in the decisions/ folder, including nested subfolders. SRS-043
SC-AACL Render each decision record to an HTML page named after the Decision Record ID. SRS-047
SC-AACM Top-nav Decision Records link on every rendered page, when at least one record exists. SRS-048
SC-AACN Title click in the overview navigates to the rendered decision page. SRS-042
SC-AACO Accept decision records placed in the decisions/ folder, including nested subfolders. SRS-043
SC-AACP ID derived from filename letters-digits prefix; full stem when no match. SRS-040
SC-AACQ Sequence Number is the digits portion of the ID. SRS-044
SC-AACR Type is the letters portion of the ID, in upper case. SRS-045
SC-AACS ID derived from filename letters-digits prefix; full stem when no match. SRS-040
SC-AACT Sequence Number is the digits portion of the ID. SRS-044
SC-AACU Type is the letters portion of the ID, in upper case. SRS-045
SC-AACV Title comes from the YAML frontmatter title field. SRS-046
SC-AACW Title comes from the YAML frontmatter title field. SRS-046
SC-AACX ID derived from filename letters-digits prefix; full stem when no match. SRS-040
SC-AACY Type is the letters portion of the ID, in upper case. SRS-045
SC-AACZ Top-nav Decision Records link on every rendered page, when at least one record exists. SRS-048
SC-AADA Top-nav Decision Records link on every rendered page, when at least one record exists. SRS-048
SC-AADB Decision Record current status comes from the "*"-marked row of the Status table. SRS-049
SC-AADC Decision Records Overview page has a Status column between Type and Title. SRS-051
SC-AADD Decision Records Overview page has a Status column between Type and Title. SRS-051
SC-AADE Render the "*" in the Status table marker column as "▶" in the rendered HTML. SRS-050
SC-AADF Highlight the current-status row in the Status table for visual emphasis. SRS-050
SC-AADG Current status is undefined when zero rows carry the marker. SRS-049
SC-AADH Decision Records Overview Status cell is empty when current status is undefined. SRS-051
SC-AADI Current status is undefined when more than one row carries the marker. SRS-049
SC-AADJ Decision Records Overview Status cell is empty when current status is undefined. SRS-051
SC-AADK Triangle substitution applies only to the Status table. SRS-050
SC-AADL Start Date is the earliest of Status Date and Scope Start Date columns. SRS-061
SC-AADM Start Date is rendered in the existing Start Date column, DD-MM-YYYY. SRS-065
SC-AADN Falls back to Status Date column when Scope is missing. SRS-061
SC-AADO Falls back to Scope Start Date column when Status is missing. SRS-061
SC-AADP Start Date is undefined when no date is parseable. SRS-064
SC-AADQ Start Date cell is empty when the attribute is undefined. SRS-065
SC-AADR Columns are identified by header text, not by position. SRS-063
SC-AADS Start Date is undefined when neither table is present. SRS-064
SC-AADT Expose Target Release Version attribute on each Decision Record. SRS-066
SC-AADU Render the Target Release Version attribute in the Release column. SRS-070
SC-AADV Release column header carries title="Target Release Version". SRS-070
SC-AADW Release column is placed between Target Date and Owner. SRS-070
SC-AADX Columns identified by header text, not by position. SRS-067
SC-AADY Target Release Version is undefined when section is missing. SRS-069
SC-AADZ Release cell is empty when attribute is undefined. SRS-070
SC-AAEA Target Release Version is undefined when the row is missing. SRS-069
SC-AAEB Target Release Version is undefined when the cell is empty. SRS-069
SC-AAEC Version value is passed through verbatim, no SemVer parsing.
SC-AAED Velocity chart placed in the second chart cell. SRS-071
SC-AAEE Six bars, Fridays ordered oldest-to-newest, DD-MM-YYYY labels. SRS-072
SC-AAEF Status as of date = latest parseable date <= date. SRS-073
SC-AAEG Record not yet proposed contributes to no bar. SRS-074
SC-AAEH Records with no Status table contribute to no velocity bar. SRS-075
SC-AAEI Status segments = union of every distinct status text. SRS-076
SC-AAEJ Same-date tie broken by document order; later row wins. SRS-073
SC-AAEK Future-dated rows are ignored for past Fridays. SRS-073
SC-AAEL A horizontal bar chart of records by current status sits in the third chart cell. SRS-083
SC-AAEM Records counted under their *-marked current status, matched case-sensitively. SRS-084
SC-AAEN Linear scale with the count shown in each bar label. SRS-086
SC-AAEO No "Undefined" category when every record has a defined current status. SRS-085
SC-AAEP Records with a missing or ambiguous marker are grouped under "Undefined". SRS-085
SC-AAEQ The "Undefined" category is ordered last, after all real statuses. SRS-087

1.2 Source Code

# frozen_string_literal: true

require_relative 'spec_helper'

RSpec.describe 'Decision Records', type: :aruba do
  context 'when the project has decision records' do
    before do
      write_file('myproject/project.yml', <<~YML)
        specifications:
          input: []
      YML
      write_file('myproject/specifications/req/req.md', <<~MD)
        # Requirements

        [REQ-001] A first requirement.
      MD
      write_file('myproject/decisions/adr-001-foo.md', <<~MD)
        ---
        title: "ADR-001: First Decision"
        ---

        ## Context

        A first decision.
      MD
      write_file('myproject/decisions/adr-002-bar.md', <<~MD)
        ---
        title: "ADR-002: Second Decision"
        ---

        ## Context

        A second decision.
      MD
      write_file('myproject/decisions/README.txt', 'ignored non-markdown file')
      run_command_and_stop('almirah please myproject')
    end

    # <REQ> Provide a Decision Records Overview page with Sequence Number, Type, Title columns. >[SRS-041] </REQ>
    it 'renders build/decisions/overview.html' do
      expect(File.exist?(expand_path('myproject/build/decisions/overview.html'))).to be true
    end

    # <REQ> ID derived from filename letters-digits prefix; full stem when no match. >[SRS-040] </REQ>
    # <REQ> Provide a Decision Records Overview page with Sequence Number, Type, Title columns. >[SRS-041] </REQ>
    # <REQ> Sequence Number is the digits portion of the ID. >[SRS-044] </REQ>
    # <REQ> Type is the letters portion of the ID, in upper case. >[SRS-045] </REQ>
    # <REQ> Title comes from the YAML frontmatter title field. >[SRS-046] </REQ>
    it 'lists all parsed decision records on the overview page' do
      doc = Nokogiri::HTML(File.read(expand_path('myproject/build/decisions/overview.html')))
      sequence_numbers = doc.xpath('//td[@class="item_id"]').map { |c| c.text.strip }
      anchor_ids = doc.xpath('//td[@class="item_id"]//a').map { |a| a['id'] }
      types = doc.xpath('//td[@class="item_type"]').map { |c| c.text.strip }
      titles = doc.xpath('//td[@class="item_text"]').map { |c| c.text.strip }
      expect(sequence_numbers).to contain_exactly('001', '002')
      expect(anchor_ids).to contain_exactly('adr-001', 'adr-002')
      expect(types).to contain_exactly('ADR', 'ADR')
      expect(titles).to contain_exactly('ADR-001: First Decision', 'ADR-002: Second Decision')
    end

    # <REQ> Top-nav Decision Records link on every rendered page, when at least one record exists. >[SRS-048] </REQ>
    it 'adds the Decision Records link to the index page top-nav' do
      doc = Nokogiri::HTML(File.read(expand_path('myproject/build/index.html')))
      link = doc.at_css('#decisions_menu_item')
      expect(link).not_to be_nil
      expect(link['href']).to eq('decisions/overview.html')
      expect(link.text).to include('Decision Records')
      expect(link.at_css('i')['class']).to include('fa-gavel')
    end

    # <REQ> Top-nav Decision Records link on every rendered page, when at least one record exists. >[SRS-048] </REQ>
    it 'adds the Decision Records link to specification page top-nav with correct relative path' do
      doc = Nokogiri::HTML(File.read(expand_path('myproject/build/specifications/req/req.html')))
      link = doc.at_css('#decisions_menu_item')
      expect(link).not_to be_nil
      expect(link['href']).to eq('../../decisions/overview.html')
    end

    # <REQ> Top-nav Decision Records link on every rendered page, when at least one record exists. >[SRS-048] </REQ>
    it 'adds the Decision Records link to the overview page itself' do
      doc = Nokogiri::HTML(File.read(expand_path('myproject/build/decisions/overview.html')))
      link = doc.at_css('#decisions_menu_item')
      expect(link).not_to be_nil
      expect(link['href']).to eq('overview.html')
    end

    # <REQ> Render each decision record to an HTML page named after the Decision Record ID. >[SRS-047] </REQ>
    it 'renders an HTML page per decision using the id as the filename' do
      expect(File.exist?(expand_path('myproject/build/decisions/adr-001.html'))).to be true
      expect(File.exist?(expand_path('myproject/build/decisions/adr-002.html'))).to be true
    end

    # <REQ> Render each decision record to an HTML page named after the Decision Record ID. >[SRS-047] </REQ>
    # <REQ> Top-nav Decision Records link on every rendered page, when at least one record exists. >[SRS-048] </REQ>
    it 'sets correct CSS/JS and top-nav paths on top-level decision pages' do
      doc = Nokogiri::HTML(File.read(expand_path('myproject/build/decisions/adr-001.html')))
      css_hrefs = doc.css('link[rel="stylesheet"]').map { |l| l['href'] }
      js_srcs   = doc.css('script[src]').map { |s| s['src'] }
      expect(css_hrefs).to include('../css/main.css')
      expect(js_srcs).to include('../scripts/main.js')
      expect(doc.at_css('#index_menu_item')['href']).to eq('../index.html')
      expect(doc.at_css('#decisions_menu_item')['href']).to eq('overview.html')
    end

    # <REQ> Title click in the overview navigates to the rendered decision page. >[SRS-042] </REQ>
    it 'links each overview title to the rendered decision page' do
      doc = Nokogiri::HTML(File.read(expand_path('myproject/build/decisions/overview.html')))
      hrefs = doc.xpath('//td[@class="item_text"]/a').map { |a| a['href'] }
      expect(hrefs).to contain_exactly('./adr-001.html', './adr-002.html')
    end
  end

  context 'when a decision record lives in a nested folder' do
    before do
      write_file('myproject/project.yml', <<~YML)
        specifications:
          input: []
      YML
      write_file('myproject/decisions/enhancements/adr-200-nested.md', <<~MD)
        ---
        title: "ADR-200: A nested decision"
        ---

        body
      MD
      run_command_and_stop('almirah please myproject')
    end

    # <REQ> Accept decision records placed in the decisions/ folder, including nested subfolders. >[SRS-043] </REQ>
    # <REQ> Render each decision record to an HTML page named after the Decision Record ID. >[SRS-047] </REQ>
    it 'mirrors the source folder structure in build/decisions' do
      expect(File.exist?(expand_path('myproject/build/decisions/enhancements/adr-200.html'))).to be true
    end

    # <REQ> Accept decision records placed in the decisions/ folder, including nested subfolders. >[SRS-043] </REQ>
    # <REQ> Render each decision record to an HTML page named after the Decision Record ID. >[SRS-047] </REQ>
    # <REQ> Top-nav Decision Records link on every rendered page, when at least one record exists. >[SRS-048] </REQ>
    it 'sets CSS/JS and top-nav paths relative to the nested depth' do
      doc = Nokogiri::HTML(File.read(expand_path('myproject/build/decisions/enhancements/adr-200.html')))
      css_hrefs = doc.css('link[rel="stylesheet"]').map { |l| l['href'] }
      js_srcs   = doc.css('script[src]').map { |s| s['src'] }
      expect(css_hrefs).to include('../../css/main.css')
      expect(js_srcs).to include('../../scripts/main.js')
      expect(doc.at_css('#index_menu_item')['href']).to eq('../../index.html')
      expect(doc.at_css('#decisions_menu_item')['href']).to eq('../overview.html')
    end

    # <REQ> Title click in the overview navigates to the rendered decision page. >[SRS-042] </REQ>
    # <REQ> Accept decision records placed in the decisions/ folder, including nested subfolders. >[SRS-043] </REQ>
    it 'links the overview title to the nested rendered page' do
      doc = Nokogiri::HTML(File.read(expand_path('myproject/build/decisions/overview.html')))
      hrefs = doc.xpath('//td[@class="item_text"]/a').map { |a| a['href'] }
      expect(hrefs).to eq(['./enhancements/adr-200.html'])
    end
  end

  context 'when a decision record has no frontmatter title' do
    before do
      write_file('myproject/project.yml', <<~YML)
        specifications:
          input: []
      YML
      write_file('myproject/decisions/adr-001-titleless.md', "body without frontmatter or heading\n")
      run_command_and_stop('almirah please myproject')
    end

    # <REQ> ID derived from filename letters-digits prefix; full stem when no match. >[SRS-040] </REQ>
    # <REQ> Sequence Number is the digits portion of the ID. >[SRS-044] </REQ>
    # <REQ> Type is the letters portion of the ID, in upper case. >[SRS-045] </REQ>
    it 'derives the id from the letters-digits prefix and falls back to <id>.md for the title' do
      doc = Nokogiri::HTML(File.read(expand_path('myproject/build/decisions/overview.html')))
      sequence_numbers = doc.xpath('//td[@class="item_id"]').map { |c| c.text.strip }
      anchor_ids = doc.xpath('//td[@class="item_id"]//a').map { |a| a['id'] }
      types = doc.xpath('//td[@class="item_type"]').map { |c| c.text.strip }
      titles = doc.xpath('//td[@class="item_text"]').map { |c| c.text.strip }
      expect(sequence_numbers).to eq(['001'])
      expect(anchor_ids).to eq(['adr-001'])
      expect(types).to eq(['ADR'])
      expect(titles).to eq(['adr-001.md'])
    end
  end

  context 'when a decision record filename has no descriptive suffix' do
    before do
      write_file('myproject/project.yml', <<~YML)
        specifications:
          input: []
      YML
      write_file('myproject/decisions/ise-1892.md', <<~MD)
        ---
        title: "ISE-1892: A redmine-style record"
        ---

        body
      MD
      run_command_and_stop('almirah please myproject')
    end

    # <REQ> ID derived from filename letters-digits prefix; full stem when no match. >[SRS-040] </REQ>
    # <REQ> Sequence Number is the digits portion of the ID. >[SRS-044] </REQ>
    # <REQ> Type is the letters portion of the ID, in upper case. >[SRS-045] </REQ>
    # <REQ> Title comes from the YAML frontmatter title field. >[SRS-046] </REQ>
    it 'uses the full letters-digits stem as the id and shows the digits in the # column' do
      doc = Nokogiri::HTML(File.read(expand_path('myproject/build/decisions/overview.html')))
      sequence_numbers = doc.xpath('//td[@class="item_id"]').map { |c| c.text.strip }
      anchor_ids = doc.xpath('//td[@class="item_id"]//a').map { |a| a['id'] }
      types = doc.xpath('//td[@class="item_type"]').map { |c| c.text.strip }
      titles = doc.xpath('//td[@class="item_text"]').map { |c| c.text.strip }
      expect(sequence_numbers).to eq(['1892'])
      expect(anchor_ids).to eq(['ise-1892'])
      expect(types).to eq(['ISE'])
      expect(titles).to eq(['ISE-1892: A redmine-style record'])
    end
  end

  context 'when a decision record has both frontmatter title and an H1' do
    before do
      write_file('myproject/project.yml', <<~YML)
        specifications:
          input: []
      YML
      write_file('myproject/decisions/adr-042-precedence.md', <<~MD)
        ---
        title: "From Frontmatter"
        ---

        # Different H1 In Body

        body
      MD
      run_command_and_stop('almirah please myproject')
    end

    # <REQ> Title comes from the YAML frontmatter title field. >[SRS-046] </REQ>
    it 'uses the frontmatter title rather than the H1' do
      doc = Nokogiri::HTML(File.read(expand_path('myproject/build/decisions/overview.html')))
      titles = doc.xpath('//td[@class="item_text"]').map { |c| c.text.strip }
      expect(titles).to eq(['From Frontmatter'])
    end
  end

  context 'when a decision record filename does not match the convention' do
    before do
      write_file('myproject/project.yml', <<~YML)
        specifications:
          input: []
      YML
      write_file('myproject/decisions/meeting-notes.md', <<~MD)
        # Meeting notes

        body
      MD
      run_command_and_stop('almirah please myproject')
    end

    # <REQ> ID derived from filename letters-digits prefix; full stem when no match. >[SRS-040] </REQ>
    # <REQ> Type is the letters portion of the ID, in upper case. >[SRS-045] </REQ>
    it 'falls back to the full filename stem as the id and as the # column label, with empty Type' do
      doc = Nokogiri::HTML(File.read(expand_path('myproject/build/decisions/overview.html')))
      sequence_numbers = doc.xpath('//td[@class="item_id"]').map { |c| c.text.strip }
      anchor_ids = doc.xpath('//td[@class="item_id"]//a').map { |a| a['id'] }
      types = doc.xpath('//td[@class="item_type"]').map { |c| c.text.strip }
      expect(sequence_numbers).to eq(['meeting-notes'])
      expect(anchor_ids).to eq(['meeting-notes'])
      expect(types).to eq([''])
    end
  end

  context 'when the project has no decision records' do
    before do
      write_file('myproject/project.yml', <<~YML)
        specifications:
          input: []
      YML
      write_file('myproject/specifications/req/req.md', <<~MD)
        # Requirements

        [REQ-001] A first requirement.
      MD
      run_command_and_stop('almirah please myproject')
    end

    it 'does not create build/decisions/overview.html' do
      expect(File.exist?(expand_path('myproject/build/decisions/overview.html'))).to be false
    end

    # <REQ> Top-nav Decision Records link on every rendered page, when at least one record exists. >[SRS-048] </REQ>
    it 'does not add the Decision Records link to the index page' do
      doc = Nokogiri::HTML(File.read(expand_path('myproject/build/index.html')))
      expect(doc.at_css('#decisions_menu_item')).to be_nil
    end

    # <REQ> Top-nav Decision Records link on every rendered page, when at least one record exists. >[SRS-048] </REQ>
    it 'does not add the Decision Records link to spec pages' do
      doc = Nokogiri::HTML(File.read(expand_path('myproject/build/specifications/req/req.html')))
      expect(doc.at_css('#decisions_menu_item')).to be_nil
    end
  end

  context 'when a decision record has a single "*" current-status marker' do
    before do
      write_file('myproject/project.yml', "specifications:\n  input: []\n")
      write_file('myproject/decisions/adr-300-marked.md', <<~MD)
        ---
        title: "ADR-300: Marked Record"
        ---

        # Status

        |  | Date | Status |
        |:---:|---|---|
        |   | 17-05-2026 | Proposed |
        | * | 17-05-2026 | Accepted |
      MD
      run_command_and_stop('almirah please myproject')
    end

    # <REQ> Decision Record current status comes from the "*"-marked row of the Status table. >[SRS-049] </REQ>
    # <REQ> Decision Records Overview page has a Status column between Type and Title. >[SRS-051] </REQ>
    it 'shows the marked row status value in the overview Status column' do
      doc = Nokogiri::HTML(File.read(expand_path('myproject/build/decisions/overview.html')))
      statuses = doc.xpath('//td[@class="item_status"]').map { |c| c.text.strip }
      expect(statuses).to eq(['Accepted'])
    end

    # <REQ> Decision Records Overview page has a Status column between Type and Title. >[SRS-051] </REQ>
    it 'places the Status column between Type and Title in the overview' do
      doc = Nokogiri::HTML(File.read(expand_path('myproject/build/decisions/overview.html')))
      table_xpath = '//table[contains(concat(" ", @class, " "), " controlled ")]/thead/th'
      header_cells = doc.xpath(table_xpath).map { |th| th.text.strip }
      expect(header_cells).to eq(['#', 'Type', 'Status', 'Title', 'Start Date', 'Target Date', 'Release', 'Owner'])
    end

    # <REQ> Render the "*" in the Status table marker column as "▶" in the rendered HTML. >[SRS-050] </REQ>
    it 'renders the marker as ▶ in the decision page Status table' do
      doc = Nokogiri::HTML(File.read(expand_path('myproject/build/decisions/adr-300.html')))
      status_table = doc.css('table.markdown_table').first
      first_column_cells = status_table.css('tr').map { |tr| tr.css('td').first&.text&.strip }.compact
      expect(first_column_cells).to include('▶')
      expect(first_column_cells).not_to include('*')
    end

    # <REQ> Highlight the current-status row in the Status table for visual emphasis. >[SRS-050] </REQ>
    it 'tags the marked row with the current_status class so CSS can highlight it' do
      doc = Nokogiri::HTML(File.read(expand_path('myproject/build/decisions/adr-300.html')))
      status_table = doc.css('table.markdown_table').first
      marked_rows = status_table.css('tr.current_status')
      expect(marked_rows.length).to eq(1)
      expect(marked_rows.first.css('td').first.text.strip).to eq('▶')
      unmarked_rows = status_table.css('tr:not(.current_status)')
      expect(unmarked_rows.any? { |tr| tr.css('td').first&.text&.include?('▶') }).to be false
    end
  end

  context 'when a decision record has no current-status marker' do
    before do
      write_file('myproject/project.yml', "specifications:\n  input: []\n")
      write_file('myproject/decisions/adr-301-unmarked.md', <<~MD)
        ---
        title: "ADR-301: Unmarked Record"
        ---

        # Status

        |  | Date | Status |
        |:---:|---|---|
        |   | 17-05-2026 | Proposed |
      MD
      run_command_and_stop('almirah please myproject')
    end

    # <REQ> Current status is undefined when zero rows carry the marker. >[SRS-049] </REQ>
    # <REQ> Decision Records Overview Status cell is empty when current status is undefined. >[SRS-051] </REQ>
    it 'leaves the overview Status cell empty' do
      doc = Nokogiri::HTML(File.read(expand_path('myproject/build/decisions/overview.html')))
      statuses = doc.xpath('//td[@class="item_status"]').map { |c| c.text.strip }
      expect(statuses).to eq([''])
    end
  end

  context 'when a decision record has multiple current-status markers' do
    before do
      write_file('myproject/project.yml', "specifications:\n  input: []\n")
      write_file('myproject/decisions/adr-302-multi.md', <<~MD)
        ---
        title: "ADR-302: Multi-Marker Record"
        ---

        # Status

        |  | Date | Status |
        |:---:|---|---|
        | * | 17-05-2026 | Proposed |
        | * | 17-05-2026 | Accepted |
      MD
      run_command_and_stop('almirah please myproject')
    end

    # <REQ> Current status is undefined when more than one row carries the marker. >[SRS-049] </REQ>
    # <REQ> Decision Records Overview Status cell is empty when current status is undefined. >[SRS-051] </REQ>
    it 'leaves the overview Status cell empty' do
      doc = Nokogiri::HTML(File.read(expand_path('myproject/build/decisions/overview.html')))
      statuses = doc.xpath('//td[@class="item_status"]').map { |c| c.text.strip }
      expect(statuses).to eq([''])
    end
  end

  context 'when a decision record has "*" in a non-Status table' do
    before do
      write_file('myproject/project.yml', "specifications:\n  input: []\n")
      write_file('myproject/decisions/adr-303-other-tables.md', <<~MD)
        ---
        title: "ADR-303: Marker In Non-Status Table"
        ---

        # Status

        |  | Date | Status |
        |:---:|---|---|
        | * | 17-05-2026 | Accepted |

        # Scope

        | Item | Status | Note |
        |---|---|---|
        | Code | Done | * |
      MD
      run_command_and_stop('almirah please myproject')
    end

    # <REQ> Triangle substitution applies only to the Status table. >[SRS-050] </REQ>
    it 'does not substitute "*" outside the Status table' do
      doc = Nokogiri::HTML(File.read(expand_path('myproject/build/decisions/adr-303.html')))
      tables = doc.css('table.markdown_table')
      status_first_col = tables[0].css('tr').map { |tr| tr.css('td').first&.text&.strip }.compact
      scope_cells = tables[1].css('td').map { |td| td.text.strip }
      expect(status_first_col).to include('▶')
      expect(scope_cells).to include('*')
      expect(scope_cells).not_to include('▶')
    end
  end

  context 'when a decision record has dates in both Status and Scope tables' do
    before do
      write_file('myproject/project.yml', "specifications:\n  input: []\n")
      write_file('myproject/decisions/adr-400-both.md', <<~MD)
        ---
        title: "ADR-400: Earliest Wins"
        ---

        # Status

        |  | Date | Status |
        |:---:|---|---|
        |   | 15-05-2026 | Proposed |
        | * | 17-05-2026 | Accepted |

        # Scope

        | Item | Status | Start Date | Target Date | Description |
        |---|---|---|---|---|
        | Code | Proposed | 10-05-2026 |  | Earlier work |
        | Tests | Proposed | 20-05-2026 |  | Later work |
      MD
      run_command_and_stop('almirah please myproject')
    end

    # <REQ> Start Date is the earliest of Status Date and Scope Start Date columns. >[SRS-061] </REQ>
    # <REQ> Start Date is rendered in the existing Start Date column, DD-MM-YYYY. >[SRS-065] </REQ>
    it 'picks the earliest date across both tables and renders it on the overview' do
      doc = Nokogiri::HTML(File.read(expand_path('myproject/build/decisions/overview.html')))
      row = doc.at_xpath('//a[@id="adr-400"]/ancestor::tr')
      cells = row.css('td.item_meta').map { |c| c.text.strip }
      expect(cells.first).to eq('10-05-2026')
    end
  end

  context 'when a decision record has no Scope table' do
    before do
      write_file('myproject/project.yml', "specifications:\n  input: []\n")
      write_file('myproject/decisions/adr-401-status-only.md', <<~MD)
        ---
        title: "ADR-401: Status Only"
        ---

        # Status

        |  | Date | Status |
        |:---:|---|---|
        | * | 12-05-2026 | Proposed |
        |   | 18-05-2026 | Accepted |
      MD
      run_command_and_stop('almirah please myproject')
    end

    # <REQ> Falls back to Status Date column when Scope is missing. >[SRS-061] </REQ>
    it 'uses the earliest Status table date' do
      doc = Nokogiri::HTML(File.read(expand_path('myproject/build/decisions/overview.html')))
      row = doc.at_xpath('//a[@id="adr-401"]/ancestor::tr')
      cells = row.css('td.item_meta').map { |c| c.text.strip }
      expect(cells.first).to eq('12-05-2026')
    end
  end

  context 'when a decision record has no Status table but has a Scope table' do
    before do
      write_file('myproject/project.yml', "specifications:\n  input: []\n")
      write_file('myproject/decisions/adr-402-scope-only.md', <<~MD)
        ---
        title: "ADR-402: Scope Only"
        ---

        # Scope

        | Item | Status | Start Date | Target Date | Description |
        |---|---|---|---|---|
        | Code | Proposed | 09-05-2026 |  | Work |
      MD
      run_command_and_stop('almirah please myproject')
    end

    # <REQ> Falls back to Scope Start Date column when Status is missing. >[SRS-061] </REQ>
    it 'uses the Scope Start Date column' do
      doc = Nokogiri::HTML(File.read(expand_path('myproject/build/decisions/overview.html')))
      row = doc.at_xpath('//a[@id="adr-402"]/ancestor::tr')
      cells = row.css('td.item_meta').map { |c| c.text.strip }
      expect(cells.first).to eq('09-05-2026')
    end
  end

  context 'when neither table has a parseable date' do
    before do
      write_file('myproject/project.yml', "specifications:\n  input: []\n")
      write_file('myproject/decisions/adr-403-nodates.md', <<~MD)
        ---
        title: "ADR-403: No Parseable Dates"
        ---

        # Status

        |  | Date | Status |
        |:---:|---|---|
        | * | TBD | Proposed |

        # Scope

        | Item | Status | Start Date | Target Date | Description |
        |---|---|---|---|---|
        | Code | Proposed |  |  | Free text |
      MD
      run_command_and_stop('almirah please myproject')
    end

    # <REQ> Start Date is undefined when no date is parseable. >[SRS-064] </REQ>
    # <REQ> Start Date cell is empty when the attribute is undefined. >[SRS-065] </REQ>
    it 'leaves the Start Date cell empty without raising' do
      doc = Nokogiri::HTML(File.read(expand_path('myproject/build/decisions/overview.html')))
      row = doc.at_xpath('//a[@id="adr-403"]/ancestor::tr')
      cells = row.css('td.item_meta').map { |c| c.text.strip }
      expect(cells.first).to eq('')
    end
  end

  context 'when the Scope table has its columns in a non-default order' do
    before do
      write_file('myproject/project.yml', "specifications:\n  input: []\n")
      write_file('myproject/decisions/adr-404-reordered.md', <<~MD)
        ---
        title: "ADR-404: Reordered Columns"
        ---

        # Scope

        | Description | Start Date | Item | Status |
        |---|---|---|---|
        | Free text | 07-05-2026 | Code | Proposed |
      MD
      run_command_and_stop('almirah please myproject')
    end

    # <REQ> Columns are identified by header text, not by position. >[SRS-063] </REQ>
    it 'reads the Start Date column by header text regardless of position' do
      doc = Nokogiri::HTML(File.read(expand_path('myproject/build/decisions/overview.html')))
      row = doc.at_xpath('//a[@id="adr-404"]/ancestor::tr')
      cells = row.css('td.item_meta').map { |c| c.text.strip }
      expect(cells.first).to eq('07-05-2026')
    end
  end

  context 'when a decision record has no dated tables at all' do
    before do
      write_file('myproject/project.yml', "specifications:\n  input: []\n")
      write_file('myproject/decisions/adr-405-bare.md', <<~MD)
        ---
        title: "ADR-405: Bare Record"
        ---

        body without status or scope
      MD
      run_command_and_stop('almirah please myproject')
    end

    # <REQ> Start Date is undefined when neither table is present. >[SRS-064] </REQ>
    it 'leaves the Start Date cell empty' do
      doc = Nokogiri::HTML(File.read(expand_path('myproject/build/decisions/overview.html')))
      row = doc.at_xpath('//a[@id="adr-405"]/ancestor::tr')
      cells = row.css('td.item_meta').map { |c| c.text.strip }
      expect(cells.first).to eq('')
    end
  end

  context 'when a decision record has a Software Versions section with a Target Release Version row' do
    before do
      write_file('myproject/project.yml', "specifications:\n  input: []\n")
      write_file('myproject/decisions/adr-500-versioned.md', <<~MD)
        ---
        title: "ADR-500: Versioned Record"
        ---

        # Software Versions

        | Software Version Category | Software Version ID |
        |---|---|
        | Latest Released Version | 0.3.1 |
        | Issue Found in Version | 0.4.0 |
        | Target Release Version | 0.4.0 |
      MD
      run_command_and_stop('almirah please myproject')
    end

    # <REQ> Expose Target Release Version attribute on each Decision Record. >[SRS-066] </REQ>
    # <REQ> Render the Target Release Version attribute in the Release column. >[SRS-070] </REQ>
    it 'renders the Target Release Version value in the Release column' do
      doc = Nokogiri::HTML(File.read(expand_path('myproject/build/decisions/overview.html')))
      row = doc.at_xpath('//a[@id="adr-500"]/ancestor::tr')
      cells = row.css('td.item_meta').map { |c| c.text.strip }
      expect(cells[2]).to eq('0.4.0')
    end

    # <REQ> Release column header carries title="Target Release Version". >[SRS-070] </REQ>
    it 'tags the Release header with a Target Release Version title attribute' do
      doc = Nokogiri::HTML(File.read(expand_path('myproject/build/decisions/overview.html')))
      headers = doc.xpath('//table[contains(concat(" ", @class, " "), " controlled ")]/thead/th')
      release_header = headers.find { |th| th.text.strip == 'Release' }
      expect(release_header).not_to be_nil
      expect(release_header['title']).to eq('Target Release Version')
    end

    # <REQ> Release column is placed between Target Date and Owner. >[SRS-070] </REQ>
    it 'places Release between Target Date and Owner in the overview' do
      doc = Nokogiri::HTML(File.read(expand_path('myproject/build/decisions/overview.html')))
      xpath = '//table[contains(concat(" ", @class, " "), " controlled ")]/thead/th'
      header_cells = doc.xpath(xpath).map { |th| th.text.strip }
      release_index = header_cells.index('Release')
      expect(header_cells[release_index - 1]).to eq('Target Date')
      expect(header_cells[release_index + 1]).to eq('Owner')
    end
  end

  context 'when the Software Versions table has columns in a non-default order' do
    before do
      write_file('myproject/project.yml', "specifications:\n  input: []\n")
      write_file('myproject/decisions/adr-501-reordered.md', <<~MD)
        ---
        title: "ADR-501: Reordered Software Versions"
        ---

        # Software Versions

        | Software Version ID | Software Version Category |
        |---|---|
        | 0.3.1 | Latest Released Version |
        | 0.5.0 | Target Release Version |
      MD
      run_command_and_stop('almirah please myproject')
    end

    # <REQ> Columns identified by header text, not by position. >[SRS-067] </REQ>
    it 'reads the Software Version ID column by header text regardless of position' do
      doc = Nokogiri::HTML(File.read(expand_path('myproject/build/decisions/overview.html')))
      row = doc.at_xpath('//a[@id="adr-501"]/ancestor::tr')
      cells = row.css('td.item_meta').map { |c| c.text.strip }
      expect(cells[2]).to eq('0.5.0')
    end
  end

  context 'when a decision record has no Software Versions section' do
    before do
      write_file('myproject/project.yml', "specifications:\n  input: []\n")
      write_file('myproject/decisions/adr-502-no-versions.md', <<~MD)
        ---
        title: "ADR-502: No Software Versions"
        ---

        body without a Software Versions section
      MD
      run_command_and_stop('almirah please myproject')
    end

    # <REQ> Target Release Version is undefined when section is missing. >[SRS-069] </REQ>
    # <REQ> Release cell is empty when attribute is undefined. >[SRS-070] </REQ>
    it 'leaves the Release cell empty' do
      doc = Nokogiri::HTML(File.read(expand_path('myproject/build/decisions/overview.html')))
      row = doc.at_xpath('//a[@id="adr-502"]/ancestor::tr')
      cells = row.css('td.item_meta').map { |c| c.text.strip }
      expect(cells[2]).to eq('')
    end
  end

  context 'when the Software Versions table is missing the Target Release Version row' do
    before do
      write_file('myproject/project.yml', "specifications:\n  input: []\n")
      write_file('myproject/decisions/adr-503-no-row.md', <<~MD)
        ---
        title: "ADR-503: Missing Target Row"
        ---

        # Software Versions

        | Software Version Category | Software Version ID |
        |---|---|
        | Latest Released Version | 0.3.1 |
        | Issue Found in Version | 0.4.0 |
      MD
      run_command_and_stop('almirah please myproject')
    end

    # <REQ> Target Release Version is undefined when the row is missing. >[SRS-069] </REQ>
    it 'leaves the Release cell empty when no Target Release Version row exists' do
      doc = Nokogiri::HTML(File.read(expand_path('myproject/build/decisions/overview.html')))
      row = doc.at_xpath('//a[@id="adr-503"]/ancestor::tr')
      cells = row.css('td.item_meta').map { |c| c.text.strip }
      expect(cells[2]).to eq('')
    end
  end

  context 'when the Target Release Version cell is empty' do
    before do
      write_file('myproject/project.yml', "specifications:\n  input: []\n")
      write_file('myproject/decisions/adr-504-empty-cell.md', <<~MD)
        ---
        title: "ADR-504: Empty Target Release Version"
        ---

        # Software Versions

        | Software Version Category | Software Version ID |
        |---|---|
        | Latest Released Version | 0.3.1 |
        | Target Release Version |  |
      MD
      run_command_and_stop('almirah please myproject')
    end

    # <REQ> Target Release Version is undefined when the cell is empty. >[SRS-069] </REQ>
    it 'leaves the Release cell empty when the source cell is empty' do
      doc = Nokogiri::HTML(File.read(expand_path('myproject/build/decisions/overview.html')))
      row = doc.at_xpath('//a[@id="adr-504"]/ancestor::tr')
      cells = row.css('td.item_meta').map { |c| c.text.strip }
      expect(cells[2]).to eq('')
    end
  end

  context 'when the Target Release Version value is non-SemVer free text' do
    before do
      write_file('myproject/project.yml', "specifications:\n  input: []\n")
      write_file('myproject/decisions/adr-505-freeform.md', <<~MD)
        ---
        title: "ADR-505: Freeform Version Value"
        ---

        # Software Versions

        | Software Version Category | Software Version ID |
        |---|---|
        | Target Release Version | n/a |
      MD
      run_command_and_stop('almirah please myproject')
    end

    # <REQ> Version value is passed through verbatim, no SemVer parsing. >[SRS-066] >[SRS-070] </REQ>
    it 'passes free-text values through verbatim' do
      doc = Nokogiri::HTML(File.read(expand_path('myproject/build/decisions/overview.html')))
      row = doc.at_xpath('//a[@id="adr-505"]/ancestor::tr')
      cells = row.css('td.item_meta').map { |c| c.text.strip }
      expect(cells[2]).to eq('n/a')
    end
  end

  # --- Velocity chart (ADR-182) -----------------------------------------------
  #
  # The chart shows record counts per status as of each of the last 6 Fridays.
  # Tests use fixture dates safely in the past (2024) so records reliably appear
  # in every bar regardless of when the suite is run.

  def velocity_data_block(html)
    block = html[/decisions_velocity_bar.*?data:\s*(\{.*?\}),\s*options:/m, 1]
    JSON.parse(block)
  end

  context 'when the project has decision records with dated Status tables' do
    before do
      write_file('myproject/project.yml', "specifications:\n  input: []\n")
      write_file('myproject/decisions/adr-600-old-impl.md', <<~MD)
        ---
        title: "ADR-600: Implemented Long Ago"
        ---

        # Status

        |  | Date | Status |
        |:---:|---|---|
        |   | 01-01-2024 | Proposed |
        |   | 02-01-2024 | Accepted |
        | * | 03-01-2024 | Implemented |
      MD
      run_command_and_stop('almirah please myproject')
    end

    # <REQ> Velocity chart placed in the second chart cell. >[SRS-071] </REQ>
    it 'emits a stacked bar chart in the second chart cell' do
      html = File.read(expand_path('myproject/build/decisions/overview.html'))
      expect(html).to include('id="decisions_velocity_bar"')
      expect(html).to include("type: 'bar'")
      expect(html).to match(/x:\s*\{\s*stacked:\s*true\s*\}/)
      expect(html).to match(/y:\s*\{\s*stacked:\s*true/)
    end

    # <REQ> Six bars, Fridays ordered oldest-to-newest, DD-MM-YYYY labels. >[SRS-072] </REQ>
    it 'renders six DD-MM-YYYY labels, 7 days apart, ending with the most recent Friday on or before today' do
      data = velocity_data_block(File.read(expand_path('myproject/build/decisions/overview.html')))
      labels = data['labels']
      expect(labels.length).to eq(6)
      labels.each { |l| expect(l).to match(/\A\d{2}-\d{2}-\d{4}\z/) }
      dates = labels.map { |l| Date.strptime(l, '%d-%m-%Y') }
      dates.each_cons(2) { |a, b| expect(b - a).to eq(7) }
      expect(dates.last.wday).to eq(5)
      expect(dates.last).to be <= Date.today
      expect(Date.today - dates.last).to be < 7
    end

    # <REQ> Status as of date = latest parseable date <= date. >[SRS-073] </REQ>
    it 'classifies an old fully-Implemented record as Implemented in every bar' do
      data = velocity_data_block(File.read(expand_path('myproject/build/decisions/overview.html')))
      implemented = data['datasets'].find { |d| d['label'] == 'Implemented' }
      expect(implemented).not_to be_nil
      expect(implemented['data']).to eq([1, 1, 1, 1, 1, 1])
    end
  end

  context 'when a decision record is dated in the future' do
    before do
      write_file('myproject/project.yml', "specifications:\n  input: []\n")
      write_file('myproject/decisions/adr-601-future.md', <<~MD)
        ---
        title: "ADR-601: Not Yet Proposed"
        ---

        # Status

        |  | Date | Status |
        |:---:|---|---|
        | * | 01-01-2030 | Proposed |
      MD
      run_command_and_stop('almirah please myproject')
    end

    # <REQ> Record not yet proposed contributes to no bar. >[SRS-074] </REQ>
    it 'contributes to no bar when its earliest date is after every Friday in the window' do
      data = velocity_data_block(File.read(expand_path('myproject/build/decisions/overview.html')))
      totals = Array.new(data['labels'].length, 0)
      data['datasets'].each do |ds|
        ds['data'].each_with_index { |v, i| totals[i] += v }
      end
      expect(totals).to eq([0, 0, 0, 0, 0, 0])
    end
  end

  context 'when a decision record has no Status section' do
    before do
      write_file('myproject/project.yml', "specifications:\n  input: []\n")
      write_file('myproject/decisions/adr-602-statusless.md', <<~MD)
        ---
        title: "ADR-602: No Status Section"
        ---

        body without status
      MD
      run_command_and_stop('almirah please myproject')
    end

    # <REQ> Records with no Status table contribute to no velocity bar. >[SRS-075] </REQ>
    it 'leaves the chart datasets empty for a statusless record' do
      data = velocity_data_block(File.read(expand_path('myproject/build/decisions/overview.html')))
      totals = Array.new(data['labels'].length, 0)
      data['datasets'].each do |ds|
        ds['data'].each_with_index { |v, i| totals[i] += v }
      end
      expect(totals).to eq([0, 0, 0, 0, 0, 0])
    end
  end

  context 'when decision records use mixed status vocabularies' do
    before do
      write_file('myproject/project.yml', "specifications:\n  input: []\n")
      write_file('myproject/decisions/adr-603-impl.md', <<~MD)
        ---
        title: "ADR-603: Standard Workflow"
        ---

        # Status

        |  | Date | Status |
        |:---:|---|---|
        | * | 01-01-2024 | Implemented |
      MD
      write_file('myproject/decisions/issue-604-done.md', <<~MD)
        ---
        title: "ISSUE-604: Issue Workflow"
        ---

        # Status

        |  | Date | Status |
        |:---:|---|---|
        | * | 01-01-2024 | Done |
      MD
      write_file('myproject/decisions/enh-605-pending.md', <<~MD)
        ---
        title: "ENH-605: Enhancement Workflow"
        ---

        # Status

        |  | Date | Status |
        |:---:|---|---|
        | * | 01-01-2024 | Pending Review |
      MD
      run_command_and_stop('almirah please myproject')
    end

    # <REQ> Status segments = union of every distinct status text. >[SRS-076] </REQ>
    it 'creates a separate dataset for each distinct status text' do
      data = velocity_data_block(File.read(expand_path('myproject/build/decisions/overview.html')))
      labels = data['datasets'].map { |d| d['label'] }
      expect(labels).to include('Implemented', 'Done', 'Pending Review')
      data['datasets'].each do |ds|
        next unless %w[Implemented Done].include?(ds['label']) || ds['label'] == 'Pending Review'

        expect(ds['data']).to eq([1, 1, 1, 1, 1, 1])
      end
    end
  end

  context 'when a Status table has multiple rows on the same date' do
    before do
      write_file('myproject/project.yml', "specifications:\n  input: []\n")
      write_file('myproject/decisions/adr-606-same-date.md', <<~MD)
        ---
        title: "ADR-606: Same-Date Rows"
        ---

        # Status

        |  | Date | Status |
        |:---:|---|---|
        |   | 01-01-2024 | Proposed |
        |   | 01-01-2024 | Accepted |
        | * | 01-01-2024 | In-Progress |
      MD
      run_command_and_stop('almirah please myproject')
    end

    # <REQ> Same-date tie broken by document order; later row wins. >[SRS-073] </REQ>
    it 'picks the row that appears later in document order' do
      data = velocity_data_block(File.read(expand_path('myproject/build/decisions/overview.html')))
      in_progress = data['datasets'].find { |d| d['label'] == 'In-Progress' }
      expect(in_progress).not_to be_nil
      expect(in_progress['data']).to eq([1, 1, 1, 1, 1, 1])
      proposed = data['datasets'].find { |d| d['label'] == 'Proposed' }
      expect(proposed&.dig('data')).to be_nil.or(eq([0, 0, 0, 0, 0, 0]))
    end
  end

  context 'when a Status table has rows whose dates straddle the chart window' do
    before do
      write_file('myproject/project.yml', "specifications:\n  input: []\n")
      write_file('myproject/decisions/adr-607-mixed.md', <<~MD)
        ---
        title: "ADR-607: Mixed Past/Future Rows"
        ---

        # Status

        |  | Date | Status |
        |:---:|---|---|
        |   | 01-01-2024 | Proposed |
        | * | 02-01-2024 | Implemented |
        |   | 01-01-2030 | Archived |
      MD
      run_command_and_stop('almirah please myproject')
    end

    # <REQ> Future-dated rows are ignored for past Fridays. >[SRS-073] </REQ>
    it 'ignores future-dated rows on Fridays earlier than their date' do
      data = velocity_data_block(File.read(expand_path('myproject/build/decisions/overview.html')))
      implemented = data['datasets'].find { |d| d['label'] == 'Implemented' }
      expect(implemented).not_to be_nil
      expect(implemented['data']).to eq([1, 1, 1, 1, 1, 1])
      archived = data['datasets'].find { |d| d['label'] == 'Archived' }
      expect(archived&.dig('data')).to be_nil.or(eq([0, 0, 0, 0, 0, 0]))
    end
  end

  def status_data_block(html)
    block = html[/decisions_status_bar.*?data:\s*(\{.*?\}),\s*options:/m, 1]
    JSON.parse(block)
  end

  # Count for the category whose label starts with the given status text.
  def status_count(data, status)
    idx = data['labels'].index { |l| l.start_with?("#{status} (") }
    idx && data['datasets'][0]['data'][idx]
  end

  def write_status_record(id, title, status_rows)
    rows = status_rows.map { |marker, date, status| "| #{marker} | #{date} | #{status} |" }.join("\n")
    write_file("myproject/decisions/#{id}.md", <<~MD)
      ---
      title: "#{title}"
      ---

      # Status

      |  | Date | Status |
      |:---:|---|---|
      #{rows}
    MD
  end

  context 'when decision records have current statuses' do
    before do
      write_file('myproject/project.yml', "specifications:\n  input: []\n")
      write_status_record('adr-700-impl-a', 'ADR-700', [['*', '01-01-2025', 'Implemented']])
      write_status_record('adr-701-impl-b', 'ADR-701', [['*', '01-01-2025', 'Implemented']])
      write_status_record('adr-702-prop', 'ADR-702', [['*', '01-01-2025', 'Proposed']])
      write_status_record('adr-703-lower', 'ADR-703', [['*', '01-01-2025', 'proposed']])
      run_command_and_stop('almirah please myproject')
    end

    # <REQ> A horizontal bar chart of records by current status sits in the third chart cell. >[SRS-083] </REQ>
    it 'emits a horizontal bar chart after the velocity chart' do
      html = File.read(expand_path('myproject/build/decisions/overview.html'))
      expect(html).to include('id="decisions_status_bar"')
      expect(html).to match(/decisions_status_bar.*?type:\s*'bar'/m)
      expect(html).to match(/decisions_status_bar.*?indexAxis:\s*'y'/m)
      expect(html.index('decisions_status_bar')).to be > html.index('decisions_velocity_bar')
    end

    # <REQ> Records counted under their *-marked current status, matched case-sensitively. >[SRS-084] </REQ>
    it 'counts records under their current status and keeps case-distinct statuses separate' do
      data = status_data_block(File.read(expand_path('myproject/build/decisions/overview.html')))
      expect(status_count(data, 'Implemented')).to eq(2)
      expect(status_count(data, 'Proposed')).to eq(1)
      expect(status_count(data, 'proposed')).to eq(1)
    end

    # <REQ> Linear scale with the count shown in each bar label. >[SRS-086] </REQ>
    it 'uses a linear scale and shows the count in every label' do
      html = File.read(expand_path('myproject/build/decisions/overview.html'))
      expect(html).to match(/decisions_status_bar.*?beginAtZero:\s*true/m)
      expect(html).not_to match(/decisions_status_bar.*?type:\s*'logarithmic'/m)
      data = status_data_block(html)
      data['labels'].each { |l| expect(l).to match(/\(\d+\)\z/) }
    end

    # <REQ> No "Undefined" category when every record has a defined current status. >[SRS-085] </REQ>
    it 'omits the Undefined category when all records have a defined status' do
      data = status_data_block(File.read(expand_path('myproject/build/decisions/overview.html')))
      expect(data['labels']).to all(satisfy { |l| !l.start_with?('Undefined (') })
    end
  end

  context 'when decision records have missing or ambiguous current-status markers' do
    before do
      write_file('myproject/project.yml', "specifications:\n  input: []\n")
      write_status_record('adr-710-defined', 'ADR-710', [['*', '01-01-2025', 'Implemented']])
      # no marker at all -> current status undefined
      write_status_record('adr-711-no-marker', 'ADR-711', [[' ', '01-01-2025', 'Proposed']])
      # two markers -> current status ambiguous, also undefined
      write_status_record('adr-712-two-markers', 'ADR-712',
                          [['*', '01-01-2025', 'Proposed'], ['*', '02-01-2025', 'Accepted']])
      run_command_and_stop('almirah please myproject')
    end

    # <REQ> Records with a missing or ambiguous marker are grouped under "Undefined". >[SRS-085] </REQ>
    it 'groups both the markerless and the multi-marker record under Undefined' do
      data = status_data_block(File.read(expand_path('myproject/build/decisions/overview.html')))
      expect(status_count(data, 'Undefined')).to eq(2)
      expect(status_count(data, 'Implemented')).to eq(1)
    end

    # <REQ> The "Undefined" category is ordered last, after all real statuses. >[SRS-087] </REQ>
    it 'places the Undefined category last' do
      data = status_data_block(File.read(expand_path('myproject/build/decisions/overview.html')))
      undefined_idx = data['labels'].index { |l| l.start_with?('Undefined (') }
      expect(undefined_idx).to eq(data['labels'].length - 1)
    end
  end
end