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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 31fa0d4ee056c833d9f2b329d994e5d6151e3430426e6d4f02dfca4c670d0c18
4
- data.tar.gz: b459baef384850cfa84cfb22c48212771ee57fa0e11cbd803c1db33dfb44aae2
3
+ metadata.gz: 99f441f58237864e8ea0bc5c8682f04d2b15757ef1f28c45317f0181d6621fb4
4
+ data.tar.gz: 0a48406aa86377612a2722455dad5013457a10805da311c74c1fbfb82cccafd7
5
5
  SHA512:
6
- metadata.gz: 989ff9a26a63210438385658edaee8a0d94a35c9d80916dfefe89575ce739f9efc75bdcc372d737a955de236fdefdeaaca0be65146363dea5833ac280cc97cdb
7
- data.tar.gz: 5cdcb2b0ca204b6e419d009f45eeb6dfd4867c7eb51196a04ad2499c7c64e5cb5288b85522f031a7a823e9242c3e8edec771d7c2d1bb8f5f9b079df7e86cc54b
6
+ metadata.gz: 7c26ede9e2a9e4f5f060c0b8679cc9afcb36214a4c5b1a2f02c15a44cd6671d7b104954f4cf4b6df3df9f3806ab7c4d0418982a75371805c773c90064b82c8b9
7
+ data.tar.gz: 32dd2e31a603391bef5a112d0b10da62b7c29e249bcc76c3e5456aab939015ff51d21036108c3366743db11e01411a2bd285b7992f1a534009d5d4cc184e9d31
data/.rubocop.yml CHANGED
@@ -8,3 +8,7 @@ Style/StringLiterals:
8
8
 
9
9
  Style/StringLiteralsInInterpolation:
10
10
  EnforcedStyle: double_quotes
11
+
12
+ Metrics/ModuleLength:
13
+ Exclude:
14
+ - "spec/**/*"
data/CLAUDE.md ADDED
@@ -0,0 +1,58 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Commands
6
+
7
+ ```sh
8
+ bin/setup # install dependencies
9
+ bundle exec rake # default task: specs + RuboCop (this is what CI runs)
10
+ bundle exec rspec # all specs
11
+ bundle exec rspec spec/commenter/comment_spec.rb # single spec file
12
+ bundle exec rspec spec/commenter/comment_spec.rb:42 # single example
13
+ bundle exec rubocop # lint (part of default rake task — must be clean)
14
+ bundle exec rubocop -a # autocorrect
15
+ bundle exec exe/commenter # run the CLI locally
16
+ bin/console # interactive prompt
17
+ ```
18
+
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
+
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.
22
+
23
+ ## What this gem does
24
+
25
+ Converts ISO comment sheets to structured YAML and back, and syncs comments to GitHub issues:
26
+
27
+ - **Import**: DOCX (ISO 2012-03 balloting template) or XLSX (ISO Online Standards Development exports) → YAML + schema file. Format auto-detected from extension.
28
+ - **Fill**: YAML → filled DOCX comment sheet (`data/iso_comment_template_2012-03.docx`), optional status-based cell shading.
29
+ - **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
+
31
+ Plain text only — formulas/images/complex formatting are unsupported (docx gem limitation).
32
+
33
+ ## Architecture
34
+
35
+ Core flow: `Parser` → `CommentSheet` (metadata + `Comment` objects) → YAML / DOCX / GitHub issues.
36
+
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`).
39
+ - `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
+ - `Commenter::Filler` — writes comments into the DOCX template table.
41
+ - `Commenter::GitHubIssueCreator` (`lib/commenter/github_integration.rb`) — Octokit-based; duplicate detection searches 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
+ - `Commenter::Cli` (`lib/commenter/cli.rb`) — Thor CLI with subcommands `import`, `fill`, `github-create`, `github-retrieve`.
43
+
44
+ ### Schemas
45
+
46
+ `schema/iso_comment_2012-03.yaml` and `schema/iso_comment_osd.yaml` are JSON-Schema-style files for IDE validation. `import` copies the matching schema (based on sheet version) into the schema dir and stamps the YAML header with a `yaml-language-server` reference. When the data model changes, update the corresponding schema file.
47
+
48
+ ### Templates
49
+
50
+ `data/github_issue_title_template.liquid` and `data/github_issue_body_template.liquid` render issue titles/bodies; both are user-overridable via config. See README.adoc for the full variable list.
51
+
52
+ ## Conventions
53
+
54
+ - Ruby style: double-quoted strings (enforced by RuboCop).
55
+ - `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
+ - Testing GitHub integration: use `--dry-run` with `GITHUB_TOKEN=dummy_token` — exercises template rendering without API calls.
57
+ - `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/README.adoc CHANGED
@@ -7,14 +7,18 @@ image:https://img.shields.io/github/commits-since/metanorma/commenter/latest.svg
7
7
 
