commenter 0.3.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -19,8 +19,7 @@ module Commenter
19
19
  end
20
20
 
21
21
  def create_issues_from_yaml(yaml_file, options = {})
22
- data = YAML.load_file(yaml_file)
23
- comment_sheet = CommentSheet.from_hash(data)
22
+ comment_sheet = CommentSheet.from_yaml(File.read(yaml_file))
24
23
 
25
24
  # Override stage if provided
26
25
  comment_sheet.stage = options[:stage] if options[:stage]
@@ -251,8 +250,12 @@ module Commenter
251
250
  puts "[GitHubIssueCreator] Using milestone number: #{milestone_config["number"]}"
252
251
  milestone_config["number"]
253
252
  elsif milestone_config["name"]
254
- puts "[GitHubIssueCreator] Using milestone name: #{milestone_config["name"]}"
255
- resolve_milestone_by_name_or_number(milestone_config["name"])
253
+ # Resolved once per run and memoized: resolving per comment caused
254
+ # duplicated milestone fetches (see 3a06c3d).
255
+ @milestone_number ||= begin
256
+ puts "[GitHubIssueCreator] Using milestone name: #{milestone_config["name"]}"
257
+ resolve_milestone_by_name_or_number(milestone_config["name"])
258
+ end
256
259
  end
257
260
  end
258
261
 
@@ -277,16 +280,14 @@ module Commenter
277
280
  def update_yaml_with_github_info(yaml_file, comment_sheet, results, options)
278
281
  # Update comments with GitHub information
279
282
  results.each do |result|
280
- next unless result[:status] == :created
283
+ next unless result[:issue_number]
281
284
 
282
285
  comment = comment_sheet.comments.find { |c| c.id == result[:comment_id] }
283
286
  next unless comment
284
287
 
285
- # Add GitHub information to the comment
286
- comment.github[:issue_number] = result[:issue_number]
287
- comment.github[:issue_url] = result[:issue_url]
288
- comment.github[:status] = "open"
289
- comment.github[:created_at] = Time.now.utc.iso8601
288
+ comment.record_github_issue(issue_number: result[:issue_number], issue_url: result[:issue_url],
289
+ status: result[:status] == :created ? "open" : comment.github_status,
290
+ created_at: comment.github_created_at || Time.now.utc.iso8601)
290
291
  end
291
292
 
292
293
  # Write updated YAML
@@ -304,8 +305,7 @@ module Commenter
304
305
  end
305
306
 
306
307
  def retrieve_observations_from_yaml(yaml_file, options = {})
307
- data = YAML.load_file(yaml_file)
308
- comment_sheet = CommentSheet.from_hash(data)
308
+ comment_sheet = CommentSheet.from_yaml(File.read(yaml_file))
309
309
 
310
310
  results = []
311
311
  comment_sheet.comments.each do |comment|
@@ -349,8 +349,8 @@ module Commenter
349
349
  if observation
350
350
  # Update comment with observation and current status
351
351
  comment.observations = observation
352
- comment.github[:status] = issue.state
353
- comment.github[:updated_at] = Time.now.utc.iso8601
352
+ comment.github.status = issue.state
353
+ comment.github.updated_at = Time.now.utc.iso8601
354
354
 
