commenter 0.3.1 → 0.4.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/CONTEXT.md +27 -0
- data/README.adoc +57 -11
- data/commenter.gemspec +1 -0
- data/lib/commenter/ballot.rb +67 -0
- data/lib/commenter/ballot_report.rb +60 -0
- data/lib/commenter/cli.rb +58 -31
- data/lib/commenter/comment.rb +100 -115
- data/lib/commenter/comment_locality.rb +18 -0
- data/lib/commenter/comment_sheet.rb +28 -68
- data/lib/commenter/comment_type.rb +5 -1
- data/lib/commenter/disposition_status.rb +37 -0
- data/lib/commenter/filler.rb +91 -59
- data/lib/commenter/github_info.rb +23 -0
- data/lib/commenter/github_integration.rb +6 -11
- data/lib/commenter/parser/osd_xlsx_parser.rb +1 -3
- data/lib/commenter/parser/track_change_docx_parser.rb +3 -5
- data/lib/commenter/parser.rb +4 -5
- data/lib/commenter/version.rb +1 -1
- data/lib/commenter.rb +11 -5
- data/spec/commenter/ballot_report_spec.rb +62 -0
- data/spec/commenter/ballot_spec.rb +68 -0
- data/spec/commenter/cli_spec.rb +41 -0
- data/spec/commenter/comment_sheet_spec.rb +10 -10
- data/spec/commenter/comment_spec.rb +22 -9
- data/spec/commenter/disposition_status_spec.rb +40 -0
- data/spec/commenter/filler_spec.rb +119 -0
- data/spec/commenter/osd_xlsx_parser_spec.rb +3 -2
- data/spec/support/redline_docx_builder.rb +11 -4
- data/spec/support/xlsx_builder.rb +17 -12
- metadata +26 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 90d4268c6a071ae365af24ef05a5c19dcdfd6fe03545c05a84767f91f70dc411
|
|
4
|
+
data.tar.gz: b44468b6f9cc18e0726c1df1c1433247e8fc19f6ff9fafc4c3f0eff341a21785
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 00d4838eadd5c0751199832ff6401a753b2bc4c27cd94d02f6c0d2bab6392a629c216c6cc350766c0d35920f871b4e2bd74102967460e370ebc15fc005a17e9c
|
|
7
|
+
data.tar.gz: '09420029845d4e6b776eeb40b726326486a409dd8f0ec33be792dcc193e30f5d5774e99777cd64e85c31a5137a69d2648eac4ab06bde2a5ea93a7d2569d0e90a'
|
data/CONTEXT.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# CONTEXT.md — Domain Glossary
|
|
2
|
+
|
|
3
|
+
The domain language of the commenter gem. Used to name modules and commands;
|
|
4
|
+
keep this current when concepts sharpen.
|
|
5
|
+
|
|
6
|
+
- **Comment Sheet** — one ballot submission as a whole: metadata (document,
|
|
7
|
+
stage, date, project, titles) plus its comments. `CommentSheet`.
|
|
8
|
+
- **Comment** — a single remark on a document: identity (id, member body),
|
|
9
|
+
locality (clause, element, line), type, the remark text, the proposed
|
|
10
|
+
change, and the disposition (`observations`).
|
|
11
|
+
- **Member Body** — the national body or committee that submitted the comment
|
|
12
|
+
(`DE`, `US`, `CS` for the ISO/CS secretariat, `**` for ISO itself). A
|
|
13
|
+
secretariat receives one sheet per member body per ballot.
|
|
14
|
+
- **Ballot** — the set of all comment sheets for one ballot round. Merging
|
|
15
|
+
member sheets into one is collation; `Ballot`.
|
|
16
|
+
- **Disposition** — the resolution of a comment: accepted, accept with
|
|
17
|
+
modifications, noted, rejected, or still open. `DispositionStatus` is the
|
|
18
|
+
single matcher for both free-text observations and OSD's structured
|
|
19
|
+
`resolution_status`.
|
|
20
|
+
- **Stage** — the ballot stage of the document (WD, CD, DIS, FDIS, PRF, PUB).
|
|
21
|
+
The same comment ID at different stages is a different comment.
|
|
22
|
+
- **Redline** — a DOCX with tracked changes (`w:ins`/`w:del`/moves), typically
|
|
23
|
+
from ISO/CS editors; also carries reviewer remark threads.
|
|
24
|
+
- **OSD** — ISO Online Standards Development; its XLSX comment exports come
|
|
25
|
+
in resolved and unresolved variants.
|
|
26
|
+
- **Unique ID** — stage-aware identity of a comment's GitHub issue
|
|
27
|
+
(`[DIS] GB-001` by default), used for duplicate detection.
|
data/README.adoc
CHANGED
|
@@ -124,12 +124,16 @@ Options:
|
|
|
124
124
|
==== Metadata (extraction limitation)
|
|
125
125
|
|
|
126
126
|
WARNING: Due to a limitation in the underlying `docx` gem, metadata fields
|
|
127
|
-
(Date, Document, Project)
|
|
128
|
-
|
|
129
|
-
|
|
127
|
+
(Date, Document, Project) cannot yet be automatically extracted from the DOCX
|
|
128
|
+
header during import (see https://github.com/ruby-docx/docx/pull/73[docx gem
|
|
129
|
+
PR #73]). Redline imports take them from the `--document` / `--stage` options;
|
|
130
|
+
for template imports, populate the YAML fields manually.
|
|
130
131
|
|
|
131
|
-
The
|
|
132
|
-
|
|
132
|
+
The reverse direction works: `commenter fill` writes the sheet's
|
|
133
|
+
`date`, `document`, and `project` values into the Date/Document/Project labels
|
|
134
|
+
of the template's page header, so the filled comment sheet is complete.
|
|
135
|
+
|
|
136
|
+
For template imports, populate the YAML metadata manually:
|
|
133
137
|
|
|
134
138
|
[source,yaml]
|
|
135
139
|
----
|
|
@@ -142,9 +146,6 @@ project: "" # Manually add: e.g., "Project name"
|
|
|
142
146
|
comments:
|
|
143
147
|
----
|
|
144
148
|
|
|
145
|
-
Alternatively, you can provide metadata during import using CLI options (planned
|
|
146
|
-
for future release).
|
|
147
|
-
|
|
148
149
|
|
|
149
150
|
==== Example YAML output
|
|
150
151
|
|
|
@@ -189,11 +190,56 @@ comments:
|
|
|
189
190
|
observations:
|
|
190
191
|
----
|
|
191
192
|
|
|
193
|
+
=== Merging comment sheets (ballot collation)
|
|
194
|
+
|
|
195
|
+
A secretariat receives one comment file per member body per ballot — mixed
|
|
196
|
+
DOCX templates, OSD XLSX exports, and late redlines that arrive by email.
|
|
197
|
+
`commenter merge` collates them into a single ballot sheet:
|
|
198
|
+
|
|
199
|
+
[source,shell]
|
|
200
|
+
----
|
|
201
|
+
commenter merge de.yaml us.yaml jp.yaml cs-redline.yaml -o ballot.yaml
|
|
202
|
+
----
|
|
203
|
+
|
|
204
|
+
Metadata (document, stage, date) is taken from the first sheet that provides
|
|
205
|
+
each field. Comments keep their input order. Identical duplicates of the same
|
|
206
|
+
comment ID are deduplicated silently; the same ID with differing content
|
|
207
|
+
raises an error listing the conflicting IDs — collation stops loudly rather
|
|
208
|
+
than silently dropping a member's comment.
|
|
209
|
+
|
|
210
|
+
=== Ballot statistics (disposition reports)
|
|
211
|
+
|
|
212
|
+
Every ballot report needs disposition counts — by member body and overall.
|
|
213
|
+
The data is already in the YAML, so `commenter stats` produces the table:
|
|
214
|
+
|
|
215
|
+
[source,shell]
|
|
216
|
+
----
|
|
217
|
+
commenter stats ballot.yaml
|
|
218
|
+
----
|
|
219
|
+
|
|
220
|
+
[source,markdown]
|
|
221
|
+
....
|
|
222
|
+
| Body | Total | Accepted | AWM | Noted | Rejected | TODO | Open | Undecided |
|
|
223
|
+
|------|-------|----------|-----|-------|----------|------|------|-----------|
|
|
224
|
+
| DE | 12 | 5 | 3 | 2 | 1 | 0 | 0 | 1 |
|
|
225
|
+
| US | 9 | 4 | 1 | 2 | 1 | 0 | 1 | 0 |
|
|
226
|
+
| **Total** | **21** | **9** | **4** | **4** | **2** | **0** | **1** | **1** |
|
|
227
|
+
|
|
228
|
+
Comment types: technical 13 · general 5 · editorial 3
|
|
229
|
+
....
|
|
230
|
+
|
|
231
|
+
A comment's disposition is read from its `observations` text, falling back to
|
|
232
|
+
the OSD `resolution_status` field, then to the GitHub issue state (a still-open
|
|
233
|
+
issue counts as *open*; no disposition at all counts as *undecided*). Use
|
|
234
|
+
`--format yaml` for machine-readable output, and `-o report.md` to write to a
|
|
235
|
+
file — the Markdown pastes directly into a GitHub issue or WG minutes.
|
|
236
|
+
|
|
192
237
|
=== Filling DOCX templates from YAML
|
|
193
238
|
|
|
194
239
|
This gem contains a command-line utility to fill a DOCX template with comments
|
|
195
240
|
from a YAML file. It generates a filled comment sheet that can be used for
|
|
196
|
-
review and resolution tracking.
|
|
241
|
+
review and resolution tracking. The sheet's `date`, `document`, and `project`
|
|
242
|
+
metadata are written into the template's page header.
|
|
197
243
|
|
|
198
244
|
The base template is the ISO comment sheet template located at
|
|
199
245
|
`data/iso_comment_template_2012-03.docx`. You can also provide a custom
|
|
@@ -605,10 +651,10 @@ recognized and applied to the observations column:
|
|
|
605
651
|
|===
|
|
606
652
|
| Status Pattern | Intended Color | Hex Code | Example
|
|
607
653
|
|
|
654
|
+
| `awm\|accept(ed)? with (modifications\|changes)` | Olive Green | #C4D79B | "Accepted with modifications"
|
|
608
655
|
| `accept(ed)?` | Green | #92D050 | "Accepted"
|
|
609
|
-
| `awm\|accept with modifications` | Olive Green | #C4D79B | "Accept with modifications"
|
|
610
656
|
| `noted` | Blue | #8DB4E2 | "Noted"
|
|
611
|
-
| `reject(ed)
|
|
657
|
+
| `reject(ed)?\|not accepted` | Pink | #FF99CC | "Rejected"
|
|
612
658
|
| `todo` | Diagonal stripes | #D9D9D9 | "TODO: Review"
|
|
613
659
|
|
|
614
660
|
|===
|
data/commenter.gemspec
CHANGED
|
@@ -34,6 +34,7 @@ Gem::Specification.new do |spec|
|
|
|
34
34
|
spec.add_dependency "docx", "~> 0.8"
|
|
35
35
|
spec.add_dependency "dotenv", "~> 2.8"
|
|
36
36
|
spec.add_dependency "liquid", "~> 5.0"
|
|
37
|
+
spec.add_dependency "lutaml-model", "~> 0.8"
|
|
37
38
|
spec.add_dependency "octokit", "~> 6.0"
|
|
38
39
|
spec.add_dependency "roo", ">= 2.10"
|
|
39
40
|
spec.add_dependency "thor", "~> 1.0"
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Commenter
|
|
4
|
+
# Merges member-body comment sheets into a single ballot sheet — the
|
|
5
|
+
# collation step secretariats otherwise do by hand (ISO's own Comment
|
|
6
|
+
# Collation Tool fails on non-conforming files).
|
|
7
|
+
#
|
|
8
|
+
# Policy: sheet metadata is first-non-nil across the given sheets (the
|
|
9
|
+
# output version comes from the first sheet); comments keep their input
|
|
10
|
+
# order. Identical duplicate IDs are deduplicated silently; the same ID
|
|
11
|
+
# with differing content raises.
|
|
12
|
+
class Ballot
|
|
13
|
+
def initialize(sheets)
|
|
14
|
+
@sheets = sheets
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def merge
|
|
18
|
+
CommentSheet.new(
|
|
19
|
+
version: first_sheet&.version || "2012-03",
|
|
20
|
+
date: first_value(:date),
|
|
21
|
+
document: first_value(:document),
|
|
22
|
+
project: first_value(:project),
|
|
23
|
+
stage: first_value(:stage),
|
|
24
|
+
title_en: first_value(:title_en),
|
|
25
|
+
title_fr: first_value(:title_fr),
|
|
26
|
+
comments: deduplicate(@sheets.flat_map(&:comments))
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def first_sheet
|
|
33
|
+
@sheets.first
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def first_value(attribute)
|
|
37
|
+
@sheets.lazy.map { |sheet| sheet.public_send(attribute) }.find { |value| !value.nil? }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def deduplicate(comments)
|
|
41
|
+
seen = {}
|
|
42
|
+
conflicts = []
|
|
43
|
+
kept = comments.reject do |comment|
|
|
44
|
+
key = comment.id.to_s
|
|
45
|
+
next false if key.empty?
|
|
46
|
+
|
|
47
|
+
existing = seen[key]
|
|
48
|
+
if existing.nil?
|
|
49
|
+
seen[key] = comment
|
|
50
|
+
false
|
|
51
|
+
elsif identical?(existing, comment)
|
|
52
|
+
true
|
|
53
|
+
else
|
|
54
|
+
conflicts << key
|
|
55
|
+
false
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
return kept if conflicts.empty?
|
|
59
|
+
|
|
60
|
+
raise Commenter::Error, "Duplicate comment IDs with differing content: #{conflicts.uniq.sort.join(", ")}"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def identical?(one, another)
|
|
64
|
+
one.comments == another.comments && one.proposed_change == another.proposed_change
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Commenter
|
|
4
|
+
# Disposition statistics over a comment sheet: the counts ballot reports
|
|
5
|
+
# need — comments per member body by disposition, and per comment type.
|
|
6
|
+
module BallotReport
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
# status bucket for one comment: matched observation text, else matched
|
|
10
|
+
# OSD resolution_status, else open when a GitHub issue is still open,
|
|
11
|
+
# else undecided.
|
|
12
|
+
def status_for(comment)
|
|
13
|
+
DispositionStatus.match(comment.observations) ||
|
|
14
|
+
DispositionStatus.match(comment.resolution_status) ||
|
|
15
|
+
(comment.github&.status == "open" ? :open : nil) ||
|
|
16
|
+
(comment.observations.to_s.strip.empty? && comment.resolution_status.to_s.strip.empty? ? :undecided : nil)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def counts(comment_sheet)
|
|
20
|
+
bodies = Hash.new { |hash, body| hash[body] = bucket_hash }
|
|
21
|
+
types = Hash.new(0)
|
|
22
|
+
comment_sheet.comments.each do |comment|
|
|
23
|
+
body = comment.body.to_s.strip.empty? ? "(unknown)" : comment.body
|
|
24
|
+
bodies[body][:total] += 1
|
|
25
|
+
status = status_for(comment)
|
|
26
|
+
bodies[body][status] += 1 if status
|
|
27
|
+
types[comment.type.to_s.strip.empty? ? "(untyped)" : comment.type] += 1
|
|
28
|
+
end
|
|
29
|
+
{ total: bodies.values.sum { |bucket| bucket[:total] }, bodies: bodies, types: types }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def to_markdown(comment_sheet)
|
|
33
|
+
data = counts(comment_sheet)
|
|
34
|
+
sorted_bodies = data[:bodies].sort_by { |body, _| body }
|
|
35
|
+
|
|
36
|
+
lines = []
|
|
37
|
+
lines << "| Body | Total | Accepted | AWM | Noted | Rejected | TODO | Open | Undecided |"
|
|
38
|
+
lines << "|------|-------|----------|-----|-------|----------|------|------|-----------|"
|
|
39
|
+
sorted_bodies.each do |body, bucket|
|
|
40
|
+
lines << "| #{body} | #{bucket[:total]} | #{bucket[:accepted]} | #{bucket[:accept_with_modifications]} | " \
|
|
41
|
+
"#{bucket[:noted]} | #{bucket[:rejected]} | #{bucket[:todo]} | #{bucket[:open]} | #{bucket[:undecided]} |"
|
|
42
|
+
end
|
|
43
|
+
totals = data[:bodies].values.reduce(bucket_hash) { |sum, bucket| merge_bucket(sum, bucket) }
|
|
44
|
+
lines << "| **Total** | **#{totals[:total]}** | **#{totals[:accepted]}** | " \
|
|
45
|
+
"**#{totals[:accept_with_modifications]}** | **#{totals[:noted]}** | " \
|
|
46
|
+
"**#{totals[:rejected]}** | **#{totals[:todo]}** | **#{totals[:open]}** | **#{totals[:undecided]}** |"
|
|
47
|
+
lines << ""
|
|
48
|
+
lines << "Comment types: #{data[:types].sort_by { |type, _| type }.map { |type, count| "#{type} #{count}" }.join(" · ")}"
|
|
49
|
+
lines.join("\n")
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def bucket_hash
|
|
53
|
+
{ total: 0, accepted: 0, accept_with_modifications: 0, noted: 0, rejected: 0, todo: 0, open: 0, undecided: 0 }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def merge_bucket(sum, bucket)
|
|
57
|
+
sum.merge(bucket) { |_key, accumulated, addition| accumulated + addition }
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
data/lib/commenter/cli.rb
CHANGED
|
@@ -21,33 +21,50 @@ module Commenter
|
|
|
21
21
|
option :document, type: :string, desc: "Redline: document identifier (e.g. ISO 2533:2026)"
|
|
22
22
|
option :stage, type: :string, desc: "Redline: approval stage (e.g. DIS)"
|
|
23
23
|
def import(input_file)
|
|
24
|
-
output_yaml = options[:output]
|
|
25
|
-
schema_dir = options[:schema_dir]
|
|
26
|
-
|
|
27
|
-
# Ensure schema directory exists
|
|
28
|
-
FileUtils.mkdir_p(schema_dir) unless Dir.exist?(schema_dir)
|
|
29
|
-
|
|
30
24
|
# Parse the input file
|
|
31
|
-
|
|
32
|
-
comment_sheet = parser.parse(input_file, options)
|
|
33
|
-
|
|
34
|
-
# Write the YAML data file with schema reference
|
|
35
|
-
File.write(output_yaml, comment_sheet.to_yaml_document(schema_dir))
|
|
36
|
-
|
|
37
|
-
# Copy schema file to output directory
|
|
38
|
-
schema_name = comment_sheet.schema_name
|
|
39
|
-
schema_source = File.join(__dir__, "../../schema/#{schema_name}")
|
|
40
|
-
schema_target = File.join(schema_dir, schema_name)
|
|
25
|
+
comment_sheet = Parser.new.parse(input_file, options)
|
|
41
26
|
|
|
42
|
-
|
|
43
|
-
FileUtils.cp(schema_source, schema_target) unless File.expand_path(schema_source) == File.expand_path(schema_target)
|
|
27
|
+
schema_target = write_sheet(comment_sheet, options[:output], options[:schema_dir])
|
|
44
28
|
|
|
45
|
-
puts "Converted #{input_file} to #{
|
|
29
|
+
puts "Converted #{input_file} to #{options[:output]}"
|
|
46
30
|
puts " Version: #{comment_sheet.version}"
|
|
47
31
|
puts " Comments: #{comment_sheet.comments.length}"
|
|
48
32
|
puts " Schema: #{schema_target}"
|
|
49
33
|
end
|
|
50
34
|
|
|
35
|
+
desc "merge INPUT.yaml...", "Merge multiple comment sheets into one ballot sheet"
|
|
36
|
+
option :output, type: :string, aliases: :o, default: "merged.yaml", desc: "Output YAML file"
|
|
37
|
+
option :schema_dir, type: :string, default: "schema", desc: "Directory for schema file"
|
|
38
|
+
def merge(*input_files)
|
|
39
|
+
raise "At least one input file is required" if input_files.empty?
|
|
40
|
+
|
|
41
|
+
sheets = input_files.map { |path| CommentSheet.from_yaml(File.read(path)) }
|
|
42
|
+
comment_sheet = Ballot.new(sheets).merge
|
|
43
|
+
write_sheet(comment_sheet, options[:output], options[:schema_dir])
|
|
44
|
+
|
|
45
|
+
puts "Merged #{input_files.length} files (#{comment_sheet.comments.length} comments) to #{options[:output]}"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
desc "stats INPUT.yaml", "Disposition statistics for ballot reports"
|
|
49
|
+
option :output, type: :string, aliases: :o, desc: "Output file (default: stdout)"
|
|
50
|
+
option :format, type: :string, default: "markdown", enum: %w[markdown yaml], desc: "Report format"
|
|
51
|
+
def stats(input_yaml)
|
|
52
|
+
comment_sheet = CommentSheet.from_yaml(File.read(input_yaml))
|
|
53
|
+
|
|
54
|
+
report = if options[:format] == "yaml"
|
|
55
|
+
BallotReport.counts(comment_sheet).to_yaml
|
|
56
|
+
else
|
|
57
|
+
BallotReport.to_markdown(comment_sheet)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
if options[:output]
|
|
61
|
+
File.write(options[:output], report)
|
|
62
|
+
puts "Wrote statistics to #{options[:output]}"
|
|
63
|
+
else
|
|
64
|
+
puts report
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
51
68
|
desc "fill INPUT.yaml", "Fill DOCX template from YAML comments"
|
|
52
69
|
option :output, type: :string, aliases: :o, default: "filled_comments.docx", desc: "Output DOCX file"
|
|
53
70
|
option :template, type: :string, aliases: :t, desc: "Custom template file"
|
|
@@ -55,23 +72,20 @@ module Commenter
|
|
|
55
72
|
def fill(input_yaml)
|
|
56
73
|
output_docx = options[:output]
|
|
57
74
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
# Extract comments from the structure
|
|
62
|
-
comments = if data.is_a?(Hash)
|
|
63
|
-
data["comments"] || data[:comments] || []
|
|
64
|
-
else
|
|
65
|
-
data || []
|
|
66
|
-
end
|
|
67
|
-
|
|
75
|
+
comment_sheet = CommentSheet.from_yaml(File.read(input_yaml))
|
|
76
|
+
comments = comment_sheet.comments
|
|
68
77
|
raise "No comments found in YAML file" if comments.empty?
|
|
69
78
|
|
|
70
79
|
# Use default template if none specified
|
|
71
80
|
template_path = options[:template] || File.join(__dir__, "../../data/iso_comment_template_2012-03.docx")
|
|
72
81
|
|
|
73
|
-
# Fill the template
|
|
74
|
-
|
|
82
|
+
# Fill the template, including the sheet metadata in the page header
|
|
83
|
+
fill_options = options.merge(
|
|
84
|
+
date: comment_sheet.date,
|
|
85
|
+
document: comment_sheet.document,
|
|
86
|
+
project: comment_sheet.project
|
|
87
|
+
).compact
|
|
88
|
+
Filler.new.fill(template_path, output_docx, comments, fill_options)
|
|
75
89
|
puts "Filled template to #{output_docx}"
|
|
76
90
|
end
|
|
77
91
|
|
|
@@ -212,5 +226,18 @@ module Commenter
|
|
|
212
226
|
def self.exit_on_failure?
|
|
213
227
|
true
|
|
214
228
|
end
|
|
229
|
+
|
|
230
|
+
private
|
|
231
|
+
|
|
232
|
+
def write_sheet(comment_sheet, output_yaml, schema_dir)
|
|
233
|
+
FileUtils.mkdir_p(schema_dir) unless Dir.exist?(schema_dir)
|
|
234
|
+
File.write(output_yaml, comment_sheet.to_yaml_document(schema_dir))
|
|
235
|
+
|
|
236
|
+
schema_name = comment_sheet.schema_name
|
|
237
|
+
schema_source = File.join(__dir__, "../../schema/#{schema_name}")
|
|
238
|
+
schema_target = File.join(schema_dir, schema_name)
|
|
239
|
+
FileUtils.cp(schema_source, schema_target) unless File.expand_path(schema_source) == File.expand_path(schema_target)
|
|
240
|
+
schema_target
|
|
241
|
+
end
|
|
215
242
|
end
|
|
216
243
|
end
|
data/lib/commenter/comment.rb
CHANGED
|
@@ -1,172 +1,157 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "lutaml/model"
|
|
4
|
+
|
|
3
5
|
module Commenter
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
6
|
+
# A single comment on a reviewed document. (De)serialization is declared
|
|
7
|
+
# with lutaml-model; the only custom mapping logic is the type expansion
|
|
8
|
+
# (short codes to full names) and empty-observation omission, both at the
|
|
9
|
+
# YAML boundary.
|
|
10
|
+
class Comment < Lutaml::Model::Serializable
|
|
11
|
+
attribute :id, :string
|
|
12
|
+
attribute :body, :string
|
|
13
|
+
attribute :locality, CommentLocality
|
|
14
|
+
attribute :type, :string
|
|
15
|
+
attribute :comments, :string
|
|
16
|
+
attribute :proposed_change, :string
|
|
17
|
+
attribute :observations, :string
|
|
18
|
+
|
|
19
|
+
# OSD-specific fields
|
|
20
|
+
attribute :user_name, :string
|
|
21
|
+
attribute :comment_type, :string
|
|
22
|
+
attribute :resolution_status, :string
|
|
23
|
+
attribute :resolution_date, :string
|
|
24
|
+
attribute :feedbacks, :string
|
|
25
|
+
attribute :motivation, :string
|
|
26
|
+
attribute :created_date, :string
|
|
27
|
+
attribute :stage_code, :string
|
|
28
|
+
|
|
29
|
+
attribute :github, GithubInfo
|
|
30
|
+
|
|
31
|
+
yaml do
|
|
32
|
+
map "id", to: :id
|
|
33
|
+
map "body", to: :body
|
|
34
|
+
map "locality", to: :locality
|
|
35
|
+
map "type", with: { to: :type_to_yaml, from: :type_from_yaml }
|
|
36
|
+
map "comments", to: :comments
|
|
37
|
+
map "proposed_change", to: :proposed_change
|
|
38
|
+
map "observations", with: { to: :observations_to_yaml, from: :observations_from_yaml }
|
|
39
|
+
map "user_name", to: :user_name
|
|
40
|
+
map "comment_type", to: :comment_type
|
|
41
|
+
map "resolution_status", to: :resolution_status
|
|
42
|
+
map "resolution_date", to: :resolution_date
|
|
43
|
+
map "feedbacks", to: :feedbacks
|
|
44
|
+
map "motivation", to: :motivation
|
|
45
|
+
map "created_date", to: :created_date
|
|
46
|
+
map "stage_code", to: :stage_code
|
|
47
|
+
map "github", to: :github
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def type_from_yaml(model, value)
|
|
51
|
+
model.type = CommentType.full_name(value)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def type_to_yaml(model, doc)
|
|
55
|
+
doc["type"] = model.type if model.type && !model.type.empty?
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def observations_from_yaml(model, value)
|
|
59
|
+
model.observations = value.to_s.empty? ? nil : value
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def observations_to_yaml(model, doc)
|
|
63
|
+
doc["observations"] = model.observations if model.observations && !model.observations.empty?
|
|
31
64
|
end
|
|
32
65
|
|
|
33
66
|
def expand_comment_type(type)
|
|
34
67
|
CommentType.full_name(type)
|
|
35
68
|
end
|
|
36
69
|
|
|
70
|
+
def to_h
|
|
71
|
+
Comment.to_hash(self)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
alias to_yaml_h to_h
|
|
75
|
+
|
|
37
76
|
def line_number
|
|
38
|
-
|
|
77
|
+
locality&.line_number
|
|
39
78
|
end
|
|
40
79
|
|
|
41
80
|
def line_number=(value)
|
|
42
|
-
|
|
81
|
+
self.locality = CommentLocality.new if locality.nil?
|
|
82
|
+
locality.line_number = value
|
|
43
83
|
end
|
|
44
84
|
|
|
45
85
|
def clause
|
|
46
|
-
|
|
86
|
+
locality&.clause
|
|
47
87
|
end
|
|
48
88
|
|
|
49
89
|
def clause=(value)
|
|
50
|
-
|
|
90
|
+
self.locality = CommentLocality.new if locality.nil?
|
|
91
|
+
locality.clause = value
|
|
51
92
|
end
|
|
52
93
|
|
|
53
94
|
def element
|
|
54
|
-
|
|
95
|
+
locality&.element
|
|
55
96
|
end
|
|
56
97
|
|
|
57
98
|
def element=(value)
|
|
58
|
-
|
|
99
|
+
self.locality = CommentLocality.new if locality.nil?
|
|
100
|
+
locality.element = value
|
|
59
101
|
end
|
|
60
102
|
|
|
61
|
-
def brief_summary(max_length =
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
# Add locality information first
|
|
65
|
-
parts << "Clause #{clause}" if clause && !clause.strip.empty?
|
|
66
|
-
parts << element if element && !element.strip.empty?
|
|
67
|
-
parts << "Line #{line_number}" if line_number && !line_number.strip.empty?
|
|
103
|
+
def brief_summary(max_length = 60)
|
|
104
|
+
description = comments.to_s.split(/[.!?\n]/).map(&:strip).reject(&:empty?).first.to_s
|
|
105
|
+
locality_text = format_locality
|
|
68
106
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
# Add description from comment text
|
|
72
|
-
if @comments && !@comments.strip.empty?
|
|
73
|
-
# Extract first sentence or truncate
|
|
74
|
-
clean_text = @comments.strip.gsub(/\s+/, " ")
|
|
75
|
-
first_sentence = clean_text.split(/[.!?]/).first&.strip
|
|
76
|
-
description = if first_sentence && first_sentence.length < max_length
|
|
77
|
-
first_sentence
|
|
78
|
-
else
|
|
79
|
-
clean_text[0...50]
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
if locality_text.empty?
|
|
83
|
-
description
|
|
84
|
-
else
|
|
85
|
-
# Combine locality + description, respecting max_length
|
|
86
|
-
combined = "#{locality_text}: #{description}"
|
|
87
|
-
combined.length <= max_length ? combined : "#{locality_text}: #{description[0...(max_length - locality_text.length - 2)]}"
|
|
88
|
-
end
|
|
89
|
-
else
|
|
107
|
+
if description.empty?
|
|
90
108
|
locality_text.empty? ? "No description" : locality_text
|
|
109
|
+
elsif locality_text.empty?
|
|
110
|
+
combined = description
|
|
111
|
+
combined.length <= max_length ? combined : "#{combined[0...(max_length - 3)]}..."
|
|
112
|
+
else
|
|
113
|
+
combined = "#{locality_text}: #{description}"
|
|
114
|
+
combined.length <= max_length ? combined : "#{locality_text}: #{description[0...(max_length - locality_text.length - 2)]}"
|
|
91
115
|
end
|
|
92
116
|
end
|
|
93
117
|
|
|
94
118
|
def github_issue_number
|
|
95
|
-
|
|
119
|
+
github&.issue_number
|
|
96
120
|
end
|
|
97
121
|
|
|
98
122
|
def github_issue_url
|
|
99
|
-
|
|
123
|
+
github&.issue_url
|
|
100
124
|
end
|
|
101
125
|
|
|
102
126
|
def github_status
|
|
103
|
-
|
|
127
|
+
github&.status
|
|
104
128
|
end
|
|
105
129
|
|
|
106
130
|
def github_created_at
|
|
107
|
-
|
|
131
|
+
github&.created_at
|
|
108
132
|
end
|
|
109
133
|
|
|
110
134
|
def github_updated_at
|
|
111
|
-
|
|
135
|
+
github&.updated_at
|
|
112
136
|
end
|
|
113
137
|
|
|
114
138
|
def has_github_issue?
|
|
115
|
-
|
|
139
|
+
!github&.issue_number.nil?
|
|
116
140
|
end
|
|
117
141
|
|
|
118
|
-
def
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
body: @body,
|
|
122
|
-
locality: @locality,
|
|
123
|
-
type: @type,
|
|
124
|
-
comments: @comments,
|
|
125
|
-
proposed_change: @proposed_change,
|
|
126
|
-
observations: @observations,
|
|
127
|
-
user_name: @user_name,
|
|
128
|
-
comment_type: @comment_type,
|
|
129
|
-
resolution_status: @resolution_status,
|
|
130
|
-
resolution_date: @resolution_date,
|
|
131
|
-
feedbacks: @feedbacks,
|
|
132
|
-
motivation: @motivation,
|
|
133
|
-
created_date: @created_date,
|
|
134
|
-
stage_code: @stage_code,
|
|
135
|
-
github: @github.empty? ? nil : @github
|
|
136
|
-
}.compact
|
|
137
|
-
end
|
|
138
|
-
|
|
139
|
-
def to_yaml_h
|
|
140
|
-
hash = to_h
|
|
141
|
-
# Remove observations if it's nil or empty
|
|
142
|
-
hash.delete(:observations) if hash[:observations].nil? || hash[:observations] == ""
|
|
143
|
-
stringify_keys(hash)
|
|
144
|
-
end
|
|
145
|
-
|
|
146
|
-
def self.from_hash(hash)
|
|
147
|
-
new(hash)
|
|
142
|
+
def record_github_issue(issue_number:, issue_url:, status:, created_at: nil)
|
|
143
|
+
self.github = GithubInfo.new(issue_number: issue_number, issue_url: issue_url,
|
|
144
|
+
status: status, created_at: created_at)
|
|
148
145
|
end
|
|
149
146
|
|
|
150
147
|
private
|
|
151
148
|
|
|
152
|
-
def
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
result[new_key] = new_value
|
|
159
|
-
end
|
|
160
|
-
end
|
|
161
|
-
|
|
162
|
-
def stringify_keys(hash)
|
|
163
|
-
return hash unless hash.is_a?(Hash)
|
|
164
|
-
|
|
165
|
-
hash.each_with_object({}) do |(key, value), result|
|
|
166
|
-
new_key = key.to_s
|
|
167
|
-
new_value = value.is_a?(Hash) ? stringify_keys(value) : value
|
|
168
|
-
result[new_key] = new_value
|
|
169
|
-
end
|
|
149
|
+
def format_locality
|
|
150
|
+
parts = []
|
|
151
|
+
parts << "Clause #{clause}" if clause && !clause.strip.empty?
|
|
152
|
+
parts << element if element && !element.strip.empty?
|
|
153
|
+
parts << "Line #{line_number}" if line_number && !line_number.strip.empty?
|
|
154
|
+
parts.join(", ")
|
|
170
155
|
end
|
|
171
156
|
end
|
|
172
157
|
end
|