8
8
  == Purpose
9
9
 
10
- Commenter is a Ruby gem for working with ISO comment sheets in DOCX format.
10
+ Commenter is a Ruby gem for working with ISO comment sheets in DOCX and XLSX
11
+ format.
11
12
 
12
13
  It provides utilities for parsing, manipulating, and serializing ISO comment
13
- data, converting between DOCX and structured YAML with schema validation.
14
+ data, converting between DOCX/XLSX and structured YAML with schema validation.
14
15
 
15
- The format is taken from:
16
+ The supported input formats are:
16
17
 
17
18
  * "ISO/IEC/CEN/CENELEC electronic balloting commenting template/version 2012-03"
19
+ (DOCX)
20
+ * ISO Online Standards Development (OSD) XLSX exports (resolved and unresolved
21
+ comment sheets)
18
22
 
19
23
  This gem only supports plain text comment extraction and filling. Only use this
20
24
  to handle plain text comments and resolutions.
@@ -49,19 +53,27 @@ $ gem install commenter
49
53
 
50
54
  == Usage
51
55
 
52
- === Importing comments from DOCX
56
+ === Importing comments
53
57
 
54
- Convert an ISO comment sheet DOCX file to structured YAML:
58
+ Convert an ISO comment sheet (DOCX or XLSX) to structured YAML.
59
+ The format is auto-detected from the file extension.
55
60
 
56
61
  [source,shell]
57
62
  ----
63
+ # From DOCX (ISO 2012-03 template or OSD DOCX export)
58
64
  $ commenter import "ISO 80000-2 review comments.docx" -o comments.yaml
65
+
66
+ # From XLSX (ISO OSD resolved comment export)
67
+ $ commenter import "91855-Comments-resolved.xlsx" -o comments.yaml
68
+
69
+ # From XLSX (ISO OSD comments-only export)
70
+ $ commenter import "ISO 5843-6-Comments.xlsx" -o comments.yaml
59
71
  ----
60
72
 
61
73
  This will create two files:
62
74
 
63
75
  `comments.yaml`:: The structured comment data
64
- `schema/iso_comment_2012-03.yaml`:: The YAML schema for validation
76
+ `schema/iso_comment_2012-03.yaml` or `schema/iso_comment_osd.yaml`:: The YAML schema for validation (selected automatically based on input format)
65
77
 
66
78
  ==== Import options
67
79
 
@@ -75,6 +87,10 @@ Options:
75
87
  `-o, --output FILE`:: Output YAML file (default: comments.yaml)
76
88
  `-e, --exclude-observations`:: Skip the observations column
77
89
  `--schema-dir DIR`:: Directory for schema file (default: schema)
90
+ `--format FORMAT`:: Force input format (`docx` or `xlsx`; auto-detected by default)
91
+ `--sheet NAME`:: XLSX sheet name to parse (default: first sheet)
92
+ `--resolved-only`:: XLSX: use resolved comments sheet only
93
+ `--unresolved-only`:: XLSX: use unresolved comments sheet only
78
94
 
79
95
  ==== Metadata (extraction limitation)
80
96
 
@@ -530,12 +546,17 @@ The comment types are defined as follows:
530
546
  `te`:: Technical comment
531
547
  `ed`:: Editorial comment
532
548
 
549
+ NOTE: In generated YAML, types are stored in expanded form (`general`,
550
+ `technical`, `editorial`). Short codes are accepted as input anywhere and are
551
+ expanded when the comment is loaded.
552
+
533
553
  === Workflow integration
534
554
 
535
555
  [source,mermaid]
536
556
  ----
537
557
  flowchart LR
538
- A[ISO Comment Sheet DOCX] --> B[commenter import]
558
+ A1[ISO Comment Sheet DOCX] --> B[commenter import]
559
+ A2[ISO OSD XLSX export] --> B
539
560
  B --> C[YAML + Schema]
540
561
  C --> D[commenter github-create]
541
562
  D --> E[YAML + GitHub Info]
@@ -566,6 +587,8 @@ recognized and applied to the observations column:
566
587
 
567
588
  == Data model
568
589
 
590
+ === ISO 2012-03 template (DOCX)
591
+
569
592
  The comment structure follows this schema:
570
593
 
571
594
  [source,yaml]
