commenter 0.2.3 → 0.3.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.
- checksums.yaml +4 -4
- data/.rubocop.yml +4 -0
- data/CLAUDE.md +58 -0
- data/README.adoc +83 -11
- data/commenter.gemspec +3 -2
- data/lib/commenter/cli.rb +21 -15
- data/lib/commenter/comment.rb +24 -1
- data/lib/commenter/comment_sheet.rb +10 -2
- data/lib/commenter/parser/osd_xlsx_parser.rb +252 -0
- data/lib/commenter/parser.rb +95 -37
- data/lib/commenter/version.rb +1 -1
- data/schema/iso_comment_2012-03.yaml +2 -2
- data/schema/iso_comment_osd.yaml +112 -0
- data/spec/commenter/cli_spec.rb +35 -0
- data/spec/commenter/osd_xlsx_parser_spec.rb +180 -0
- data/spec/support/osd_fixtures.rb +94 -0
- data/spec/support/xlsx_builder.rb +147 -0
- metadata +26 -5
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "roo"
|
|
4
|
+
require_relative "../comment_sheet"
|
|
5
|
+
require_relative "../comment"
|
|
6
|
+
|
|
7
|
+
module Commenter
|
|
8
|
+
class Parser
|
|
9
|
+
# Parser for ISO Online Standards Development (OSD) XLSX exports.
|
|
10
|
+
#
|
|
11
|
+
# Supports two variants, detected from the header row:
|
|
12
|
+
# - "resolved": single "Comments (N)" sheet with resolution data
|
|
13
|
+
# (columns: Comment ID, User name, Clause nb, ..., Resolution status,
|
|
14
|
+
# Motivation, Resolution Date, Stage code)
|
|
15
|
+
# - "unresolved": multi-sheet format with "Comments (N)",
|
|
16
|
+
# "Unresolved comments (N)", "Resolved comments (N)" sheets
|
|
17
|
+
# (columns: User name, Clause nb, ..., Comment type, Comment/Motivation,
|
|
18
|
+
# Comment on text, Proposal on Text, Proposed change, Replies,
|
|
19
|
+
# Resolution status, Justification, Resolution Date, Date, Comment number)
|
|
20
|
+
#
|
|
21
|
+
# Both variants share the same header structure in the first rows:
|
|
22
|
+
# Row 0: [Date, Reference, nil, "", Title EN, nil, nil, Title FR, ...]
|
|
23
|
+
# Row 1: ["2026-04-21", "ISO/DIS 5843-6(en)", ...]
|
|
24
|
+
# Row 2: empty
|
|
25
|
+
# Row 3: column headers
|
|
26
|
+
# Row 4+: data
|
|
27
|
+
class OsdXlsxParser
|
|
28
|
+
# Header names are mapped per variant onto common attribute keys so both
|
|
29
|
+
# variants share a single comment builder. Headers are mapped by real
|
|
30
|
+
# column position, tolerating gaps in the header row.
|
|
31
|
+
RESOLVED_COLUMN_KEYS = {
|
|
32
|
+
"Comment ID" => :id,
|
|
33
|
+
"User name" => :user_name,
|
|
34
|
+
"Clause nb" => :clause,
|
|
35
|
+
"Clause Title" => :clause_title,
|
|
36
|
+
"Subtype" => :comment_type,
|
|
37
|
+
"Comment" => :comment_text,
|
|
38
|
+
"Proposal on Text" => :proposal_on_text,
|
|
39
|
+
"Proposed change" => :proposed_change,
|
|
40
|
+
"Feedbacks" => :feedbacks,
|
|
41
|
+
"Created Date" => :created_date,
|
|
42
|
+
"Resolution status" => :resolution_status,
|
|
43
|
+
"Motivation" => :motivation,
|
|
44
|
+
"Resolution Date" => :resolution_date,
|
|
45
|
+
"Stage code" => :stage_code
|
|
46
|
+
}.freeze
|
|
47
|
+
|
|
48
|
+
UNRESOLVED_COLUMN_KEYS = {
|
|
49
|
+
"User name" => :user_name,
|
|
50
|
+
"Clause nb" => :clause,
|
|
51
|
+
"Clause Title" => :clause_title,
|
|
52
|
+
"Comment type" => :comment_type,
|
|
53
|
+
"Comment/Motivation" => :comment_text,
|
|
54
|
+
"Proposal on Text" => :proposal_on_text,
|
|
55
|
+
"Proposed change" => :proposed_change,
|
|
56
|
+
"Replies" => :feedbacks,
|
|
57
|
+
"Resolution status" => :resolution_status,
|
|
58
|
+
"Justification" => :motivation,
|
|
59
|
+
"Resolution Date" => :resolution_date,
|
|
60
|
+
"Date" => :created_date,
|
|
61
|
+
"Comment number" => :id
|
|
62
|
+
}.freeze
|
|
63
|
+
|
|
64
|
+
def parse(xlsx_path, options = {})
|
|
65
|
+
xlsx = Roo::Spreadsheet.open(xlsx_path)
|
|
66
|
+
|
|
67
|
+
sheet_name = select_sheet(xlsx, options)
|
|
68
|
+
sheet = xlsx.sheet(sheet_name)
|
|
69
|
+
header_row_num = find_header_row(sheet)
|
|
70
|
+
variant = detect_variant(sheet.row(header_row_num))
|
|
71
|
+
|
|
72
|
+
comments = parse_comments(sheet, header_row_num, variant, options)
|
|
73
|
+
CommentSheet.new(version: "osd", **extract_metadata(xlsx, sheet_name), comments: comments)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
def select_sheet(xlsx, options)
|
|
79
|
+
sheets = xlsx.sheets
|
|
80
|
+
|
|
81
|
+
if options[:sheet]
|
|
82
|
+
raise "Sheet '#{options[:sheet]}' not found. Available: #{sheets.join(", ")}" unless sheets.include?(options[:sheet])
|
|
83
|
+
|
|
84
|
+
return options[:sheet]
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
if options[:resolved_only]
|
|
88
|
+
sheets.find { |s| s.start_with?("Resolved") } || sheets.find { |s| s.start_with?("Comments") }
|
|
89
|
+
elsif options[:unresolved_only]
|
|
90
|
+
sheets.find { |s| s.start_with?("Unresolved") } || sheets.find { |s| s.start_with?("Comments") }
|
|
91
|
+
else
|
|
92
|
+
sheets.first # Default: first sheet (usually "Comments (N)" with all comments)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def find_header_row(sheet)
|
|
97
|
+
(1..10).each do |row_num|
|
|
98
|
+
headers = sheet.row(row_num).to_a.compact.map(&:to_s)
|
|
99
|
+
return row_num if headers.include?("User name") || headers.include?("Comment ID")
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
raise "Could not find header row in XLSX sheet"
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def detect_variant(header_row)
|
|
106
|
+
headers = header_row.to_a.compact.map(&:to_s)
|
|
107
|
+
if headers.include?("Comment ID") || headers.include?("Subtype")
|
|
108
|
+
:resolved
|
|
109
|
+
elsif headers.include?("Comment/Motivation") || headers.include?("Comment type")
|
|
110
|
+
:unresolved
|
|
111
|
+
else
|
|
112
|
+
headers.length >= 15 ? :resolved : :unresolved
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def extract_metadata(xlsx, sheet_name)
|
|
117
|
+
values = xlsx.sheet(sheet_name).row(2)
|
|
118
|
+
metadata = { date: nil, document: nil, project: nil, stage: nil, title_en: nil, title_fr: nil }
|
|
119
|
+
return metadata if values.nil?
|
|
120
|
+
|
|
121
|
+
metadata[:date] = header_value(values[0], "Date")
|
|
122
|
+
|
|
123
|
+
reference = header_value(values[1], "Reference")
|
|
124
|
+
if reference
|
|
125
|
+
metadata[:document] = reference
|
|
126
|
+
metadata[:stage] = extract_stage(reference)
|
|
127
|
+
metadata[:project] = extract_project(reference)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
metadata[:title_en] = header_value(values[4], "Title EN")
|
|
131
|
+
metadata[:title_fr] = header_value(values[7], "Title FR")
|
|
132
|
+
metadata
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def header_value(raw, label)
|
|
136
|
+
return nil if raw.nil?
|
|
137
|
+
|
|
138
|
+
value = raw.is_a?(Date) ? raw.strftime("%Y-%m-%d") : raw.to_s.strip
|
|
139
|
+
value.empty? || value == label ? nil : value
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def extract_stage(reference)
|
|
143
|
+
case reference
|
|
144
|
+
when %r{/WD\b}i then "WD"
|
|
145
|
+
when %r{/CD\b}i then "CD"
|
|
146
|
+
when %r{/DIS\b}i then "DIS"
|
|
147
|
+
when %r{/FDIS\b}i then "FDIS"
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def extract_project(reference)
|
|
152
|
+
# Extract ISO number from reference like "ISO/DIS 5843-6(en)"
|
|
153
|
+
match = reference.match(%r{(ISO[\s/]*\w*\s*\d+[\d-]*)})
|
|
154
|
+
match ? match[1].strip.gsub(/\s+/, " ") : nil
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def parse_comments(sheet, header_row_num, variant, options)
|
|
158
|
+
column_keys = variant == :resolved ? RESOLVED_COLUMN_KEYS : UNRESOLVED_COLUMN_KEYS
|
|
159
|
+
col_map = build_column_map(sheet.row(header_row_num), column_keys)
|
|
160
|
+
|
|
161
|
+
comments = []
|
|
162
|
+
(header_row_num + 1..sheet.last_row).each do |row_num|
|
|
163
|
+
row = sheet.row(row_num)
|
|
164
|
+
next if row.nil? || row.to_a.compact.empty?
|
|
165
|
+
|
|
166
|
+
comment = build_comment(row, col_map, options)
|
|
167
|
+
comments << comment if comment
|
|
168
|
+
end
|
|
169
|
+
comments
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def build_column_map(header_row, column_keys)
|
|
173
|
+
header_row.to_a.each_with_index.with_object({}) do |(header, index), map|
|
|
174
|
+
key = column_keys[header.to_s.strip]
|
|
175
|
+
map[key] = index if key
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def build_comment(row, col_map, options)
|
|
180
|
+
id = cell_value(row, col_map[:id])
|
|
181
|
+
return nil if id.nil?
|
|
182
|
+
|
|
183
|
+
id_str = id.is_a?(Float) ? id.to_i.to_s : id.to_s
|
|
184
|
+
comment_text = cell_value(row, col_map[:comment_text]) || ""
|
|
185
|
+
return nil if comment_text.strip.empty? && id_str.empty?
|
|
186
|
+
|
|
187
|
+
attrs = base_attributes(row, col_map, id_str, comment_text)
|
|
188
|
+
.merge(resolution_attributes(row, col_map))
|
|
189
|
+
attrs[:observations] = resolution_observations(row, col_map) unless options[:exclude_observations]
|
|
190
|
+
Comment.new(attrs)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def base_attributes(row, col_map, id_str, comment_text)
|
|
194
|
+
user_name = cell_value_str(row, col_map[:user_name]).to_s
|
|
195
|
+
clause = cell_value_str(row, col_map[:clause]).to_s
|
|
196
|
+
clause_title = cell_value_str(row, col_map[:clause_title]).to_s
|
|
197
|
+
{
|
|
198
|
+
id: id_str,
|
|
199
|
+
body: user_name,
|
|
200
|
+
locality: {
|
|
201
|
+
clause: clause.empty? ? nil : clause,
|
|
202
|
+
element: clause_title.empty? ? nil : clause_title
|
|
203
|
+
}.compact,
|
|
204
|
+
type: normalize_comment_type(cell_value_str(row, col_map[:comment_type]).to_s),
|
|
205
|
+
comments: comment_text.to_s.strip,
|
|
206
|
+
proposed_change: cell_value_str(row, col_map[:proposed_change]) ||
|
|
207
|
+
cell_value_str(row, col_map[:proposal_on_text])
|
|
208
|
+
}
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def resolution_attributes(row, col_map)
|
|
212
|
+
{
|
|
213
|
+
user_name: cell_value_str(row, col_map[:user_name]).to_s,
|
|
214
|
+
comment_type: cell_value_str(row, col_map[:comment_type]).to_s,
|
|
215
|
+
resolution_status: cell_value_str(row, col_map[:resolution_status]),
|
|
216
|
+
resolution_date: cell_value_str(row, col_map[:resolution_date]),
|
|
217
|
+
feedbacks: cell_value_str(row, col_map[:feedbacks]),
|
|
218
|
+
motivation: cell_value_str(row, col_map[:motivation]),
|
|
219
|
+
created_date: cell_value_str(row, col_map[:created_date]),
|
|
220
|
+
stage_code: cell_value_str(row, col_map[:stage_code])
|
|
221
|
+
}
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def resolution_observations(row, col_map)
|
|
225
|
+
parts = [cell_value_str(row, col_map[:resolution_status]),
|
|
226
|
+
cell_value_str(row, col_map[:motivation])].compact
|
|
227
|
+
parts.empty? ? nil : parts.join(". ")
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def normalize_comment_type(type_str)
|
|
231
|
+
case type_str.strip.downcase
|
|
232
|
+
when "editorial", "ed" then "ed"
|
|
233
|
+
when "technical", "te" then "te"
|
|
234
|
+
when "general", "ge" then "ge"
|
|
235
|
+
else type_str.strip
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def cell_value(row, index)
|
|
240
|
+
return nil if index.nil? || row.length <= index
|
|
241
|
+
|
|
242
|
+
value = row[index]
|
|
243
|
+
value.is_a?(String) && value.strip.empty? ? nil : value
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def cell_value_str(row, index)
|
|
247
|
+
value = cell_value(row, index)
|
|
248
|
+
value.nil? || value.to_s.strip.empty? ? nil : value.to_s.strip
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
end
|
data/lib/commenter/parser.rb
CHANGED
|
@@ -1,12 +1,44 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "docx"
|
|
4
|
+
require "pathname"
|
|
4
5
|
require_relative "comment_sheet"
|
|
5
6
|
require_relative "comment"
|
|
7
|
+
require_relative "parser/osd_xlsx_parser"
|
|
6
8
|
|
|
7
9
|
module Commenter
|
|
8
10
|
class Parser
|
|
9
|
-
def parse(
|
|
11
|
+
def parse(input_path, options = {})
|
|
12
|
+
format = detect_format(input_path, options)
|
|
13
|
+
|
|
14
|
+
case format
|
|
15
|
+
when :docx
|
|
16
|
+
parse_docx(input_path, options)
|
|
17
|
+
when :xlsx
|
|
18
|
+
parse_xlsx(input_path, options)
|
|
19
|
+
else
|
|
20
|
+
raise "Unsupported file format: #{input_path}. Supported formats: .docx, .xlsx"
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def detect_format(path, options = {})
|
|
27
|
+
return options[:format].to_sym if options[:format]
|
|
28
|
+
|
|
29
|
+
ext = File.extname(path).downcase
|
|
30
|
+
case ext
|
|
31
|
+
when ".docx" then :docx
|
|
32
|
+
when ".xlsx", ".xls" then :xlsx
|
|
33
|
+
else :unknown
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def parse_xlsx(xlsx_path, options)
|
|
38
|
+
OsdXlsxParser.new.parse(xlsx_path, options)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def parse_docx(docx_path, options)
|
|
10
42
|
doc = Docx::Document.open(docx_path)
|
|
11
43
|
|
|
12
44
|
# Extract metadata from the first table
|
|
@@ -17,41 +49,13 @@ module Commenter
|
|
|
17
49
|
raise "No comments table found in document" unless comments_table
|
|
18
50
|
raise "Comments table appears to be empty" if comments_table.row_count < 2
|
|
19
51
|
|
|
20
|
-
comments = []
|
|
21
|
-
|
|
22
52
|
# Process all rows - don't skip any rows, respect all content
|
|
23
|
-
(0..comments_table.row_count - 1).
|
|
24
|
-
|
|
25
|
-
cells = row.cells.map { |c| c.text.strip }
|
|
26
|
-
|
|
27
|
-
# Skip only completely empty rows
|
|
53
|
+
comments = (0..comments_table.row_count - 1).map do |row_index|
|
|
54
|
+
cells = comments_table.rows[row_index].cells.map { |cell| cell.text.strip }
|
|
28
55
|
next if cells.all?(&:empty?)
|
|
29
56
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
body = id.include?("-") ? id.split("-").first : id
|
|
33
|
-
|
|
34
|
-
# Create comment with symbol keys, respecting all input data
|
|
35
|
-
comment_attrs = {
|
|
36
|
-
id: id,
|
|
37
|
-
body: body,
|
|
38
|
-
locality: {
|
|
39
|
-
line_number: cells[1] && cells[1].empty? ? nil : cells[1],
|
|
40
|
-
clause: cells[2] && cells[2].empty? ? nil : cells[2],
|
|
41
|
-
element: cells[3] && cells[3].empty? ? nil : cells[3]
|
|
42
|
-
},
|
|
43
|
-
type: cells[4] || "",
|
|
44
|
-
comments: cells[5] || "",
|
|
45
|
-
proposed_change: cells[6] || ""
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
# Handle observations column
|
|
49
|
-
unless options[:exclude_observations]
|
|
50
|
-
comment_attrs[:observations] = cells[7] && cells[7].empty? ? nil : cells[7]
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
comments << Comment.new(comment_attrs)
|
|
54
|
-
end
|
|
57
|
+
osd_docx_row?(cells) ? build_osd_docx_comment(cells, options) : build_classic_docx_comment(cells, options)
|
|
58
|
+
end.compact
|
|
55
59
|
|
|
56
60
|
# Create comment sheet
|
|
57
61
|
CommentSheet.new(
|
|
@@ -63,7 +67,64 @@ module Commenter
|
|
|
63
67
|
)
|
|
64
68
|
end
|
|
65
69
|
|
|
66
|
-
|
|
70
|
+
# The DOCX from ISO OSD has 9 columns:
|
|
71
|
+
# 0: User name, 1: (empty/line), 2: Clause nb, 3: Clause Title,
|
|
72
|
+
# 4: Type, 5: Comment, 6: (empty/proposal), 7: Observations, 8: Comment number
|
|
73
|
+
# Detected by checking if last column looks like a numeric ID.
|
|
74
|
+
def osd_docx_row?(cells)
|
|
75
|
+
cells.length >= 9 && cells[8].to_s.match?(/^\d+$/)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def build_osd_docx_comment(cells, options)
|
|
79
|
+
attrs = {
|
|
80
|
+
id: cells[8],
|
|
81
|
+
body: cells[0].to_s.strip,
|
|
82
|
+
locality: {
|
|
83
|
+
clause: presence(cells[2]),
|
|
84
|
+
element: presence(cells[3])
|
|
85
|
+
},
|
|
86
|
+
type: normalize_type(cells[4]),
|
|
87
|
+
comments: cells[5] || "",
|
|
88
|
+
proposed_change: presence(cells[6]),
|
|
89
|
+
user_name: cells[0].to_s.strip
|
|
90
|
+
}
|
|
91
|
+
attrs[:observations] = presence(cells[7]) unless options[:exclude_observations]
|
|
92
|
+
Comment.new(attrs)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Classic ISO comment template format (8 columns):
|
|
96
|
+
# 0: ID, 1: line_number, 2: clause, 3: element, 4: type, 5: comments,
|
|
97
|
+
# 6: proposed_change, 7: observations
|
|
98
|
+
def build_classic_docx_comment(cells, options)
|
|
99
|
+
id = cells[0] || ""
|
|
100
|
+
attrs = {
|
|
101
|
+
id: id,
|
|
102
|
+
body: id.include?("-") ? id.split("-").first : id,
|
|
103
|
+
locality: {
|
|
104
|
+
line_number: presence(cells[1]),
|
|
105
|
+
clause: presence(cells[2]),
|
|
106
|
+
element: presence(cells[3])
|
|
107
|
+
},
|
|
108
|
+
type: cells[4] || "",
|
|
109
|
+
comments: cells[5] || "",
|
|
110
|
+
proposed_change: cells[6] || ""
|
|
111
|
+
}
|
|
112
|
+
attrs[:observations] = presence(cells[7]) unless options[:exclude_observations]
|
|
113
|
+
Comment.new(attrs)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def presence(value)
|
|
117
|
+
value && !value.empty? ? value : nil
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def normalize_type(type_str)
|
|
121
|
+
case type_str.to_s.strip.downcase
|
|
122
|
+
when "editorial" then "ed"
|
|
123
|
+
when "technical" then "te"
|
|
124
|
+
when "general" then "ge"
|
|
125
|
+
else type_str.to_s.strip
|
|
126
|
+
end
|
|
127
|
+
end
|
|
67
128
|
|
|
68
129
|
def extract_metadata(doc)
|
|
69
130
|
metadata = { date: nil, document: nil, project: nil }
|
|
@@ -96,9 +157,6 @@ module Commenter
|
|
|
96
157
|
project_match = all_text.match(/Project:\s*([^\n\r]+)/)
|
|
97
158
|
metadata[:project] = project_match[1]&.strip if project_match
|
|
98
159
|
|
|
99
|
-
# If no metadata found, try to extract from filename or other sources
|
|
100
|
-
# This is a fallback - in practice, users might need to provide metadata manually
|
|
101
|
-
|
|
102
160
|
metadata
|
|
103
161
|
end
|
|
104
162
|
end
|
data/lib/commenter/version.rb
CHANGED
|
@@ -68,8 +68,8 @@ properties:
|
|
|
68
68
|
required: ["clause"]
|
|
69
69
|
type:
|
|
70
70
|
type: string
|
|
71
|
-
enum: ["ge", "te", "ed"]
|
|
72
|
-
description: "Type of comment: ge=general, te=technical, ed=editorial"
|
|
71
|
+
enum: ["ge", "te", "ed", "general", "technical", "editorial"]
|
|
72
|
+
description: "Type of comment: ge=general, te=technical, ed=editorial. Codes are expanded to full names in output"
|
|
73
73
|
comments:
|
|
74
74
|
type: string
|
|
75
75
|
description: "The actual comment text"
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# ISO Comment Schema for ISO Online Standards Development (OSD) format
|
|
2
|
+
# Supports both resolved and unresolved XLSX exports from ISO OSD
|
|
3
|
+
$schema: http://json-schema.org/draft-07/schema#
|
|
4
|
+
title: ISO Comment OSD
|
|
5
|
+
description: Schema for ISO comments exported from ISO Online Standards Development (OSD) platform
|
|
6
|
+
type: object
|
|
7
|
+
properties:
|
|
8
|
+
version:
|
|
9
|
+
type: string
|
|
10
|
+
const: "osd"
|
|
11
|
+
description: Version identifier for OSD format
|
|
12
|
+
date:
|
|
13
|
+
type: ["string", "null"]
|
|
14
|
+
description: Date from the XLSX header
|
|
15
|
+
format: date
|
|
16
|
+
document:
|
|
17
|
+
type: ["string", "null"]
|
|
18
|
+
description: Document reference (e.g. ISO/DIS 5843-6(en))
|
|
19
|
+
project:
|
|
20
|
+
type: ["string", "null"]
|
|
21
|
+
description: Project name
|
|
22
|
+
title_en:
|
|
23
|
+
type: ["string", "null"]
|
|
24
|
+
description: Document title in English
|
|
25
|
+
title_fr:
|
|
26
|
+
type: ["string", "null"]
|
|
27
|
+
description: Document title in French
|
|
28
|
+
stage:
|
|
29
|
+
type: ["string", "null"]
|
|
30
|
+
description: Approval stage (WD/CD/DIS/FDIS/PRF/PUB)
|
|
31
|
+
enum: [null, "WD", "CD", "DIS", "FDIS", "PRF", "PUB"]
|
|
32
|
+
comments:
|
|
33
|
+
type: array
|
|
34
|
+
description: Array of comment entries from the ISO OSD export
|
|
35
|
+
items:
|
|
36
|
+
type: object
|
|
37
|
+
properties:
|
|
38
|
+
id:
|
|
39
|
+
type: ["string", "number"]
|
|
40
|
+
description: "Comment ID (numeric from OSD)"
|
|
41
|
+
body:
|
|
42
|
+
type: ["string", "null"]
|
|
43
|
+
description: "Member body or user name"
|
|
44
|
+
locality:
|
|
45
|
+
type: object
|
|
46
|
+
description: "Location information for the comment"
|
|
47
|
+
properties:
|
|
48
|
+
line_number:
|
|
49
|
+
type: ["string", "null"]
|
|
50
|
+
clause:
|
|
51
|
+
type: ["string", "null"]
|
|
52
|
+
element:
|
|
53
|
+
type: ["string", "null"]
|
|
54
|
+
description: "Clause Title from OSD"
|
|
55
|
+
type:
|
|
56
|
+
type: ["string", "null"]
|
|
57
|
+
description: "Type of comment; codes (ge/te/ed) are expanded to general/technical/editorial in output"
|
|
58
|
+
enum: [null, "ge", "te", "ed", "general", "technical", "editorial"]
|
|
59
|
+
comments:
|
|
60
|
+
type: ["string", "null"]
|
|
61
|
+
description: "The comment text"
|
|
62
|
+
proposed_change:
|
|
63
|
+
type: ["string", "null"]
|
|
64
|
+
description: "Proposed change text"
|
|
65
|
+
observations:
|
|
66
|
+
type: ["string", "null"]
|
|
67
|
+
description: "Resolution status or observations"
|
|
68
|
+
user_name:
|
|
69
|
+
type: ["string", "null"]
|
|
70
|
+
description: "User name from OSD"
|
|
71
|
+
comment_type:
|
|
72
|
+
type: ["string", "null"]
|
|
73
|
+
description: "Comment type/subtype from OSD (e.g. Editorial, General, Technical)"
|
|
74
|
+
resolution_status:
|
|
75
|
+
type: ["string", "null"]
|
|
76
|
+
description: "Resolution status (Accepted, Partially accepted, Rejected, etc.)"
|
|
77
|
+
resolution_date:
|
|
78
|
+
type: ["string", "null"]
|
|
79
|
+
description: "Date when the comment was resolved"
|
|
80
|
+
feedbacks:
|
|
81
|
+
type: ["string", "null"]
|
|
82
|
+
description: "Feedback/replies from the discussion"
|
|
83
|
+
motivation:
|
|
84
|
+
type: ["string", "null"]
|
|
85
|
+
description: "Motivation/justification for resolution"
|
|
86
|
+
created_date:
|
|
87
|
+
type: ["string", "null"]
|
|
88
|
+
description: "Date when the comment was created"
|
|
89
|
+
stage_code:
|
|
90
|
+
type: ["string", "null"]
|
|
91
|
+
description: "ISO stage code (e.g. 40.20)"
|
|
92
|
+
github:
|
|
93
|
+
type: ["object", "null"]
|
|
94
|
+
description: "GitHub integration information"
|
|
95
|
+
properties:
|
|
96
|
+
issue_number:
|
|
97
|
+
type: integer
|
|
98
|
+
issue_url:
|
|
99
|
+
type: string
|
|
100
|
+
format: uri
|
|
101
|
+
status:
|
|
102
|
+
type: string
|
|
103
|
+
enum: ["open", "closed"]
|
|
104
|
+
created_at:
|
|
105
|
+
type: string
|
|
106
|
+
format: date-time
|
|
107
|
+
updated_at:
|
|
108
|
+
type: string
|
|
109
|
+
format: date-time
|
|
110
|
+
required: ["issue_number", "issue_url", "status"]
|
|
111
|
+
required: ["id", "locality", "comments"]
|
|
112
|
+
required: ["version", "comments"]
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "commenter/cli"
|
|
5
|
+
require_relative "../support/osd_fixtures"
|
|
6
|
+
require_relative "../support/xlsx_builder"
|
|
7
|
+
|
|
8
|
+
RSpec.describe Commenter::Cli do
|
|
9
|
+
include OsdFixtures
|
|
10
|
+
|
|
11
|
+
describe "#import" do
|
|
12
|
+
it "writes OSD YAML and copies the OSD schema for XLSX input" do
|
|
13
|
+
Dir.mktmpdir do |dir|
|
|
14
|
+
xlsx = File.join(dir, "comments.xlsx")
|
|
15
|
+
rows = header_block(OsdFixtures::RESOLVED_HEADERS) + [
|
|
16
|
+
[1, "John Doe", "5.2.1", "Requirements", "Comment", "Editorial",
|
|
17
|
+
"The values in Table 3 are inconsistent.", nil, "Correct the values.", nil, nil, nil,
|
|
18
|
+
"2026-04-01", "Accepted", nil, "2026-04-10", "40.20"]
|
|
19
|
+
]
|
|
20
|
+
XlsxBuilder.write(xlsx, [["Comments (1)", rows]])
|
|
21
|
+
output = File.join(dir, "out", "comments.yaml")
|
|
22
|
+
schema_dir = File.join(dir, "out", "schema")
|
|
23
|
+
|
|
24
|
+
described_class.start(["import", xlsx, "--output", output, "--schema-dir", schema_dir])
|
|
25
|
+
|
|
26
|
+
data = YAML.safe_load_file(output)
|
|
27
|
+
expect(data["version"]).to eq("osd")
|
|
28
|
+
expect(data["comments"].length).to eq(1)
|
|
29
|
+
expect(data["comments"].first["id"]).to eq("1")
|
|
30
|
+
expect(File.read(output)).to start_with("# yaml-language-server: $schema=")
|
|
31
|
+
expect(File).to exist(File.join(schema_dir, "iso_comment_osd.yaml"))
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|