355
355
  {
356
356
  comment_id: comment.id,
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Commenter
4
+ # Synchronizes the comment YAML with GitHub issues, treating the YAML as
5
+ # the source of truth: comments without an issue get one, issue content is
6
+ # refreshed from the YAML (per the conflict policy), and issues whose
7
+ # comment has a recorded disposition can be closed.
8
+ class GitHubSync < GitHubIssueCreator
9
+ # Returns the sync actions for one comment by comparing the rendered
10
+ # issue content against the issue's current state. Pure — no network —
11
+ # so the conflict matrix is directly specifiable.
12
+ #
13
+ # conflict policy:
14
+ # yaml - re-render title/body over the issue (YAML wins)
15
+ # github - leave differing issue content untouched (GitHub wins)
16
+ # skip - take no action on differing content, report the conflict
17
+ def self.reconcile_actions(issue:, rendered:, disposition:, conflict: "yaml",
18
+ close_on_disposition: true)
19
+ actions = []
20
+ differing = [rendered[:title].to_s.strip, rendered[:body].to_s.strip] !=
21
+ [issue[:title].to_s.strip, issue[:body].to_s.strip]
22
+
23
+ if differing
24
+ case conflict
25
+ when "yaml" then actions << :update_content
26
+ when "github" then actions << :keep_github_content
27
+ when "skip" then actions << :conflict
28
+ end
29
+ end
30
+
31
+ actions << :close if close_on_disposition && issue[:state] == "open" && disposition && !disposition.strip.empty?
32
+
33
+ actions
34
+ end
35
+
36
+ def sync(yaml_file, options = {})
37
+ comment_sheet = CommentSheet.from_yaml(File.read(yaml_file))
38
+ comment_sheet.stage = options[:stage] if options[:stage]
39
+
40
+ results = comment_sheet.comments.map do |comment|
41
+ options[:dry_run] ? plan(comment, options) : reconcile(comment, comment_sheet, options)
42
+ end
43
+
44
+ update_yaml_with_github_info(yaml_file, comment_sheet, results, options) unless options[:dry_run]
45
+ results
46
+ end
47
+
48
+ private
49
+
50
+ # Dry-run preview, offline: comments with a known issue are planned from
51
+ # the last state recorded in the YAML, not from a live fetch.
52
+ def plan(comment, options)
53
+ return { comment_id: comment.id, actions: [:create] } unless comment.has_github_issue?
54
+
55
+ actions = [:reconcile]
56
+ if close_on_disposition?(options) && comment.github_status == "open" &&
57
+ comment.observations && !comment.observations.strip.empty?
58
+ actions << :close
59
+ end
60
+ { comment_id: comment.id, issue_number: comment.github_issue_number, actions: actions }
61
+ end
62
+
63
+ def reconcile(comment, comment_sheet, options)
64
+ issue = find_existing_issue(comment, comment_sheet)
65
+ return create_issue(comment, comment_sheet, options) unless issue
66
+
67
+ title = @title_template.render(template_variables(comment, comment_sheet))
68
+ body = @body_template.render(template_variables(comment, comment_sheet))
69
+ actions = self.class.reconcile_actions(
70
+ issue: { title: issue.title, body: issue.body, state: issue.state },
71
+ rendered: { title: title, body: body },
72
+ disposition: comment.observations,
73
+ conflict: options[:conflict] || "yaml",
74
+ close_on_disposition: close_on_disposition?(options)
75
+ )
76
+
77
+ apply(comment, issue, title, body, actions)
78
+ rescue Octokit::Error => e
79
+ { comment_id: comment.id, status: :error, message: e.message }
80
+ end
81
+
82
+ def apply(comment, issue, title, body, actions)
83
+ result = { comment_id: comment.id, issue_number: issue.number, status: :unchanged, actions: actions }
84
+ comment.record_github_issue(issue_number: issue.number, issue_url: issue.html_url, status: issue.state)
85
+
86
+ if actions.include?(:update_content)
87
+ @github_client.update_issue(@repo, issue.number, title: title, body: body)
88
+ result[:status] = :updated
89
+ end
90
+
91
+ if actions.include?(:close)
92
+ @github_client.close_issue(@repo, issue.number)
93
+ comment.github.status = "closed"
94
+ comment.github.updated_at = Time.now.utc.iso8601
95
+ result[:status] = actions.include?(:update_content) ? :updated_and_closed : :closed
96
+ end
97
+
98
+ result
99
+ end
100
+
101
+ def close_on_disposition?(options)
102
+ options.fetch(:close_on_disposition, true)
103
+ end
104
+ end
105
+ end
@@ -1,8 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "roo"
4
- require_relative "../comment_sheet"
5
- require_relative "../comment"
6
4
 
7
5
  module Commenter
8
6
  class Parser
@@ -201,7 +199,7 @@ module Commenter
201
199
  clause: clause.empty? ? nil : clause,
202
200
  element: clause_title.empty? ? nil : clause_title
203
201
  }.compact,