@@ -593,14 +616,61 @@ comments: # Array of comment objects
593
616
  updated_at: string # ISO 8601 timestamp (optional)
594
617
  ----
595
618
 
619
+ === ISO OSD format (XLSX)
620
+
621
+ The OSD format includes additional metadata and resolution fields extracted from
622
+ the ISO Online Standards Development platform:
623
+
624
+ [source,yaml]
625
+ ----
626
+ version: "osd" # OSD format identifier
627
+ date: string # Date from XLSX header (auto-extracted)
628
+ document: string # Document reference, e.g. "ISO/DIS 5843-6(en)"
629
+ project: string # Project name (auto-extracted)
630
+ stage: string # Stage: WD, CD, DIS, FDIS, PRF, PUB (auto-extracted)
631
+ title_en: string # Document title in English (auto-extracted)
632
+ title_fr: string # Document title in French (auto-extracted)
633
+ comments:
634
+ - id: string # Comment ID (numeric from OSD)
635
+ body: string # User name / member body
636
+ locality:
637
+ clause: string # Clause number
638
+ element: string # Clause title
639
+ type: "ge" | "te" | "ed"
640
+ comments: string # Comment text
641
+ proposed_change: string
642
+ observations: string # Built from resolution_status + motivation
643
+ user_name: string # OSD user name
644
+ comment_type: string # Subtype: Editorial, General, Technical
645
+ resolution_status: string # Accepted, Partially accepted, Rejected, etc.
646
+ resolution_date: string # Date resolved
647
+ feedbacks: string # Discussion replies
648
+ motivation: string # Resolution justification
649
+ created_date: string # Date comment was created
650
+ stage_code: string # ISO stage code (e.g. "40.20")
651
+ ----
652
+
653
+ The OSD XLSX format comes in two variants, both auto-detected:
654
+
655
+ *Resolved*: 17 columns starting with `Comment ID`, includes resolution data
656
+ (`Resolution status`, `Motivation`, `Resolution Date`, `Stage code`)
657
+
658
+ *Unresolved/comments-only*: 15 columns starting with `User name`, includes
659
+ `Comment type`, `Comment/Motivation`, `Replies`, `Comment number`
660
+
596
661
 
597
662
  == Schema validation
598
663
 
599
- Each exported YAML file includes a schema reference for IDE support:
664
+ Each exported YAML file includes a schema reference for IDE support.
665
+ The schema is selected automatically based on the input format:
600
666
 
601
667
  [source,yaml]
602
668
  ----
669
+ # For ISO 2012-03 DOCX imports:
603
670
  # yaml-language-server: $schema=schema/iso_comment_2012-03.yaml
671
+
672
+ # For ISO OSD XLSX imports:
673
+ # yaml-language-server: $schema=schema/iso_comment_osd.yaml
604
674
  ----
605
675
 
606
676
  This enables:
@@ -695,7 +765,8 @@ The gem is organized into several key components:
695
765
 
696
766
  `Commenter::Comment`:: Represents individual comments with locality, type, and content
697
767
  `Commenter::CommentSheet`:: Container for multiple comments with metadata
698
- `Commenter::Parser`:: Handles DOCX parsing and YAML generation
768
+ `Commenter::Parser`:: Handles DOCX and XLSX parsing with auto-detection
769
+ `Commenter::Parser::OsdXlsxParser`:: Parses ISO OSD XLSX exports (resolved and unresolved variants)
699
770
  `Commenter::Filler`:: Fills DOCX templates with comment data
700
771
  `Commenter::GitHubIssueCreator`:: Creates GitHub issues from comments
701
772
 
@@ -703,7 +774,7 @@ The gem is organized into several key components:
703
774
 
704
775
  `Commenter::Cli`:: Thor-based command-line interface with subcommands:
705
776
 
706
- ** `import` - Convert DOCX to YAML
777
+ ** `import` - Convert DOCX or XLSX to YAML
707
778
  ** `fill` - Fill DOCX template from YAML
708
779
  ** `github-create` - Create GitHub issues from comments
709
780
  ** `github-retrieve` - Retrieve observations from GitHub issues
@@ -714,7 +785,8 @@ The gem is organized into several key components:
714
785
  * `data/github_issue_title_template.liquid` - GitHub issue title template
715
786
  * `data/github_issue_body_template.liquid` - GitHub issue body template
716
787
  * `data/github_config_sample.yaml` - Sample GitHub configuration
