kettle-changelog 1.0.0 → 1.0.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: 470b4a1221d494c5197d12fa669c5c2b554cbc71a66c2bc15d4d46a1ccaaef8a
4
- data.tar.gz: 59f8f87524474affe14bb3c4a4907268af1ce1205a79679e521bd5e6a225b624
3
+ metadata.gz: 8737c49a9337bff6a8234bea1914fd8a054ca66a9fe1107b020cc80ef52205d2
4
+ data.tar.gz: e15afde1f514972d8417785a657c1a0b5c618fd3edca8a4e5be3c4f59aa7ff75
5
5
  SHA512:
6
- metadata.gz: ea215da6524469a17c0ead84eb652377de8ecd31eac9382b4c8012381d60a62b254b34719a03c02d96eedb6184785c1510d5a246cc3fe3b44b4439edba396cbf
7
- data.tar.gz: 17ba27fb850c28d715b553740636a3f6a4db3ea7fd5a0bfa225867d7336c5f26342b8a486f6ff23f857f6da46616f8e8f751b8d29d49d6edf54bbac0e1c78351
6
+ metadata.gz: 8afab816b2675e3ca97449dd50176be20d4ec15723fa4107ccb98f6a6d0c8779467b823f5f472c569d9b856ba6229217c2e5ca7b5e2d300058453a1d79182197
7
+ data.tar.gz: fd565677b152771afcc28db39a541c061fe58d026c3dd9f571d97cab4801dd8d7b9365678c7fc8d23f5c5340882cce55c977c69e58ff5c109e1bbb3f85f19e28
checksums.yaml.gz.sig CHANGED
Binary file
data/CHANGELOG.md CHANGED
@@ -28,10 +28,23 @@ Please file a bug if you notice a violation of semantic versioning.
28
28
 
29
29
  ### Fixed
30
30
 
31
- - Preserve family-local dependency wiring during aggregate coverage runs.
32
-
33
31
  ### Security
34
32
 
33
+ ## [1.0.1] - 2026-08-11
34
+
35
+ - TAG: [v1.0.1][1.0.1t]
36
+ - COVERAGE: 93.56% -- 944/1009 lines in 5 files
37
+ - BRANCH COVERAGE: 77.39% -- 332/429 branches in 5 files
38
+ - 42.31% documented
39
+
40
+ ### Added
41
+
42
+ - Expose structural transfer-entry merging for consumers that replay kettle-jem template history without duplicating Markdown traversal.
43
+
44
+ ### Fixed
45
+
46
+ - Preserve family-local dependency wiring during aggregate coverage runs.
47
+
35
48
  ## [1.0.0] - 2026-08-10
36
49
 
37
50
  - TAG: [v1.0.0][1.0.0t]
@@ -114,6 +127,8 @@ Please file a bug if you notice a violation of semantic versioning.
114
127
 
115
128
  - Initial release
116
129
 
117
- [Unreleased]: https://github.com/kettle-dev/kettle-changelog/compare/v1.0.0...HEAD
130
+ [Unreleased]: https://github.com/kettle-dev/kettle-changelog/compare/v1.0.1...HEAD
131
+ [1.0.1]: https://github.com/kettle-dev/kettle-changelog/compare/v1.0.0...v1.0.1
132
+ [1.0.1t]: https://github.com/kettle-dev/kettle-changelog/releases/tag/v1.0.1
118
133
  [1.0.0]: https://github.com/kettle-dev/kettle-changelog/compare/v0.1.0...v1.0.0
119
134
  [1.0.0t]: https://github.com/kettle-dev/kettle-changelog/releases/tag/v1.0.0
data/README.md CHANGED
@@ -395,7 +395,7 @@ Thanks for RTFM. ☺️
395
395
  [📌gitmoji]: https://gitmoji.dev
396
396
  [📌gitmoji-img]: https://img.shields.io/badge/gitmoji_commits-%20%F0%9F%98%9C%20%F0%9F%98%8D-34495e.svg?style=flat-square
