commenter 0.3.0 → 0.3.1

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: 99f441f58237864e8ea0bc5c8682f04d2b15757ef1f28c45317f0181d6621fb4
4
- data.tar.gz: 0a48406aa86377612a2722455dad5013457a10805da311c74c1fbfb82cccafd7
3
+ metadata.gz: 1609bb9eed80e408daebc9c67cda2c657e8484186acce89b823e6b411a5f9ea5
4
+ data.tar.gz: 46316674e2c639b9256390385d2cb3c59ed8946b604b44fbb0be28728475cd38
5
5
  SHA512:
6
- metadata.gz: 7c26ede9e2a9e4f5f060c0b8679cc9afcb36214a4c5b1a2f02c15a44cd6671d7b104954f4cf4b6df3df9f3806ab7c4d0418982a75371805c773c90064b82c8b9
7
- data.tar.gz: 32dd2e31a603391bef5a112d0b10da62b7c29e249bcc76c3e5456aab939015ff51d21036108c3366743db11e01411a2bd285b7992f1a534009d5d4cc184e9d31
6
+ metadata.gz: 887aa19ab8ca3559fd20caf3e19a9941b631c677faeda86553597671883fc8dbae8c1e1f6d83bd0014e036774743addc16bc538b7404f6939be6636e1d623704
7
+ data.tar.gz: 3b850d0872f2f572b599d0bf7a70a977e1e5e7e797f5fb3447905c09cc7578cff18d99de6b32ae3b539758b7319b07590fdfd9d5249f74024ffdcd842d48d09d
data/.rubocop.yml CHANGED
@@ -12,3 +12,7 @@ Style/StringLiteralsInInterpolation:
12
12
  Metrics/ModuleLength:
13
13
  Exclude:
14
14
  - "spec/**/*"