717
- * `schema/iso_comment_2012-03.yaml` - YAML schema for validation
788
+ * `schema/iso_comment_2012-03.yaml` - YAML schema for ISO 2012-03 validation
789
+ * `schema/iso_comment_osd.yaml` - YAML schema for ISO OSD format validation
718
790
 
719
791
  === Debugging
720
792
 
data/commenter.gemspec CHANGED
@@ -12,8 +12,8 @@ Gem::Specification.new do |spec|
12
12
  spec.authors = ["Ribose"]
13
13
  spec.email = ["open.source@ribose.com"]
14
14
 
15
- spec.summary = "Library to work with ISO comment sheets in DOCX format."
16
- spec.description = "Convert between ISO comment sheet DOCX and structured YAML with schema validation."
15
+ spec.summary = "Library to work with ISO comment sheets in DOCX and XLSX formats."
16
+ spec.description = "Convert between ISO comment sheets (DOCX and XLSX) and structured YAML with schema validation."
17
17
  spec.homepage = "https://github.com/metanorma/commenter"
18
18
  spec.license = "BSD-2-Clause"
19
19
  spec.required_ruby_version = Gem::Requirement.new(">= 2.6.0")
@@ -35,5 +35,6 @@ Gem::Specification.new do |spec|
35
35
  spec.add_dependency "dotenv", "~> 2.8"
36
36
  spec.add_dependency "liquid", "~> 5.0"
37
37
  spec.add_dependency "octokit", "~> 6.0"
38
+ spec.add_dependency "roo", ">= 2.10"
38
39
  spec.add_dependency "thor", "~> 1.0"
39
40
  end
data/lib/commenter/cli.rb CHANGED
@@ -9,37 +9,43 @@ require "commenter/github_integration"
9
9
 
10
10
  module Commenter
11
11
  class Cli < Thor
12
- desc "import INPUT.docx", "Convert DOCX comment sheet to YAML"
12
+ desc "import INPUT", "Convert comment sheet (DOCX or XLSX) to YAML"
13
13
  option :output, type: :string, aliases: :o, default: "comments.yaml", desc: "Output YAML file"
14
14
  option :exclude_observations, type: :boolean, aliases: :e, desc: "Exclude observations column"
15
15
  option :schema_dir, type: :string, default: "schema", desc: "Directory for schema file"
16
- def import(input_docx)
16
+ option :format, type: :string, desc: "Force input format (docx or xlsx)"
17
+ option :sheet, type: :string, desc: "XLSX sheet name to parse (default: first sheet)"
18
+ option :resolved_only, type: :boolean, desc: "XLSX: use resolved comments sheet only"
19
+ option :unresolved_only, type: :boolean, desc: "XLSX: use unresolved comments sheet only"
20
+ def import(input_file)
17
21
  output_yaml = options[:output]
18
22
  schema_dir = options[:schema_dir]
19
23
 
20
24
  # Ensure schema directory exists
21
25
  FileUtils.mkdir_p(schema_dir) unless Dir.exist?(schema_dir)
22
26
 
23
- # Parse the DOCX file
27
+ # Parse the input file
24
28
  parser = Parser.new
25
- comment_sheet = parser.parse(input_docx, options)
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"
26
33
 
27
34
  # Write the YAML data file with schema reference
28
- yaml_content = generate_yaml_with_header(comment_sheet.to_yaml_h, schema_dir)
35
+ yaml_content = generate_yaml_with_header(comment_sheet.to_yaml_h, schema_dir, schema_name)
29
36
  File.write(output_yaml, yaml_content)
30
37
 
31
38
  # Copy schema file to output directory
32
- schema_source = File.join(__dir__, "../../schema/iso_comment_2012-03.yaml")
33
- schema_target = File.join(schema_dir, "iso_comment_2012-03.yaml")
39
+ schema_source = File.join(__dir__, "../../schema/#{schema_name}")
40
+ schema_target = File.join(schema_dir, schema_name)
34
41
 
35
42
  # Only copy if source and target are different
36
- unless File.expand_path(schema_source) == File.expand_path(schema_target)
37
- FileUtils.cp(schema_source,
38
- schema_target)
39
- end
43
+ FileUtils.cp(schema_source, schema_target) unless File.expand_path(schema_source) == File.expand_path(schema_target)
40
44
 
41
- puts "Converted #{input_docx} to #{output_yaml}"
42
- puts "Schema file created at #{schema_target}"
45
+ puts "Converted #{input_file} to #{output_yaml}"
46
+ puts " Version: #{comment_sheet.version}"
47
+ puts " Comments: #{comment_sheet.comments.length}"
48
+ puts " Schema: #{schema_target}"
43
49
  end