204
- type: CommentType.code(cell_value_str(row, col_map[:comment_type]).to_s),
202
+ type: CommentType.full_name(cell_value_str(row, col_map[:comment_type])),
205
203
  comments: comment_text.to_s.strip,
206
204
  proposed_change: cell_value_str(row, col_map[:proposed_change]) ||
207
205
  cell_value_str(row, col_map[:proposal_on_text])
@@ -2,8 +2,6 @@
2
2
 
3
3
  require "zip"
4
4
  require "nokogiri"
5
- require_relative "../comment"
6
- require_relative "../comment_sheet"
7
5
 
8
6
  module Commenter
9
7
  class Parser
@@ -54,7 +52,7 @@ module Commenter
54
52
  raise Commenter::Error, "word/document.xml not found in #{path}" unless entry
55
53
 
56
54
  remarks = read_remarks(zip)
57
- changes = stream_changes(entry.get_input_stream, anchors)
55
+ changes = entry.get_input_stream { |stream| stream_changes(stream, anchors) }
58
56
  end
59
57
 
60
58
  CommentSheet.new(
@@ -80,7 +78,7 @@ module Commenter
80
78
  entry = zip.glob("word/comments.xml").first
81
79
  return {} unless entry
82
80
 
83
- document = Nokogiri::XML(entry.get_input_stream)
81
+ document = entry.get_input_stream { |stream| Nokogiri::XML(stream) }
84
82
  document.xpath("//w:comment", "w" => W_NS).each_with_object({}) do |node, remarks|
85
83
  remarks[node["w:id"]] = {
86
84
  author: node["w:author"],
@@ -102,7 +100,7 @@ module Commenter
102
100
  id: format("%<body>s-%03<index>d", body: body, index: index + 1),
103
101
  body: body,
104
102
  locality: { clause: change[:clause], line_number: nil, element: change[:element] },
105
- type: options[:type] || "te",
103
+ type: CommentType.full_name(options[:type] || "te"),
106
104
  comments: "Track change (#{KIND_COMMENT.fetch(kind, kind)})",
107
105
  proposed_change: "#{KIND_LABEL.fetch(kind, kind)}: \"#{change[:text].strip}\"",
108
106
  observations: observations_for(options)
@@ -2,13 +2,12 @@
2
2
 
3
3
  require "docx"
4
4
  require "pathname"
5
- require_relative "comment_sheet"
6
- require_relative "comment"
7
- require_relative "parser/osd_xlsx_parser"
8
- require_relative "parser/track_change_docx_parser"
9
5
 
10
6
  module Commenter
11
7
  class Parser
8
+ autoload :OsdXlsxParser, "commenter/parser/osd_xlsx_parser"
9
+ autoload :TrackChangeDocxParser, "commenter/parser/track_change_docx_parser"
10
+
12
11
  def parse(input_path, options = {})
13
12
  format = detect_format(input_path, options)
14
13
 
@@ -90,7 +89,7 @@ module Commenter
90
89
  clause: presence(cells[2]),
91
90
  element: presence(cells[3])
92
91
  },
93
- type: CommentType.code(cells[4]),
92
+ type: CommentType.full_name(cells[4]),
94
93
  comments: cells[5] || "",
95
94
  proposed_change: presence(cells[6]),
96
95
  user_name: cells[0].to_s.strip
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Commenter
4
- VERSION = "0.3.1"
4
+ VERSION = "0.5.0"
5
5
  end
data/lib/commenter.rb CHANGED
@@ -1,11 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "commenter/version"
4
- require_relative "commenter/comment"
5
- require_relative "commenter/comment_sheet"
6
- require_relative "commenter/parser"
7
- require_relative "commenter/filler"
8
- require_relative "commenter/github_integration"
9
4
 
10
5
  # Commenter converts ISO comment sheets (DOCX/XLSX) to structured YAML and
11
6
  # syncs comments to GitHub issues.
@@ -13,5 +8,18 @@ module Commenter
13
8
  class Error < StandardError; end
14
9
 
15
10
  autoload :CommentType, "commenter/comment_type"
11
+ autoload :CommentLocality, "commenter/comment_locality"
12
+ autoload :GithubInfo, "commenter/github_info"
13
+ autoload :DispositionStatus, "commenter/disposition_status"
14
+ autoload :Ballot, "commenter/ballot"
15
+ autoload :BallotReport, "commenter/ballot_report"
16
+ autoload :BallotDiff, "commenter/ballot_diff"
17
+ autoload :Comment, "commenter/comment"
18
+ autoload :CommentSheet, "commenter/comment_sheet"
16
19
  autoload :GitHubSession, "commenter/github_session"
20
+ autoload :Parser, "commenter/parser"
21
+ autoload :Filler, "commenter/filler"
22
+ autoload :GitHubIssueCreator, "commenter/github_integration"
23
+ autoload :GitHubIssueRetriever, "commenter/github_integration"
24
+ autoload :GitHubSync, "commenter/github_sync"
17
25
  end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe Commenter::BallotDiff do
6
+ def sheet(comments)
7
+ Commenter::CommentSheet.new(comments: comments)
8
+ end
9
+
10
+ def comment(id, text: "Text for #{id}", observations: nil)
11
+ Commenter::Comment.new(id: id, body: id.split("-").first, comments: text, observations: observations)
12
+ end
13
+
14
+ let(:cd) do
15
+ sheet([
16
+ comment("DE-001"),
17
+ comment("DE-002", text: "Old wording"),
18
+ comment("US-001")
19
+ ])
20
+ end
21
+
22
+ let(:dis) do
23
+ sheet([
24
+ comment("DE-001"),
25
+ comment("DE-002", text: "Revised wording"),
26
+ comment("US-001", observations: "Accepted."),
27
+ comment("JP-001")
28
+ ])
29
+ end
30
+
31
+ it "classifies new, withdrawn, repeated, and revised comments" do
32
+ result = described_class.diff(cd, dis)
33
+
34
+ expect(result[:new]).to eq(["JP-001"])
35
+ expect(result[:withdrawn]).to eq([]) # US-001 present in both
36
+ expect(result[:repeated]).to eq(%w[DE-001 US-001])
37
+ expect(result[:revised]).to eq(["DE-002"])
38
+ end
39
+
40
+ it "counts comments with a disposition in the later stage as resolved" do
41
+ result = described_class.diff(cd, dis)
42
+
43
+ expect(result[:resolved]).to eq(["US-001"])
44
+ end
45
+
46
+ it "reports withdrawn comments" do
47
+ result = described_class.diff(dis, cd)
48
+
49
+ expect(result[:withdrawn]).to eq(["JP-001"])
50
+ expect(result[:new]).to eq([])
51
+ end
52
+
53
+ it "renders a markdown table" do
54
+ markdown = described_class.to_markdown(cd, dis)
55
+
56
+ expect(markdown).to start_with("| Category | Count | Comments |")
57
+ expect(markdown).to include("| New | 1 | JP-001 |")
58
+ expect(markdown).to include("| Revised | 1 | DE-002 |")
59
+ expect(markdown).to include("Resolved (disposition recorded in the later stage): 1")
60
+ end
61
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe Commenter::BallotReport do
6
+ def comment(body:, observations: nil, resolution_status: nil, github: {}, type: "technical")
7
+ Commenter::Comment.new(
8
+ id: "#{body}-1", body: body, type: type,
9
+ comments: "A comment", observations: observations,
10
+ resolution_status: resolution_status, github: github
11
+ )
12
+ end
13
+
14
+ let(:sheet) do
15
+ Commenter::CommentSheet.new(comments: [
16
+ comment(body: "DE", observations: "Accepted."),
17
+ comment(body: "DE", observations: "Accept with modifications"),
18
+ comment(body: "US", observations: "Noted"),
19
+ comment(body: "US", observations: "Rejected"),
20
+ comment(body: "US"),
21
+ comment(body: "CS", resolution_status: "Not accepted"),
22
+ comment(body: "CS", github: { status: "open" })
23
+ ])
24
+ end
25
+
26
+ describe ".status_for" do
27
+ it "prefers observation text, then resolution status, then GitHub state" do
28
+ comments = sheet.comments
29
+ expect(described_class.status_for(comments[0])).to eq(:accepted)
30
+ expect(described_class.status_for(comments[4])).to eq(:undecided)
31
+ expect(described_class.status_for(comments[5])).to eq(:rejected)
32
+ expect(described_class.status_for(comments[6])).to eq(:open)
33
+ end
34
+ end
35
+
36
+ describe ".counts" do
37
+ it "counts per member body, disposition, and type" do
38
+ counts = described_class.counts(sheet)
39
+
40
+ expect(counts[:total]).to eq(7)
41
+ expect(counts[:bodies]["DE"][:total]).to eq(2)
42
+ expect(counts[:bodies]["DE"][:accepted]).to eq(1)
43
+ expect(counts[:bodies]["DE"][:accept_with_modifications]).to eq(1)
44
+ expect(counts[:bodies]["US"][:noted]).to eq(1)
45
+ expect(counts[:bodies]["US"][:rejected]).to eq(1)
46
+ expect(counts[:bodies]["US"][:undecided]).to eq(1)
47
+ expect(counts[:bodies]["CS"][:open]).to eq(1)
48
+ expect(counts[:types]["technical"]).to eq(7)
49
+ end
50
+ end
51
+
52
+ describe ".to_markdown" do
53
+ it "renders a ballot report table with totals" do
54
+ markdown = described_class.to_markdown(sheet)
55
+
56
+ expect(markdown).to start_with("| Body | Total | Accepted | AWM | Noted | Rejected | TODO | Open | Undecided |")
57
+ expect(markdown).to include("| DE | 2 | 1 | 1 | 0 | 0 | 0 | 0 | 0 |")
58
+ expect(markdown).to include("| **Total** | **7** | **1** | **1** | **1** | **2** | **0** | **1** | **1** |")
59
+ expect(markdown).to include("Comment types: technical 7")
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe Commenter::Ballot do
6
+ def sheet(body:, ids:, document: nil, stage: nil)
7
+ Commenter::CommentSheet.new(
8
+ version: "2012-03",
9
+ document: document,
10
+ stage: stage,
11
+ comments: ids.map do |id|
12
+ Commenter::Comment.new(id: id, body: body, comments: "Comment #{id} from #{body}")
13
+ end
14
+ )
15
+ end
16
+
17
+ it "combines comments from all sheets in input order" do
18
+ merged = described_class.new([
19
+ sheet(body: "DE", ids: %w[DE-001 DE-002]),
20
+ sheet(body: "US", ids: %w[US-001])
21
+ ]).merge
22
+
23
+ expect(merged.comments.map(&:id)).to eq(%w[DE-001 DE-002 US-001])
24
+ expect(merged.comments.map(&:body)).to eq(%w[DE DE US])
25
+ end
26
+
27
+ it "takes metadata from the first sheet that provides it" do
28
+ merged = described_class.new([
29
+ sheet(body: "DE", ids: %w[DE-001], stage: nil),
30
+ sheet(body: "US", ids: %w[US-001], document: "ISO 2533:2026", stage: "DIS")
31
+ ]).merge
32
+
33
+ expect(merged.version).to eq("2012-03")
34
+ expect(merged.document).to eq("ISO 2533:2026")
35
+ expect(merged.stage).to eq("DIS")
36
+ end
37
+
38
+ it "silently deduplicates identical comments sharing an ID" do
39
+ duplicate = Commenter::Comment.new(id: "DE-001", body: "DE", comments: "Same text", proposed_change: "Same change")
40
+
41
+ merged = described_class.new([
42
+ Commenter::CommentSheet.new(comments: [duplicate]),
43
+ Commenter::CommentSheet.new(comments: [
44
+ Commenter::Comment.new(id: "DE-001", body: "DE", comments: "Same text",
45
+ proposed_change: "Same change")
46
+ ])
47
+ ]).merge
48
+
49
+ expect(merged.comments.map(&:id)).to eq(["DE-001"])
50
+ end
51
+
52
+ it "raises when the same ID carries differing content" do
53
+ conflicting = Commenter::Comment.new(id: "DE-001", body: "DE", comments: "Different text entirely")
54
+
55
+ expect do
56
+ described_class.new([
57
+ sheet(body: "DE", ids: %w[DE-001]),
58
+ Commenter::CommentSheet.new(comments: [conflicting])
59
+ ]).merge
60
+ end.to raise_error(Commenter::Error, /DE-001/)
61
+ end
62
+
63
+ it "returns an empty sheet when no input sheets are given" do
64
+ merged = described_class.new([]).merge
65
+
66
+ expect(merged.comments).to eq([])
67
+ end
68
+ end
@@ -32,4 +32,45 @@ RSpec.describe Commenter::Cli do
32
32
  end
33
33
  end
34
34
  end
35
+
36
+ describe "#merge" do
37
+ it "writes a combined sheet with schema reference" do
38
+ Dir.mktmpdir do |dir|
39
+ one = File.join(dir, "de.yaml")
40
+ two = File.join(dir, "us.yaml")
41
+ File.write(one, { "version" => "2012-03", "comments" => [
42
+ { "id" => "DE-001", "body" => "DE", "comments" => "First" }
43
+ ] }.to_yaml)
44
+ File.write(two, { "comments" => [
45
+ { "id" => "US-001", "body" => "US", "comments" => "Second" }
46
+ ] }.to_yaml)
47
+ output = File.join(dir, "out", "merged.yaml")
48
+ schema_dir = File.join(dir, "out", "schema")
49
+
50
+ described_class.start(["merge", one, two, "--output", output, "--schema-dir", schema_dir])
51
+
52
+ data = YAML.safe_load_file(output)
53
+ expect(data["comments"].map { |comment| comment["id"] }).to eq(%w[DE-001 US-001])
54
+ expect(File).to exist(File.join(schema_dir, "iso_comment_2012-03.yaml"))
55
+ end
56
+ end
57
+ end
58
+
59
+ describe "#stats" do
60
+ it "prints a markdown disposition report" do
61
+ Dir.mktmpdir do |dir|
62
+ input = File.join(dir, "comments.yaml")
63
+ File.write(input, { "comments" => [
64
+ { "id" => "DE-001", "body" => "DE", "comments" => "A", "observations" => "Accepted." },
65
+ { "id" => "US-001", "body" => "US", "comments" => "B", "observations" => "Noted" }
66
+ ] }.to_yaml)
67
+
68
+ expect { described_class.start(["stats", input]) }
69
+ .to output(a_string_including(
70
+ "| DE | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 |",
71
+ "| US | 1 | 0 | 0 | 1 | 0 | 0 | 0 | 0 |"
72
+ )).to_stdout
73
+ end
74
+ end
75
+ end
35
76
  end
@@ -115,21 +115,21 @@ RSpec.describe Commenter::CommentSheet do
115
115
 
116
116
  hash = sheet.to_h
117
117
 
118
- expect(hash[:version]).to eq("2012-03")
119
- expect(hash[:date]).to eq("2024-06-04")
120
- expect(hash[:document]).to eq("Test Document")
121
- expect(hash[:project]).to eq("Test Project")
122
- expect(hash[:stage]).to eq("DIS")
123
- expect(hash[:comments]).to be_an(Array)
124
- expect(hash[:comments].first).to be_a(Hash)
118
+ expect(hash["version"]).to eq("2012-03")
119
+ expect(hash["date"]).to eq("2024-06-04")
120
+ expect(hash["document"]).to eq("Test Document")
121
+ expect(hash["project"]).to eq("Test Project")
122
+ expect(hash["stage"]).to eq("DIS")
123
+ expect(hash["comments"]).to be_an(Array)
124
+ expect(hash["comments"].first).to be_a(Hash)
125
125
  end
126
126
 
127
- it "includes nil stage when not set" do
127
+ it "omits unset attributes from the hash" do
128
128
  sheet = described_class.new({})
129
129
  hash = sheet.to_h
130
130
 
131
- expect(hash).to have_key(:stage)
132
- expect(hash[:stage]).to be_nil
131
+ expect(hash).not_to have_key("stage")
132
+ expect(sheet.version).to eq("2012-03")
133
133
  end
134
134
  end
135
135
 
@@ -9,7 +9,7 @@ RSpec.describe Commenter::Comment do
9
9
  id: "US-001",
10
10
  body: "US",
11
11
  locality: { clause: "5.1", element: "Table 1", line_number: "42" },
12
- type: "te",
12
+ type: "technical",
13
13
  comments: "Test comment",
14
14
  proposed_change: "Test change",
15
15
  observations: "Test observations"
@@ -28,6 +28,19 @@ RSpec.describe Commenter::Comment do
28
28
  expect(comment.observations).to eq("Test observations")
29
29
  end
30
30
 
31
+ it "expands short type codes when loaded from YAML" do
32
+ yaml = <<~YAML
33
+ ---
34
+ id: US-009
35
+ type: te
36
+ YAML
37
+
38
+ comment = described_class.from_yaml(yaml)
39
+
40
+ expect(comment.id).to eq("US-009")
41
+ expect(comment.type).to eq("technical")
42
+ end
43
+
31
44
  it "handles string keys in attributes" do
32
45
  attributes = {
33
46
  "id" => "US-001",
@@ -162,7 +175,7 @@ RSpec.describe Commenter::Comment do
162
175
  id: "US-001",
163
176
  body: "US",
164
177
  locality: { clause: "5.1" },
165
- type: "te",
178
+ type: "technical",
166
179
  comments: "Test",
167
180
  proposed_change: "Change",
168
181
  observations: "Obs"
@@ -170,13 +183,13 @@ RSpec.describe Commenter::Comment do
170
183
 
171
184
  hash = comment.to_h
172
185
 
173
- expect(hash[:id]).to eq("US-001")
174
- expect(hash[:body]).to eq("US")
175
- expect(hash[:locality][:clause]).to eq("5.1")
176
- expect(hash[:type]).to eq("technical")
177
- expect(hash[:comments]).to eq("Test")
178
- expect(hash[:proposed_change]).to eq("Change")
179
- expect(hash[:observations]).to eq("Obs")
186
+ expect(hash["id"]).to eq("US-001")
187
+ expect(hash["body"]).to eq("US")
188
+ expect(hash["locality"]["clause"]).to eq("5.1")
189
+ expect(hash["type"]).to eq("technical")
190
+ expect(hash["comments"]).to eq("Test")
191
+ expect(hash["proposed_change"]).to eq("Change")
192
+ expect(hash["observations"]).to eq("Obs")
180
193
  end
181
194
  end
182
195
 
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe Commenter::DispositionStatus do
6
+ describe ".match" do
7
+ it "classifies accepted dispositions" do
8
+ expect(described_class.match("Accepted")).to eq(:accepted)
9
+ expect(described_class.match("accepted.")).to eq(:accepted)
10
+ expect(described_class.match("Partially accepted")).to eq(:accepted)
11
+ end
12
+
13
+ it "classifies accept-with-modifications before the bare accept pattern" do
14
+ expect(described_class.match("Accept with modifications")).to eq(:accept_with_modifications)
15
+ expect(described_class.match("Accepted with modifications")).to eq(:accept_with_modifications)
16
+ expect(described_class.match("Accepted, with changes")).to eq(:accept_with_modifications)
17
+ expect(described_class.match("AWM")).to eq(:accept_with_modifications)
18
+ end
19
+
20
+ it "classifies rejection including the 'not accepted' phrasing" do
21
+ expect(described_class.match("Rejected")).to eq(:rejected)
22
+ expect(described_class.match("Not accepted")).to eq(:rejected)
23
+ end
24
+
25
+ it "classifies noted, todo, and non-matching text" do
26
+ expect(described_class.match("Noted")).to eq(:noted)
27
+ expect(described_class.match("TODO: review")).to eq(:todo)
28
+ expect(described_class.match("Deferred to next revision")).to be_nil
29
+ expect(described_class.match("")).to be_nil
30
+ expect(described_class.match(nil)).to be_nil
31
+ end
32
+ end
33
+
34
+ describe ".statuses" do
35
+ it "lists the canonical statuses" do
36
+ expect(described_class.statuses)
37
+ .to eq(%i[accept_with_modifications rejected accepted noted todo])
38
+ end
39
+ end
40
+ end