commenter 0.3.0 → 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/.rubocop.yml +4 -0
- data/CLAUDE.md +17 -7
- data/CONTEXT.md +27 -0
- data/README.adoc +92 -17
- 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 +62 -43
- data/lib/commenter/comment.rb +103 -126
- data/lib/commenter/comment_locality.rb +18 -0
- data/lib/commenter/comment_sheet.rb +31 -63
- data/lib/commenter/comment_type.rb +39 -0
- 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 +17 -71
- data/lib/commenter/github_session.rb +35 -0
- data/lib/commenter/parser/osd_xlsx_parser.rb +1 -12
- data/lib/commenter/parser/track_change_docx_parser.rb +330 -0
- data/lib/commenter/parser.rb +10 -13
- data/lib/commenter/version.rb +1 -1
- data/lib/commenter.rb +16 -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 +35 -10
- data/spec/commenter/comment_spec.rb +22 -9
- data/spec/commenter/comment_type_spec.rb +45 -0
- data/spec/commenter/disposition_status_spec.rb +40 -0
- data/spec/commenter/filler_spec.rb +119 -0
- data/spec/commenter/github_integration_spec.rb +65 -32
- data/spec/commenter/osd_xlsx_parser_spec.rb +3 -2
- data/spec/commenter/track_change_docx_parser_spec.rb +277 -0
- data/spec/support/redline_docx_builder.rb +95 -0
- data/spec/support/xlsx_builder.rb +17 -12
- metadata +32 -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/.rubocop.yml
CHANGED
data/CLAUDE.md
CHANGED
|
@@ -18,13 +18,23 @@ bin/console # interactive prompt
|
|
|
18
18
|
|
|
19
19
|
CI (metanorma/ci `generic-rake.yml`) runs `bundle exec rake` on Ruby 3.2/3.4/4.0 across macOS/Ubuntu/Windows. RuboCop offenses fail CI — run `bundle exec rake`, not just rspec, before pushing. `Gemfile.lock` is gitignored; dependencies resolve fresh in CI.
|
|
20
20
|
|
|
21
|
-
Specs generate XLSX fixtures at runtime via `spec/support/xlsx_builder.rb` (plain rubyzip, no spreadsheet-writing dependency) plus row data in `spec/support/osd_fixtures.rb` — there are no binary fixture files in the repo.
|
|
21
|
+
Specs generate XLSX fixtures at runtime via `spec/support/xlsx_builder.rb` (plain rubyzip, no spreadsheet-writing dependency) plus row data in `spec/support/osd_fixtures.rb` — there are no binary fixture files in the repo. Redline DOCX fixtures are built at runtime via `spec/support/redline_docx_builder.rb` (same approach).
|
|
22
|
+
|
|
23
|
+
## Release
|
|
24
|
+
|
|
25
|
+
NEVER bump `lib/commenter/version.rb` by hand and never push tags. Releases go through the GHA workflow, which does the version bump, `v*` tag, and gem push itself:
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
gh workflow run release.yml --ref main -f next_version=X.Y.Z
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The version number is the maintainer's decision — ask, never infer it from semver reasoning. PRs are rebase-merged.
|
|
22
32
|
|
|
23
33
|
## What this gem does
|
|
24
34
|
|
|
25
35
|
Converts ISO comment sheets to structured YAML and back, and syncs comments to GitHub issues:
|
|
26
36
|
|
|
27
|
-
- **Import**: DOCX (ISO 2012-03 balloting template) or XLSX (ISO Online Standards Development exports) → YAML + schema file. Format auto-detected from extension.
|
|
37
|
+
- **Import**: DOCX (ISO 2012-03 balloting template) or XLSX (ISO Online Standards Development exports) → YAML + schema file. Format auto-detected from extension. Redline DOCX with tracked changes is dispatched to `Parser::TrackChangeDocxParser` via `--format redline`.
|
|
28
38
|
- **Fill**: YAML → filled DOCX comment sheet (`data/iso_comment_template_2012-03.docx`), optional status-based cell shading.
|
|
29
39
|
- **GitHub round-trip**: `github-create` makes issues from YAML via Liquid templates; `github-retrieve` pulls `> **OBSERVATION:**` blockquotes from closed issues back into the YAML's observations field.
|
|
30
40
|
|
|
@@ -34,11 +44,12 @@ Plain text only — formulas/images/complex formatting are unsupported (docx gem
|
|
|
34
44
|
|
|
35
45
|
Core flow: `Parser` → `CommentSheet` (metadata + `Comment` objects) → YAML / DOCX / GitHub issues.
|
|
36
46
|
|
|
37
|
-
- `Commenter::Comment` / `Commenter::CommentSheet` — data model. `Comment` carries the common fields plus optional OSD-specific fields (`user_name`, `resolution_status`, `motivation`, etc.) and a `github` sub-hash tracking issue state. The sheet's `version` field (`"2012-03"` vs `"osd"`) selects the output schema.
|
|
38
|
-
- `Commenter::Parser` — dispatches by format: `.docx` parsed inline with the `docx` gem; `.xlsx` delegated to `Parser::OsdXlsxParser` (uses `roo`)
|
|
47
|
+
- `Commenter::Comment` / `Commenter::CommentSheet` — data model. `Comment` carries the common fields plus optional OSD-specific fields (`user_name`, `resolution_status`, `motivation`, etc.) and a `github` sub-hash tracking issue state. Comment types: short codes (`ge`/`te`/`ed`) are expanded to full names (`general`/`technical`/`editorial`) when a `Comment` is loaded; YAML output stores the expanded form. The sheet's `version` field (`"2012-03"` vs `"osd"`) selects the output schema.
|
|
48
|
+
- `Commenter::Parser` — dispatches by format: `.docx` parsed inline with the `docx` gem; `.xlsx` delegated to `Parser::OsdXlsxParser` (uses `roo`); `redline` delegated to `Parser::TrackChangeDocxParser`.
|
|
39
49
|
- `Parser::OsdXlsxParser` — auto-detects two OSD export variants by header row ("resolved" 17-col starting `Comment ID`; "unresolved" 15-col starting `User name`), extracts metadata (date/reference/stage/titles) from header rows 1–2, maps columns by header name into `Comment` attributes, and synthesizes `observations` from `resolution_status` + `motivation`.
|
|
40
|
-
- `
|
|
41
|
-
- `Commenter::
|
|
50
|
+
- `Parser::TrackChangeDocxParser` — redline DOCX import: streams `word/document.xml` with Nokogiri XML::Reader (redlines can exceed 100 MB), captures `w:ins`/`w:del`/`w:moveFrom`/`w:moveTo` as comment entries (`proposed_change` renders the change itself, e.g. `Insert: "text"`; clause resolved from the nearest preceding heading including sub-clauses; element resolved from Table/Figure/Formula/NOTE references in the same clause; self-closing paragraph-mark markers skipped), plus reviewer `w:comment` threads as `-CNNN` remark entries (remark verbatim in `comments`, instruction reworded in `proposed_change`, empty `observations` for the owner). Stamps `observations` from the `observations`/`accept_all` options.
|
|
51
|
+
- `Commenter::Filler` — writes comments into the DOCX template table; maps observation status text (`accept(ed)?`, `noted`, `reject(ed)?`, …) to shading colors.
|
|
52
|
+
- GitHub integration (`lib/commenter/github_integration.rb`) — two classes with parallel structure: `GitHubIssueCreator` (`github-create`) and `GitHubIssueRetriever` (`github-retrieve`). Each loads its own config YAML and Octokit client, and rewrites the comments YAML in place after the run. Duplicate detection searches issue titles for a unique ID rendered from a configurable Liquid `unique_id` template (stage-aware by default: `[DIS] GB-001`), so the same comment ID at different ballot stages creates separate issues.
|
|
42
53
|
- `Commenter::Cli` (`lib/commenter/cli.rb`) — Thor CLI with subcommands `import`, `fill`, `github-create`, `github-retrieve`.
|
|
43
54
|
|
|
44
55
|
### Schemas
|
|
@@ -55,4 +66,3 @@ Core flow: `Parser` → `CommentSheet` (metadata + `Comment` objects) → YAML /
|
|
|
55
66
|
- `data/` files (DOCX template, Liquid templates, sample config) are shipped with the gem and loaded relative to `__dir__` — do not treat them as disposable.
|
|
56
67
|
- Testing GitHub integration: use `--dry-run` with `GITHUB_TOKEN=dummy_token` — exercises template rendering without API calls.
|
|
57
68
|
- `sig/commenter.rbs` is a minimal stub; RBS coverage is not currently maintained.
|
|
58
|
-
- Release: pushing a `v*` tag triggers `.github/workflows/release.yml`. Version lives in `lib/commenter/version.rb`.
|
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
|
@@ -19,6 +19,8 @@ The supported input formats are:
|
|
|
19
19
|
(DOCX)
|
|
20
20
|
* ISO Online Standards Development (OSD) XLSX exports (resolved and unresolved
|
|
21
21
|
comment sheets)
|
|
22
|
+
* Redlined Word documents containing tracked changes (e.g. ISO/CS editor
|
|
23
|
+
redlines), imported with `--format redline`
|
|
22
24
|
|
|
23
25
|
This gem only supports plain text comment extraction and filling. Only use this
|
|
24
26
|
to handle plain text comments and resolutions.
|
|
@@ -56,7 +58,9 @@ $ gem install commenter
|
|
|
56
58
|
=== Importing comments
|
|
57
59
|
|
|
58
60
|
Convert an ISO comment sheet (DOCX or XLSX) to structured YAML.
|
|
59
|
-
The format is auto-detected from the file extension
|
|
61
|
+
The format is auto-detected from the file extension; redline DOCX must be
|
|
62
|
+
selected explicitly with `--format redline` because the extension cannot
|
|
63
|
+
distinguish a redline from a comment sheet.
|
|
60
64
|
|
|
61
65
|
[source,shell]
|
|
62
66
|
----
|
|
@@ -68,8 +72,28 @@ $ commenter import "91855-Comments-resolved.xlsx" -o comments.yaml
|
|
|
68
72
|
|
|
69
73
|
# From XLSX (ISO OSD comments-only export)
|
|
70
74
|
$ commenter import "ISO 5843-6-Comments.xlsx" -o comments.yaml
|
|
75
|
+
|
|
76
|
+
# From a redlined DOCX with tracked changes (ISO/CS editor redline)
|
|
77
|
+
$ commenter import "ISO_DIS 2533 Enquiry Trackchange Word.docx" --format redline \
|
|
78
|
+
--body CS --document "ISO 2533:2026" --stage DIS \
|
|
79
|
+
--observations "Accepted. ISO/CS tracked change accepted." \
|
|
80
|
+
-o comments-cs.yaml
|
|
71
81
|
----
|
|
72
82
|
|
|
83
|
+
Each tracked change (`w:ins`, `w:del`, `w:moveFrom`, `w:moveTo`) becomes a
|
|
84
|
+
comment entry whose `proposed_change` renders the change itself
|
|
85
|
+
(`Insert: "..."` / `Delete: "..."` / `Move ...: "..."`) and whose
|
|
86
|
+
`locality.clause` is resolved from the nearest preceding heading (including
|
|
87
|
+
sub-clauses such as `4.2.1` and `Annex A`), with `locality.element` resolved
|
|
88
|
+
from the nearest `Table`/`Figure`/`Formula`/`NOTE` reference in the same
|
|
89
|
+
clause. Self-closing paragraph-mark markers are skipped because they carry
|
|
90
|
+
no content.
|
|
91
|
+
|
|
92
|
+
Reviewer comment threads (`w:comment`) are emitted after the track changes
|
|
93
|
+
with `-CNNN` ids: the remark verbatim in `comments`, its instruction
|
|
94
|
+
reworded as the `proposed_change` (a leading "Please " is dropped), and
|
|
95
|
+
empty `observations` for the document owner to draft.
|
|
96
|
+
|
|
73
97
|
This will create two files:
|
|
74
98
|
|
|
75
99
|
`comments.yaml`:: The structured comment data
|
|
@@ -87,20 +111,29 @@ Options:
|
|
|
87
111
|
`-o, --output FILE`:: Output YAML file (default: comments.yaml)
|
|
88
112
|
`-e, --exclude-observations`:: Skip the observations column
|
|
89
113
|
`--schema-dir DIR`:: Directory for schema file (default: schema)
|
|
90
|
-
`--format FORMAT`:: Force input format (`docx` or `
|
|
91
|
-
`--sheet NAME`:: XLSX sheet name to parse (default: first sheet)
|
|
114
|
+
`--format FORMAT`:: Force input format (`docx`, `xlsx` or `redline`; auto-detected by default)
|
|
115
|
+
`--sheet NAME`:: XLSX: sheet name to parse (default: first sheet)
|
|
92
116
|
`--resolved-only`:: XLSX: use resolved comments sheet only
|
|
93
117
|
`--unresolved-only`:: XLSX: use unresolved comments sheet only
|
|
118
|
+
`--body CODE`:: Redline: member body code for comment IDs (default: `CS`)
|
|
119
|
+
`--observations TEXT`:: Redline: observations stamped on all track changes
|
|
120
|
+
`--accept-all`:: Redline: stamp `Accepted. Tracked change accepted.` on all track changes (an explicit `--observations` wins)
|
|
121
|
+
`--document ID`:: Redline: document identifier (e.g. `ISO 2533:2026`)
|
|
122
|
+
`--stage STAGE`:: Redline: approval stage (e.g. `DIS`)
|
|
94
123
|
|
|
95
124
|
==== Metadata (extraction limitation)
|
|
96
125
|
|
|
97
126
|
WARNING: Due to a limitation in the underlying `docx` gem, metadata fields
|
|
98
|
-
(Date, Document, Project)
|
|
99
|
-
|
|
100
|
-
|
|
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.
|
|
101
131
|
|
|
102
|
-
The
|
|
103
|
-
|
|
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:
|
|
104
137
|
|
|
105
138
|
[source,yaml]
|
|
106
139
|
----
|
|
@@ -113,9 +146,6 @@ project: "" # Manually add: e.g., "Project name"
|
|
|
113
146
|
comments:
|
|
114
147
|
----
|
|
115
148
|
|
|
116
|
-
Alternatively, you can provide metadata during import using CLI options (planned
|
|
117
|
-
for future release).
|
|
118
|
-
|
|
119
149
|
|
|
120
150
|
==== Example YAML output
|
|
121
151
|
|
|
@@ -134,7 +164,7 @@ comments:
|
|
|
134
164
|
line_number:
|
|
135
165
|
clause: "_whole document"
|
|
136
166
|
element:
|
|
137
|
-
type:
|
|
167
|
+
type: general
|
|
138
168
|
comments: |
|
|
139
169
|
The document should include more examples
|
|
140
170
|
to clarify the implementation requirements.
|
|
@@ -150,7 +180,7 @@ comments:
|
|
|
150
180
|
line_number: "45"
|
|
151
181
|
clause: "5.2.1"
|
|
152
182
|
element: "Table 3"
|
|
153
|
-
type:
|
|
183
|
+
type: technical
|
|
154
184
|
comments: |
|
|
155
185
|
The values in Table 3 appear to be
|
|
156
186
|
inconsistent with the formula in 5.1.
|
|
@@ -160,11 +190,56 @@ comments:
|
|
|
160
190
|
observations:
|
|
161
191
|
----
|
|
162
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
|
+
|
|
163
237
|
=== Filling DOCX templates from YAML
|
|
164
238
|
|
|
165
239
|
This gem contains a command-line utility to fill a DOCX template with comments
|
|
166
240
|
from a YAML file. It generates a filled comment sheet that can be used for
|
|
167
|
-
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.
|
|
168
243
|
|
|
169
244
|
The base template is the ISO comment sheet template located at
|
|
170
245
|
`data/iso_comment_template_2012-03.docx`. You can also provide a custom
|
|
@@ -471,7 +546,7 @@ comments:
|
|
|
471
546
|
locality:
|
|
472
547
|
clause: "5.2.1"
|
|
473
548
|
element: "Table 3"
|
|
474
|
-
type:
|
|
549
|
+
type: technical
|
|
475
550
|
comments: "The values in Table 3 appear inconsistent..."
|
|
476
551
|
proposed_change: "Correct the values in column 2..."
|
|
477
552
|
observations: ""
|
|
@@ -576,10 +651,10 @@ recognized and applied to the observations column:
|
|
|
576
651
|
|===
|
|
577
652
|
| Status Pattern | Intended Color | Hex Code | Example
|
|
578
653
|
|
|
654
|
+
| `awm\|accept(ed)? with (modifications\|changes)` | Olive Green | #C4D79B | "Accepted with modifications"
|
|
579
655
|
| `accept(ed)?` | Green | #92D050 | "Accepted"
|
|
580
|
-
| `awm\|accept with modifications` | Olive Green | #C4D79B | "Accept with modifications"
|
|
581
656
|
| `noted` | Blue | #8DB4E2 | "Noted"
|
|
582
|
-
| `reject(ed)
|
|
657
|
+
| `reject(ed)?\|not accepted` | Pink | #FF99CC | "Rejected"
|
|
583
658
|
| `todo` | Diagonal stripes | #D9D9D9 | "TODO: Review"
|
|
584
659
|
|
|
585
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
|
@@ -3,51 +3,68 @@
|
|
|
3
3
|
require "thor"
|
|
4
4
|
require "yaml"
|
|
5
5
|
require "fileutils"
|
|
6
|
-
require "commenter
|
|
7
|
-
require "commenter/filler"
|
|
8
|
-
require "commenter/github_integration"
|
|
6
|
+
require "commenter"
|
|
9
7
|
|
|
10
8
|
module Commenter
|
|
11
9
|
class Cli < Thor
|
|
12
|
-
desc "import INPUT", "Convert comment sheet (DOCX or XLSX) to YAML"
|
|
10
|
+
desc "import INPUT", "Convert comment sheet (DOCX or XLSX) or redline DOCX to YAML"
|
|
13
11
|
option :output, type: :string, aliases: :o, default: "comments.yaml", desc: "Output YAML file"
|
|
14
12
|
option :exclude_observations, type: :boolean, aliases: :e, desc: "Exclude observations column"
|
|
15
13
|
option :schema_dir, type: :string, default: "schema", desc: "Directory for schema file"
|
|
16
|
-
option :format, type: :string, desc: "Force input format (docx or
|
|
14
|
+
option :format, type: :string, desc: "Force input format (docx, xlsx or redline)"
|
|
17
15
|
option :sheet, type: :string, desc: "XLSX sheet name to parse (default: first sheet)"
|
|
18
16
|
option :resolved_only, type: :boolean, desc: "XLSX: use resolved comments sheet only"
|
|
19
17
|
option :unresolved_only, type: :boolean, desc: "XLSX: use unresolved comments sheet only"
|
|
18
|
+
option :body, type: :string, desc: "Redline: member body code for comment IDs (default: CS)"
|
|
19
|
+
option :observations, type: :string, desc: "Redline: observations stamped on all track changes"
|
|
20
|
+
option :accept_all, type: :boolean, desc: "Redline: stamp 'Accepted. Tracked change accepted.' on all track changes"
|
|
21
|
+
option :document, type: :string, desc: "Redline: document identifier (e.g. ISO 2533:2026)"
|
|
22
|
+
option :stage, type: :string, desc: "Redline: approval stage (e.g. DIS)"
|
|
20
23
|
def import(input_file)
|
|
21
|
-
output_yaml = options[:output]
|
|
22
|
-
schema_dir = options[:schema_dir]
|
|
23
|
-
|
|
24
|
-
# Ensure schema directory exists
|
|
25
|
-
FileUtils.mkdir_p(schema_dir) unless Dir.exist?(schema_dir)
|
|
26
|
-
|
|
27
24
|
# Parse the input file
|
|
28
|
-
|
|
29
|
-
comment_sheet = parser.parse(input_file, options)
|
|
30
|
-
|
|
31
|
-
# Determine which schema to use based on version
|
|
32
|
-
schema_name = comment_sheet.version == "osd" ? "iso_comment_osd.yaml" : "iso_comment_2012-03.yaml"
|
|
25
|
+
comment_sheet = Parser.new.parse(input_file, options)
|
|
33
26
|
|
|
34
|
-
|
|
35
|
-
yaml_content = generate_yaml_with_header(comment_sheet.to_yaml_h, schema_dir, schema_name)
|
|
36
|
-
File.write(output_yaml, yaml_content)
|
|
37
|
-
|
|
38
|
-
# Copy schema file to output directory
|
|
39
|
-
schema_source = File.join(__dir__, "../../schema/#{schema_name}")
|
|
40
|
-
schema_target = File.join(schema_dir, schema_name)
|
|
27
|
+
schema_target = write_sheet(comment_sheet, options[:output], options[:schema_dir])
|
|
41
28
|
|
|
42
|
-
|
|
43
|
-
FileUtils.cp(schema_source, schema_target) unless File.expand_path(schema_source) == File.expand_path(schema_target)
|
|
44
|
-
|
|
45
|
-
puts "Converted #{input_file} to #{output_yaml}"
|
|
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
|
|
|
@@ -215,10 +229,15 @@ module Commenter
|
|
|
215
229
|
|
|
216
230
|
private
|
|
217
231
|
|
|
218
|
-
def
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
|
222
241
|
end
|
|
223
242
|
end
|
|
224
243
|
end
|