15
+
16
+ Metrics/BlockLength:
17
+ Exclude:
18
+ - "spec/**/*"
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
- - `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.
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/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,10 +111,15 @@ 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 `xlsx`; auto-detected by default)
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
 
@@ -134,7 +163,7 @@ comments:
134
163
  line_number:
135
164
  clause: "_whole document"
136
165
  element:
137
- type: ge
166
+ type: general
138
167
  comments: |
139
168
  The document should include more examples
140
169
  to clarify the implementation requirements.
@@ -150,7 +179,7 @@ comments:
150
179
  line_number: "45"
151
180
  clause: "5.2.1"
152
181
  element: "Table 3"
153
- type: te
182
+ type: technical
154
183
  comments: |
155
184
  The values in Table 3 appear to be
156
185
  inconsistent with the formula in 5.1.
@@ -471,7 +500,7 @@ comments:
471
500
  locality:
472
501
  clause: "5.2.1"
473
502
  element: "Table 3"
474
- type: te
503
+ type: technical
475
504
  comments: "The values in Table 3 appear inconsistent..."
476
505
  proposed_change: "Correct the values in column 2..."
477
506
  observations: ""
data/lib/commenter/cli.rb CHANGED
@@ -3,20 +3,23 @@
3
3
  require "thor"
4
4
  require "yaml"
5
5
  require "fileutils"
6
- require "commenter/parser"
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 xlsx)"
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
24
  output_yaml = options[:output]
22
25
  schema_dir = options[:schema_dir]
@@ -28,14 +31,11 @@ module Commenter
28
31
  parser = Parser.new
29
32
  comment_sheet = parser.parse(input_file, options)
30
33
 
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"
33
-
34
34
  # Write the YAML data file with schema reference
35
- yaml_content = generate_yaml_with_header(comment_sheet.to_yaml_h, schema_dir, schema_name)
36
- File.write(output_yaml, yaml_content)
35
+ File.write(output_yaml, comment_sheet.to_yaml_document(schema_dir))
37
36
 
38
37
  # Copy schema file to output directory
38
+ schema_name = comment_sheet.schema_name
39
39
  schema_source = File.join(__dir__, "../../schema/#{schema_name}")
40
40
  schema_target = File.join(schema_dir, schema_name)
41
41
 
@@ -212,13 +212,5 @@ module Commenter
212
212
  def self.exit_on_failure?
213
213
  true
214
214
  end
215
-
216
- private
217
-
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)
220
- header = "# yaml-language-server: $schema=#{schema_path}\n\n"
221
- header + data.to_yaml
222
- end
223
215
  end
224
216
  end
@@ -13,7 +13,7 @@ module Commenter
13
13
  @id = attrs[:id]
14
14
  @body = attrs[:body]
15
15
  @locality = symbolize_keys(attrs[:locality] || {})
16
- @type = expand_comment_type(attrs[:type])
16
+ @type = CommentType.full_name(attrs[:type])
17
17
  @comments = attrs[:comments]
18
18
  @proposed_change = attrs[:proposed_change]
19
19
  @observations = attrs[:observations]
@@ -30,16 +30,8 @@ module Commenter
30
30
  @stage_code = attrs[:stage_code]
31
31
  end
32
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.
36
33
  def expand_comment_type(type)
37
- case type&.downcase
38
- when "ge" then "general"
39
- when "te" then "technical"
40
- when "ed" then "editorial"
41
- else type
42
- end
34
+ CommentType.full_name(type)
43
35
  end
44
36
 
45
37
  def line_number
@@ -45,6 +45,14 @@ module Commenter
45
45
  stringify_keys(hash)
46
46
  end
47
47
 
48
+ def schema_name
49
+ version == "osd" ? "iso_comment_osd.yaml" : "iso_comment_2012-03.yaml"
50
+ end
51
+
52
+ def to_yaml_document(schema_dir = "schema")
53
+ "# yaml-language-server: $schema=#{File.join(schema_dir.to_s, schema_name)}\n\n#{to_yaml_h.to_yaml}"
54
+ end
55
+
48
56
  def self.from_hash(hash)
49
57
  new(hash)
50
58
  end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Commenter
4
+ # Single home for the comment type vocabulary: short codes (ge/te/ed) and
5
+ # their expanded names (general/technical/editorial). Unrecognized values
6
+ # pass through unchanged; display_name capitalizes recognized types.
7
+ module CommentType
8
+ FULL_NAMES = {
9
+ "ge" => "general",
10
+ "te" => "technical",
11
+ "ed" => "editorial"
12
+ }.freeze
13
+
14
+ CODES = FULL_NAMES.invert.freeze
15
+
16
+ DISPLAY_NAMES = FULL_NAMES.each_with_object({}) do |(code, full), display|
17
+ display[code] = full.capitalize
18
+ display[full] = full.capitalize
19
+ end.freeze
20
+
21
+ module_function
22
+
23
+ def code(type)
24
+ CODES.fetch(type.to_s.strip.downcase) { type&.to_s&.strip }
25
+ end
26
+
27
+ def full_name(type)
28
+ FULL_NAMES.fetch(type.to_s.strip.downcase) { type&.to_s&.strip }
29
+ end
30
+
31
+ def display_name(type)
32
+ DISPLAY_NAMES.fetch(type.to_s.strip.downcase) { type || "Unknown" }
33
+ end
34
+ end
35
+ end
@@ -8,11 +8,10 @@ require "dotenv/load"
8
8
  module Commenter
9
9
  class GitHubIssueCreator
10
10
  def initialize(config_path, title_template_path = nil, body_template_path = nil)
11
- @config = load_config(config_path)
12
- @github_client = create_github_client
13
- @repo = @config.dig("github", "repository")
14
-
15
- raise "GitHub repository not specified in config" unless @repo
11
+ @session = GitHubSession.new(config_path)
12
+ @config = @session.config
13
+ @github_client = @session.client
14
+ @repo = @session.repo
16
15
 
17
16
  @title_template = load_liquid_template(title_template_path || default_title_template_path)
18
17
  @body_template = load_liquid_template(body_template_path || default_body_template_path)
@@ -43,19 +42,6 @@ module Commenter
43
42
 
44
43
  private
45
44
 
46
- def load_config(config_path)
47
- YAML.load_file(config_path)
48
- rescue Errno::ENOENT
49
- raise "Configuration file not found: #{config_path}"
50
- end
51
-
52
- def create_github_client
53
- token = @config.dig("github", "token") || ENV["GITHUB_TOKEN"]
54
- raise "GitHub token not found. Set GITHUB_TOKEN environment variable or specify in config file." unless token
55
-
56
- Octokit::Client.new(access_token: token)
57
- end
58
-
59
45
  def default_title_template_path
60
46
  File.join(__dir__, "../../data/github_issue_title_template.liquid")
61
47
  end
@@ -103,7 +89,7 @@ module Commenter
103
89
  "comment_id" => comment.id || "",
104
90
  "body" => comment.body || "",
105
91
  "type" => comment.type || "",
106
- "type_full_name" => expand_comment_type(comment.type),
92
+ "type_full_name" => CommentType.display_name(comment.type),
107
93
  "comments" => comment.comments || "",
108
94
  "proposed_change" => comment.proposed_change || "",
109
95
  "observations" => comment.observations || "",
@@ -134,15 +120,6 @@ module Commenter
134
120
  @unique_id_template.render(base_variables).strip
135
121
  end
136
122
 
137
- def expand_comment_type(type)
138
- case type&.downcase
139
- when "ge", "general" then "General"
140
- when "te", "technical" then "Technical"
141
- when "ed", "editorial" then "Editorial"
142
- else type || "Unknown"
143
- end
144
- end
145
-
146
123
  def format_locality(comment)
147
124
  parts = []
148
125
  parts << "Clause #{comment.clause}" if comment.clause && !comment.clause.strip.empty?
@@ -314,23 +291,16 @@ module Commenter
314
291
 
315
292
  # Write updated YAML
316
293
  output_file = options[:output] || yaml_file
317
- yaml_content = generate_yaml_with_header(comment_sheet.to_yaml_h)
318
- File.write(output_file, yaml_content)
319
- end
320
-
321
- def generate_yaml_with_header(data)
322
- header = "# yaml-language-server: $schema=schema/iso_comment_2012-03.yaml\n\n"
323
- header + data.to_yaml
294
+ File.write(output_file, comment_sheet.to_yaml_document)
324
295
  end
325
296
  end
326
297
 
327
298
  class GitHubIssueRetriever
328
299
  def initialize(config_path)
329
- @config = load_config(config_path)
330
- @github_client = create_github_client
331
- @repo = @config.dig("github", "repository")
332
-
333
- raise "GitHub repository not specified in config" unless @repo
300
+ @session = GitHubSession.new(config_path)
301
+ @config = @session.config
302
+ @github_client = @session.client
303
+ @repo = @session.repo
334
304
  end
335
305
 
336
306
  def retrieve_observations_from_yaml(yaml_file, options = {})
@@ -357,19 +327,6 @@ module Commenter
357
327
 
358
328
  private
359
329
 
360
- def load_config(config_path)
361
- YAML.load_file(config_path)
362
- rescue Errno::ENOENT
363
- raise "Configuration file not found: #{config_path}"
364
- end
365
-
366
- def create_github_client
367
- token = @config.dig("github", "token") || ENV["GITHUB_TOKEN"]
368
- raise "GitHub token not found. Set GITHUB_TOKEN environment variable or specify in config file." unless token
369
-
370
- Octokit::Client.new(access_token: token)
371
- end
372
-
373
330
  def retrieve_observation(comment, options)
374
331
  issue_number = comment.github_issue_number
375
332
 
@@ -486,13 +443,7 @@ module Commenter
486
443
 
487
444
  def update_yaml_with_observations(yaml_file, comment_sheet, options)
488
445
  output_file = options[:output] || yaml_file
489
- yaml_content = generate_yaml_with_header(comment_sheet.to_yaml_h)
490
- File.write(output_file, yaml_content)
491
- end
492
-
493
- def generate_yaml_with_header(data)
494
- header = "# yaml-language-server: $schema=schema/iso_comment_2012-03.yaml\n\n"
495
- header + data.to_yaml
446
+ File.write(output_file, comment_sheet.to_yaml_document)
496
447
  end
497
448
  end
498
449
  end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "octokit"
4
+ require "yaml"
5
+
6
+ module Commenter
7
+ # Shared connection setup for the GitHub integration commands: loads the
8
+ # configuration file, builds the Octokit client, and resolves the target
9
+ # repository. One constructor, one test surface.
10
+ class GitHubSession
11
+ attr_reader :config, :client, :repo
12
+
13
+ def initialize(config_path)
14
+ @config = load_config(config_path)
15
+ @client = Octokit::Client.new(access_token: required_token)
16
+ @repo = @config.dig("github", "repository")
17
+ raise "GitHub repository not specified in config" unless @repo
18
+ end
19
+
20
+ private
21
+
22
+ def load_config(config_path)
23
+ YAML.load_file(config_path)
24
+ rescue Errno::ENOENT
25
+ raise "Configuration file not found: #{config_path}"
26
+ end
27
+
28
+ def required_token
29
+ token = @config.dig("github", "token") || ENV["GITHUB_TOKEN"]
30
+ raise "GitHub token not found. Set GITHUB_TOKEN environment variable or specify in config file." unless token
31
+
32
+ token
33
+ end
34
+ end
35
+ end
@@ -201,7 +201,7 @@ module Commenter
201
201
  clause: clause.empty? ? nil : clause,
202
202
  element: clause_title.empty? ? nil : clause_title
203
203
  }.compact,
204
- type: normalize_comment_type(cell_value_str(row, col_map[:comment_type]).to_s),
204
+ type: CommentType.code(cell_value_str(row, col_map[:comment_type]).to_s),
205
205
  comments: comment_text.to_s.strip,
206
206
  proposed_change: cell_value_str(row, col_map[:proposed_change]) ||
207
207
  cell_value_str(row, col_map[:proposal_on_text])
@@ -227,15 +227,6 @@ module Commenter
227
227
  parts.empty? ? nil : parts.join(". ")
228
228
  end
229
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
230
  def cell_value(row, index)
240
231
  return nil if index.nil? || row.length <= index
241
232