397
397
  [🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
398
- [🧮kloc-img]: https://img.shields.io/badge/KLOC-0.866-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
398
+ [🧮kloc-img]: https://img.shields.io/badge/KLOC-1.009-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
399
399
  [🔐security]: https://github.com/kettle-dev/kettle-changelog/blob/main/SECURITY.md
400
400
  [🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
401
401
  [📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
@@ -0,0 +1,227 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kettle
4
+ module Changelog
5
+ class TransferMerger
6
+ STANDARD_HEADINGS = %w[Added Changed Deprecated Removed Fixed Security].freeze
7
+
8
+ def self.apply(content:, entries:, excluded_keys: [])
9
+ new(content: content, entries: entries, excluded_keys: excluded_keys).run
10
+ end
11
+
12
+ def initialize(content:, entries:, excluded_keys: [])
13
+ @content = content.to_s
14
+ @entries = Array(entries).filter_map do |entry|
15
+ next unless entry.is_a?(Hash)
16
+
17
+ key = entry_value(entry, :key).to_s
18
+ lines = Array(entry_value(entry, :lines))
19
+ next if key.empty? || lines.empty?
20
+
21
+ {
22
+ key: key,
23
+ section: entry_value(entry, :section),
24
+ lines: lines
25
+ }
26
+ end
27
+ @excluded_keys = Array(excluded_keys).map(&:to_s).reject(&:empty?).to_set
28
+ end
29
+
30
+ def run
31
+ normalized = remove_excluded_entries(@content)
32
+ existing_keys = transfer_occurrences(normalized).map { |occurrence| occurrence.fetch(:key) }.to_set
33
+ missing = @entries.reject { |entry| existing_keys.include?(entry.fetch(:key).to_s) }
34
+ return ensure_trailing_newline(normalized) if missing.empty?
35
+
36
+ lines = source_lines(normalized)
37
+ unreleased = unreleased_line_index(lines)
38
+ return ensure_trailing_newline(normalized) unless unreleased
39
+
40
+ destination_end = unreleased_end_index(lines, unreleased)
41
+ destination_body = lines[(unreleased + 1)...destination_end] || []
42
+ items = changelog_items(destination_body)
43
+ missing.group_by { |entry| entry.fetch(:section, "### Changed") }.each do |section, section_entries|
44
+ section = STANDARD_HEADINGS.include?(section.to_s.delete_prefix("### ")) ? "### #{section.to_s.delete_prefix("### ").strip}" : "### Changed"
45
+ items[section] ||= []
46
+ items[section].pop while items[section].any? && items[section].last.to_s.strip.empty?
47
+ items[section] << "" if items[section].any?
48
+ section_entries.each do |entry|
49
+ items[section].concat(Array(entry.fetch(:lines)).map(&:rstrip))
50
+ end
51
+ end
52
+
53
+ merged_lines = lines[0...unreleased] +
54
+ build_unreleased_section(lines.fetch(unreleased), items) +
55
+ lines[destination_end..].to_a
56
+ ensure_trailing_newline(merged_lines.join("\n").gsub(/\n{3,}/, "\n\n"))
57
+ end
58
+
59
+ private
60
+
61
+ def remove_excluded_entries(content)
62
+ return content if @excluded_keys.empty?
63
+
64
+ lines = source_lines(content)
65
+ removals = transfer_occurrences(content).filter_map do |occurrence|
66
+ next unless @excluded_keys.include?(occurrence.fetch(:key).to_s)
67
+
68
+ (occurrence.fetch(:start_line)..occurrence.fetch(:end_line))
69
+ end
70
+ return content if removals.empty?
71
+
72
+ lines.each_with_index.reject do |_line, index|
73
+ removals.any? { |range| range.cover?(index + 1) }
74
+ end.map(&:first).join("\n").then { |text| ensure_trailing_newline(text.gsub(/\n{3,}/, "\n\n")) }
75
+ end
76
+
77
+ def transfer_occurrences(content)
78
+ context = markdown_context(content)
79
+ release_sections = context.structural_owners(owner_scope: :heading_sections)
80
+ .select { |owner| owner.level == 2 }
81
+ .sort_by { |owner| owner.location.start_line }
82
+ list_items = context.structural_owners(owner_scope: :list_items)
83
+ list_items.filter_map do |item|
84
+ next unless item.depth == 1
85
+
86
+ key = transfer_key(item.source)
87
+ next unless key
88
+
89
+ release = release_section_for(release_sections, item)
90
+ next unless release
91
+
92
+ {
93
+ key: key,
94
+ release_heading: "## #{release.heading_text}",
95
+ start_line: item.location.start_line,
96
+ end_line: item.location.end_line
97
+ }
98
+ end
99
+ end
100
+
101
+ def transfer_key(source)
102
+ source.to_s.lines.first.to_s.strip[/\A[-*] +((?:kettle-jem-template)-\d{8}-\d{3})\b/, 1]
103
+ end
104
+
105
+ def release_section_for(sections, item)
106
+ sections.each_with_index.find do |section, index|
107
+ next_section = sections[index + 1]
108
+ section_end_line = next_section ? next_section.location.start_line - 1 : section.location.end_line
109
+ item.location.start_line > section.location.start_line && item.location.end_line <= section_end_line
110
+ end&.first
111
+ end
112
+
113
+ def changelog_items(body_lines)
114
+ items = {}
115
+ heading = nil
116
+ index = 0
117
+ while index < body_lines.length
118
+ line = body_lines.fetch(index)
119
+ if line.start_with?("### ")
120
+ heading = line.strip
121
+ items[heading] ||= []
122
+ index += 1
123
+ next
124
+ end
125
+
126
+ if bullet_line?(line)
127
+ collected, index = collect_list_item(body_lines, index)
128
+ items[heading] ||= []
129
+ items[heading].concat(collected)
130
+ next
131
+ end
132
+
133
+ index += 1
134
+ end
135
+ items
136
+ end
137
+
138
+ def build_unreleased_section(heading, items)
139
+ lines = [heading]
140
+ STANDARD_HEADINGS.each do |standard_heading|
141
+ heading_line = "### #{standard_heading}"
142
+ lines << ""
143
+ lines << heading_line
144
+ lines << ""
145
+ section_items = items[heading_line].to_a.dup
146
+ section_items.pop while section_items.any? && section_items.last.to_s.strip.empty?
147
+ lines.concat(section_items) if section_items.any?
148
+ end
149
+ lines << ""
150
+ end
151
+
152
+ def source_lines(content)
153
+ content.to_s.split("\n")
154
+ end
155
+
156
+ def unreleased_line_index(lines)
157
+ lines.index { |line| line.to_s.strip == "## [Unreleased]" }
158
+ end
159
+
160
+ def unreleased_end_index(lines, unreleased_index)
161
+ index = unreleased_index + 1
162
+ while index < lines.length
163
+ line = lines.fetch(index)
164
+ return index if link_reference_line?(line)
165
+ return index if line.start_with?("# ") || (line.start_with?("## ") && line.to_s.strip != "## [Unreleased]")
166
+
167
+ index += 1
168
+ end
169
+ lines.length
170
+ end
171
+
172
+ def link_reference_line?(line)
173
+ line.to_s.lstrip.start_with?("[") && line.to_s.include?("]:")
174
+ end
175
+
176
+ def entry_value(entry, key)
177
+ entry[key] || entry[key.to_s]
178
+ end
179
+
180
+ def bullet_line?(line)
181
+ line.to_s.lstrip.start_with?("- ", "* ")
182
+ end
183
+
184
+ def collect_list_item(lines, start_index)
185
+ line = lines.fetch(start_index).to_s
186
+ base_indent = line.length - line.lstrip.length
187
+ item_lines = [line.rstrip]
188
+ index = start_index + 1
189
+ in_fence = false
190
+ while index < lines.length
191
+ current = lines.fetch(index).to_s
192
+ current_indent = current.length - current.lstrip.length
193
+ break if !in_fence && heading_line?(current)
194
+ break if !in_fence && bullet_line?(current) && current_indent <= base_indent
195
+
196
+ if current.lstrip.start_with?("```")
197
+ in_fence = !in_fence
198
+ item_lines << current.rstrip
199
+ index += 1
200
+ next
201
+ end
202
+
203
+ break unless in_fence || current.strip.empty? || current_indent > base_indent
204
+
205
+ item_lines << current.rstrip
206
+ index += 1
207
+ end
208
+ [item_lines, index]
209
+ end
210
+
211
+ def heading_line?(line)
212
+ line.to_s.lstrip.start_with?("#")
213
+ end
214
+
215
+ def markdown_context(content)
216
+ require "ast/crispr/markdown/markly"
217
+ Ast::Crispr::Markdown::Markly.document_context(content: content, source_label: "CHANGELOG.md")
218
+ rescue LoadError => error
219
+ raise Error, "kettle-changelog transfer merging requires ast-crispr-markdown-markly (#{error.message})"
220
+ end
221
+
222
+ def ensure_trailing_newline(content)
223
+ "#{content.to_s.rstrip}\n"
224
+ end
225
+ end
226
+ end
227
+ end
@@ -5,7 +5,7 @@ module Kettle
5
5
  # Version namespace for this gem.
6
6
  module Version
7
7
  # Current gem version.
8
- VERSION = "1.0.0"
8
+ VERSION = "1.0.1"
9
9
  end
10
10
  # Current gem version exposed at the traditional constant location.
11
11
  VERSION = Version::VERSION # Traditional Constant Location
@@ -10,6 +10,7 @@ module Kettle
10
10
 
11
11
  autoload :CLI, "kettle/changelog/cli"
12
12
  autoload :EntryAdder, "kettle/changelog/entry_adder"
13
+ autoload :TransferMerger, "kettle/changelog/transfer_merger"
13
14
  end
14
15
  end
15
16
 
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kettle-changelog
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter H. Boling
@@ -295,6 +295,7 @@ files:
295
295
  - lib/kettle/changelog.rb
296
296
  - lib/kettle/changelog/cli.rb
297
297
  - lib/kettle/changelog/entry_adder.rb
298
+ - lib/kettle/changelog/transfer_merger.rb
298
299
  - lib/kettle/changelog/version.rb
299
300
  - sig/kettle/changelog.rbs
300
301
  homepage: https://github.com/kettle-dev/kettle-changelog
@@ -302,10 +303,10 @@ licenses:
302
303
  - AGPL-3.0-only
303
304
  metadata:
304
305
  homepage_uri: https://kettle-changelog.galtzo.com
305
- source_code_uri: https://github.com/kettle-dev/kettle-changelog/tree/v1.0.0
306
- changelog_uri: https://github.com/kettle-dev/kettle-changelog/blob/v1.0.0/CHANGELOG.md
306
+ source_code_uri: https://github.com/kettle-dev/kettle-changelog/tree/v1.0.1
307
+ changelog_uri: https://github.com/kettle-dev/kettle-changelog/blob/v1.0.1/CHANGELOG.md
307
308
  bug_tracker_uri: https://github.com/kettle-dev/kettle-changelog/issues
308
- documentation_uri: https://www.rubydoc.info/gems/kettle-changelog/1.0.0
309
+ documentation_uri: https://www.rubydoc.info/gems/kettle-changelog/1.0.1
309
310
  funding_uri: https://github.com/sponsors/pboling
310
311
  wiki_uri: https://github.com/kettle-dev/kettle-changelog/wiki
311
312
  news_uri: https://www.railsbling.com/tags/kettle-changelog
metadata.gz.sig CHANGED
Binary file