44
50
 
45
51
  desc "fill INPUT.yaml", "Fill DOCX template from YAML comments"
@@ -209,8 +215,8 @@ module Commenter
209
215
 
210
216
  private
211
217
 
212
- def generate_yaml_with_header(data, schema_dir)
213
- schema_path = File.join(schema_dir, "iso_comment_2012-03.yaml")
218
+ def generate_yaml_with_header(data, schema_dir, schema_name = "iso_comment_2012-03.yaml")
219
+ schema_path = File.join(schema_dir, schema_name)
214
220
  header = "# yaml-language-server: $schema=#{schema_path}\n\n"
215
221
  header + data.to_yaml
216
222
  end
@@ -2,7 +2,9 @@
2
2
 
3
3
  module Commenter
4
4
  class Comment
5
- attr_accessor :id, :body, :locality, :type, :comments, :proposed_change, :observations, :github
5
+ attr_accessor :id, :body, :locality, :type, :comments, :proposed_change, :observations, :github,
6
+ :user_name, :comment_type, :resolution_status, :resolution_date, :feedbacks,
7
+ :motivation, :created_date, :stage_code
6
8
 
7
9
  def initialize(attributes = {})
8
10
  # Normalize input to symbols
@@ -16,8 +18,21 @@ module Commenter
16
18
  @proposed_change = attrs[:proposed_change]
17
19
  @observations = attrs[:observations]
18
20
  @github = symbolize_keys(attrs[:github] || {})
21
+
22
+ # OSD-specific fields
23
+ @user_name = attrs[:user_name]
24
+ @comment_type = attrs[:comment_type]
25
+ @resolution_status = attrs[:resolution_status]
26
+ @resolution_date = attrs[:resolution_date]
27
+ @feedbacks = attrs[:feedbacks]
28
+ @motivation = attrs[:motivation]
29
+ @created_date = attrs[:created_date]
30
+ @stage_code = attrs[:stage_code]
19
31
  end
20
32
 
33
+ # Expands short type codes to full names. One-way by design: already
34
+ # expanded values pass through unchanged so that reloading YAML output
35
+ # stays stable.
21
36
  def expand_comment_type(type)
22
37
  case type&.downcase
23
38
  when "ge" then "general"
@@ -117,6 +132,14 @@ module Commenter
117
132
  comments: @comments,
118
133
  proposed_change: @proposed_change,
119
134
  observations: @observations,
135
+ user_name: @user_name,
136
+ comment_type: @comment_type,
137
+ resolution_status: @resolution_status,
138
+ resolution_date: @resolution_date,
139
+ feedbacks: @feedbacks,
140
+ motivation: @motivation,
141
+ created_date: @created_date,
142
+ stage_code: @stage_code,
120
143
  github: @github.empty? ? nil : @github
121
144
  }.compact
122
145
  end
@@ -4,7 +4,7 @@ require_relative "comment"
4
4
 
5
5
  module Commenter
6
6
  class CommentSheet
7
- attr_accessor :version, :date, :document, :project, :stage, :comments
7
+ attr_accessor :version, :date, :document, :project, :stage, :comments, :title_en, :title_fr
8
8
 
9
9
  def initialize(attributes = {})
10
10
  # Normalize input to symbols
@@ -15,6 +15,8 @@ module Commenter
15
15
  @document = attrs[:document]
16
16
  @project = attrs[:project]
17
17
  @stage = attrs[:stage]
18
+ @title_en = attrs[:title_en]
19
+ @title_fr = attrs[:title_fr]
18
20
  @comments = (attrs[:comments] || []).map { |c| c.is_a?(Comment) ? c : Comment.from_hash(c) }
19
21
  end
20
22
 
@@ -29,12 +31,18 @@ module Commenter
29
31
  document: @document,
30
32
  project: @project,
31
33
  stage: @stage,
34
+ title_en: @title_en,
35
+ title_fr: @title_fr,
32
36
  comments: @comments.map(&:to_h)
33
37
  }
34
38
  end
35
39
 
36
40
  def to_yaml_h
37
- stringify_keys(to_h.merge(comments: @comments.map(&:to_yaml_h)))
41
+ hash = to_h.merge(comments: @comments.map(&:to_yaml_h))
42
+ # Remove nil-valued keys for cleaner YAML output
43
+ hash.delete(:title_en) if hash[:title_en].nil?
44
+ hash.delete(:title_fr) if hash[:title_fr].nil?
45
+ stringify_keys(hash)
38
46
  end
39
47
 
40
48
  def self.from_hash(hash)