yaml-merge 7.1.6 → 7.1.8

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: e1d6e4b41e159981a900735e298bfe8091566e9c639ce4f394a6528718659484
4
- data.tar.gz: '003499bdb2ece6e5022aed479b61f0655574b451df095f2fc465275030d1d2d9'
3
+ metadata.gz: a4fc343ed5a3cdb282f7e215a62d0754a4b0e1908959582f09f871af039771f2
4
+ data.tar.gz: 39f6895dc4d2066f488437744156357ad40cdfb2334030bc37ed652e6c7a2b56
5
5
  SHA512:
6
- metadata.gz: ecf5a8c23fb8458021586437968824c11263b0dfb823c58ebd6718d19c5fa2b7ea812f5a3874bd85659933b139ef114d5c05ea1b05b1c15ff6c703a2eae41595
7
- data.tar.gz: 8a9abe6a47707384e1f0c3ef5b34791c3ce9eeb81f7f01f8347839fc0d4b4d0842f91b09a3733b7af5d3b0f19126fa97402035172f90a6057aa180fc73052445
6
+ metadata.gz: 077125af08b9a5547bbd434776db9ca73f0d88fd706583bb1d296e213b77456ab1760abd0cca5f6b060dc87b735becc1963b49d28bcf6a4ae8de90d78a54d16c
7
+ data.tar.gz: 197d2a7cbc7d0dda453d1ab1676ca4e91013c6f74f8378e23113376d7c66a3269be3544373df6be5e62d88cbad8b152a94658a22838e8c4485ab6889ec39f316
checksums.yaml.gz.sig CHANGED
Binary file
data/README.md CHANGED
@@ -22,8 +22,9 @@ I've summarized my thoughts in [this blog post](https://dev.to/galtzo/hostile-ta
22
22
  ## 🌻 Synopsis <a href="https://discord.gg/3qme4XHNKN"><img alt="Galtzo FLOSS Logo by Aboling0, CC BY-SA 4.0" src="https://logos.galtzo.com/assets/images/galtzo-floss/avatar-128px.svg" width="8%" align="right"/></a> <a href="https://ruby-toolbox.com"><img alt="ruby-lang Logo, Yukihiro Matsumoto, Ruby Visual Identity Team, CC BY-SA 2.5" src="https://logos.galtzo.com/assets/images/ruby-lang/avatar-128px.svg" width="8%" align="right"/></a>
23
23
 
24
24
  `Yaml::Merge` is the canonical YAML family package. It is the YAML substrate for
25
- StructuredMerge Ruby: it registers the tree-sitter YAML path and should own the
26
- shared YAML merge behavior used by provider implementations. YAML parsing and
25
+ StructuredMerge Ruby: it registers the TSLP YAML path and owns the shared
26
+ source-preserving provider behavior used by parser-specific implementations.
27
+ YAML parsing and
27
28
  backend selection enter through `tree_haver`; source-preserving
28
29
  partial document insertion, replacement, and removal should enter through
29
30
  `ast-crispr`; merge orchestration should use `ast-merge`.
@@ -39,6 +40,7 @@ Hash/Array round-trips.
39
40
  - Backend feature profiles for the shared language-pack parser.
40
41
  - Path-based owner matching for mapping entries.
41
42
  - Shared YAML-family merge semantics for provider gems.
43
+ - A TSLP workflow provider and a reusable provider engine for native parsers.
42
44
  - Destination-wins array policy.
43
45
  - Hash result API for fixture runners and higher-level tools.
44
46
 
@@ -0,0 +1,168 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yaml
4
+ # YAML workflow provider integration.
5
+ module Merge
6
+ # TSLP projection for the shared YAML source-preserving provider.
7
+ # rubocop:disable Metrics/AbcSize, Metrics/ClassLength, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/ParameterLists, Metrics/PerceivedComplexity -- tree-sitter YAML validation and projection remain one adapter boundary
8
+ class Provider < SourcePreservingProvider
9
+ PROVIDER_ID = 'ruby.yaml'
10
+ BACKEND = BACKEND_REFERENCE
11
+ PACKAGE_NAME = Merge::PACKAGE_NAME
12
+ PACKAGE_VERSION = Version::VERSION
13
+ ROLE = :workflow
14
+
15
+ private
16
+
17
+ def analyze_role(role, source)
18
+ analysis = TreeHaver.with_backend(backend_id) { FileAnalysis.new(source) }
19
+ return invalid_analysis(role, source, analysis) unless analysis.valid?
20
+
21
+ documents = analysis.documents
22
+ issues = []
23
+ issues << issue(:multiple_documents, 'YAML input must contain exactly one document') unless documents.one?
24
+ document = documents.first
25
+ root = document&.body_node
26
+ issues << issue(:non_mapping_root, 'YAML document root must be a mapping') unless root&.mapping?
27
+ issues.concat(node_issues(root)) if root
28
+
29
+ parsed = Yaml::Merge.parse_yaml(source, 'yaml', backend: backend_id)
30
+ unless parsed[:ok]
31
+ diagnostic = parsed.fetch(:diagnostics).first || {}
32
+ return invalid_document(role, source, diagnostic[:category] || :parse_error, diagnostic[:message])
33
+ end
34
+
35
+ entries = root&.mapping? ? mapping_entries(analysis, root, parsed.dig(:analysis, :document), source, role) : []
36
+ duplicate = entries.group_by(&:key).find { |_key, matches| matches.length > 1 }
37
+ issues << issue(:duplicate_key, "Duplicate YAML key #{duplicate.first}", key: duplicate.first) if duplicate
38
+
39
+ Document.new(
40
+ role: role,
41
+ source: source,
42
+ analysis: analysis,
43
+ entries: entries.freeze,
44
+ by_key: entries.to_h { |entry| [entry.key, entry] }.freeze,
45
+ issues: issues.uniq.freeze,
46
+ document_attributes: document ? node_attributes(document) : {}
47
+ )
48
+ rescue TreeHaver::Error => e
49
+ invalid_document(role, source, :parse_error, e.message)
50
+ rescue StandardError => e
51
+ invalid_document(role, source, :structural_error, e.message)
52
+ end
53
+
54
+ def invalid_analysis(role, source, analysis)
55
+ message = analysis.errors.map(&:to_s).join('; ')
56
+ invalid_document(role, source, :parse_error, message.empty? ? 'YAML parse failed' : message)
57
+ end
58
+
59
+ def node_issues(root)
60
+ issues = []
61
+ walk_nodes(root.node) do |node|
62
+ type = node.type.to_s
63
+ issues << issue(:alias, 'YAML aliases require an exact one-sided merge') if type == 'alias'
64
+ issues << issue(:anchor, 'YAML anchors require an exact one-sided merge') if type == 'anchor'
65
+ issues << issue(:directive, 'YAML directives require an exact one-sided merge') if type.include?('directive')
66
+ if %w[flow_mapping flow_sequence].include?(type)
67
+ issues << issue(:flow_collection, 'Flow-style collections require an exact one-sided merge')
68
+ end
69
+ end
70
+ issues.uniq.freeze
71
+ end
72
+
73
+ def walk_nodes(node, &block)
74
+ yield node
75
+ node.children.each { |child| walk_nodes(child, &block) }
76
+ end
77
+
78
+ def mapping_entries(analysis, root, semantic_document, source, role)
79
+ root.mapping_pairs.filter_map.with_index do |pair, index|
80
+ key = pair.key_name
81
+ next invalid_top_level_entry(pair, source, role, index) unless key
82
+
83
+ build_entry(analysis, pair, key, semantic_document[key], source, role, index)
84
+ end
85
+ end
86
+
87
+ def invalid_top_level_entry(pair, source, role, index)
88
+ Entry.new(
89
+ key: "<complex-key-#{index}>",
90
+ start_line: pair.start_line,
91
+ end_line: pair.end_line,
92
+ fragment: source_lines(source, pair.start_line, pair.end_line),
93
+ semantic: nil,
94
+ attributes: node_attributes(pair.value_node),
95
+ role: role,
96
+ ownership_provable: false
97
+ )
98
+ end
99
+
100
+ def build_entry(analysis, pair, key, semantic, source, role, index)
101
+ key_line = pair.start_line
102
+ value_end = pair.end_line
103
+ leading_start = owned_leading_start(analysis, pair, index, key_line)
104
+ Entry.new(
105
+ key: key,
106
+ start_line: leading_start,
107
+ end_line: value_end,
108
+ fragment: source_lines(source, leading_start, value_end),
109
+ semantic: semantic,
110
+ attributes: node_attributes(pair.value_node),
111
+ role: role,
112
+ ownership_provable: ownership_provable?(analysis, pair, index, key_line, leading_start)
113
+ )
114
+ end
115
+
116
+ def owned_leading_start(analysis, pair, index, key_line)
117
+ return key_line if index.zero?
118
+
119
+ region = analysis.comment_attachment_for(pair)&.leading_region
120
+ return key_line unless region && region.end_line == key_line - 1
121
+ return key_line if region.metadata[:floating]
122
+
123
+ region.start_line
124
+ end
125
+
126
+ def ownership_provable?(analysis, pair, index, key_line, leading_start)
127
+ return true if leading_start == key_line
128
+ return false if index.zero?
129
+
130
+ region = analysis.comment_attachment_for(pair)&.leading_region
131
+ region && !region.metadata[:floating] && region.start_line == leading_start
132
+ end
133
+
134
+ def node_attributes(node)
135
+ return {} unless node
136
+
137
+ {
138
+ type: node.type.to_s,
139
+ canonical_type: node.type.to_s,
140
+ signature: public_value(node.signature),
141
+ children: node.semantic_children.map do |child|
142
+ node_attributes(NodeWrapper.new(child, lines: node.lines, source: node.source))
143
+ end
144
+ }
145
+ end
146
+
147
+ def public_value(value)
148
+ case value
149
+ when Array then value.map { |item| public_value(item) }
150
+ when Hash then value.to_h { |key, item| [key.to_s, public_value(item)] }
151
+ when String, Integer, Float, TrueClass, FalseClass, NilClass then value
152
+ else value.to_s
153
+ end
154
+ end
155
+ end
156
+ # rubocop:enable Metrics/AbcSize, Metrics/ClassLength, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/ParameterLists, Metrics/PerceivedComplexity
157
+
158
+ class << self
159
+ def merge_provider
160
+ @merge_provider ||= Provider.new
161
+ end
162
+
163
+ def register_provider!(replace: false)
164
+ Ast::Merge.register_provider(merge_provider, replace: replace)
165
+ end
166
+ end
167
+ end
168
+ end
@@ -125,7 +125,7 @@ module Yaml
125
125
 
126
126
  def merge_matched_nodes(template_node, dest_node, template_analysis, dest_analysis, template_owners: nil,
127
127
  dest_owners: nil, template_boundary_line: nil, dest_boundary_line: nil)
128
- if template_node.document? && dest_node.document?
128
+ if node_kind?(template_node, :document?) && node_kind?(dest_node, :document?)
129
129
  dest_header_end = emit_document_header(dest_node, dest_analysis)
130
130
  template_header_end = document_header_end_line(template_node)
131
131
  merge_node_lists(
@@ -136,12 +136,12 @@ module Yaml
136
136
  template_boundary_line: template_header_end,
137
137
  dest_boundary_line: dest_header_end
138
138
  )
139
- elsif template_node.mapping_pair? && dest_node.mapping_pair?
139
+ elsif node_kind?(template_node, :mapping_pair?) && node_kind?(dest_node, :mapping_pair?)
140
140
  merge_mapping_pair(template_node, dest_node, template_analysis, dest_analysis, template_owners: template_owners,
141
141
  dest_owners: dest_owners,
142
142
  template_boundary_line: template_boundary_line,
143
143
  dest_boundary_line: dest_boundary_line)
144
- elsif template_node.mapping? && dest_node.mapping?
144
+ elsif node_kind?(template_node, :mapping?) && node_kind?(dest_node, :mapping?)
145
145
  merge_node_lists(
146
146
  template_node.mergeable_children,
147
147
  dest_node.mergeable_children,
@@ -167,6 +167,10 @@ module Yaml
167
167
  end
168
168
  end
169
169
 
170
+ def node_kind?(node, predicate)
171
+ node.respond_to?(predicate) && node.public_send(predicate)
172
+ end
173
+
170
174
  def merge_mapping_pair(template_node, dest_node, template_analysis, dest_analysis, template_owners: nil,
171
175
  dest_owners: nil, template_boundary_line: nil, dest_boundary_line: nil)
172
176
  template_value = template_node.value_node&.unwrap_value_node
@@ -0,0 +1,799 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'digest'
4
+
5
+ module Yaml
6
+ # Parser-neutral YAML structural merge provider integration.
7
+ module Merge
8
+ # Conservative source-preserving provider for top-level YAML mappings.
9
+ # rubocop:disable Metrics/AbcSize, Metrics/ClassLength, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/ParameterLists, Metrics/PerceivedComplexity -- provider validation, planning, rendering, and verification form one safety boundary
10
+ class SourcePreservingProvider
11
+ DEFAULT_PROFILE = :source_preserving
12
+ DIALECTS = %i[yaml].freeze
13
+
14
+ Entry = Data.define(
15
+ :key,
16
+ :start_line,
17
+ :end_line,
18
+ :fragment,
19
+ :semantic,
20
+ :attributes,
21
+ :role,
22
+ :ownership_provable
23
+ )
24
+ Document = Data.define(
25
+ :role,
26
+ :source,
27
+ :analysis,
28
+ :entries,
29
+ :by_key,
30
+ :issues,
31
+ :document_attributes
32
+ )
33
+ Decision = Data.define(:key, :base, :ours, :theirs, :selected_role, :classification, :conflict)
34
+
35
+ def provider_id = self.class::PROVIDER_ID
36
+ def family = 'yaml'
37
+ def backend_reference = self.class::BACKEND
38
+ def backend_id = backend_reference.id.to_sym
39
+ def package_name = self.class::PACKAGE_NAME
40
+ def package_version = self.class::PACKAGE_VERSION
41
+ def role = self.class.const_defined?(:ROLE, false) ? self.class::ROLE : :backend
42
+
43
+ def capabilities
44
+ {
45
+ operations: Ast::Merge::ProviderContract::OPERATIONS,
46
+ dialects: DIALECTS,
47
+ backends: [backend_id],
48
+ profiles: [DEFAULT_PROFILE],
49
+ role: role,
50
+ source_preservation: %i[
51
+ exact_source
52
+ mapping_entry_fragments
53
+ comment_ownership
54
+ line_provenance
55
+ reparse
56
+ semantic_verification
57
+ ast_attribute_verification
58
+ ]
59
+ }.freeze
60
+ end
61
+
62
+ def analyze(request)
63
+ document = analyze_role(:source, request.fetch(:source))
64
+ return invalid_document_failure(:analyze, request, [document]) unless document.issues.empty?
65
+
66
+ result(
67
+ :analyze,
68
+ request,
69
+ analysis: analysis_payload(document),
70
+ extensions: analysis_extensions(document),
71
+ verification: { source_parsed: true, structurally_unambiguous: true }
72
+ )
73
+ end
74
+
75
+ def diff2(request)
76
+ before = analyze_role(:before, request.fetch(:before_source))
77
+ after = analyze_role(:after, request.fetch(:after_source))
78
+ unsafe = [before, after].reject { |document| document.issues.empty? }
79
+ return invalid_document_failure(:diff2, request, unsafe) unless unsafe.empty?
80
+
81
+ changes = mapping_changes(before, after)
82
+ result(
83
+ :diff2,
84
+ request,
85
+ diff: { before: analysis_payload(before), after: analysis_payload(after), changes: changes },
86
+ changes: changes,
87
+ verification: { before_parsed: true, after_parsed: true }
88
+ )
89
+ end
90
+
91
+ def merge2(request)
92
+ documents = {
93
+ incoming: analyze_role(:incoming, request.fetch(:incoming_source)),
94
+ current: analyze_role(:current, request.fetch(:current_source))
95
+ }
96
+ unsafe = documents.values.reject { |document| document.issues.empty? }
97
+ return invalid_document_failure(:merge2, request, unsafe) unless unsafe.empty?
98
+ return render_exact_merge2(request, documents) if documents[:incoming].source == documents[:current].source
99
+
100
+ merged = recursive_merge2(documents)
101
+ rendered = render_recursive_merge2(request, documents, merged)
102
+ verification = verify_recursive_merge2(rendered, documents)
103
+ return verification_failure(:merge2, request, rendered, [], verification) unless verification[:semantic_match]
104
+
105
+ result(
106
+ :merge2,
107
+ request,
108
+ output: rendered.content,
109
+ changes: mapping_changes(documents[:current], analyze_role(:output, rendered.content)),
110
+ render_report: render_report(rendered, :recursive_mapping_composite),
111
+ verification: verification
112
+ )
113
+ end
114
+
115
+ def merge3(request)
116
+ documents = %i[base ours theirs].to_h do |role|
117
+ [role, analyze_role(role, request.fetch(:"#{role}_source"))]
118
+ end
119
+ exact_role = exact_revision_role(documents)
120
+ return render_exact(request, documents, exact_role) if exact_role
121
+
122
+ unsafe = documents.values.reject { |document| document.issues.empty? }
123
+ return structural_conflict(request, documents, unsafe) unless unsafe.empty?
124
+ return unmanaged_conflict(request, documents) unless unmanaged_unchanged?(documents)
125
+
126
+ decisions = three_way_decisions(documents)
127
+ conflicts = decisions.select(&:conflict)
128
+ return render_conflicts(request, documents, decisions, conflicts) unless conflicts.empty?
129
+
130
+ render_composite(request, documents, decisions)
131
+ end
132
+
133
+ private
134
+
135
+ def analyze_role(_role, _source)
136
+ raise NotImplementedError, "#{self.class} must implement #analyze_role"
137
+ end
138
+
139
+ def invalid_document(role, source, category, message)
140
+ Document.new(
141
+ role: role,
142
+ source: source,
143
+ analysis: nil,
144
+ entries: [].freeze,
145
+ by_key: {}.freeze,
146
+ issues: [issue(category, message)].freeze,
147
+ document_attributes: {}
148
+ )
149
+ end
150
+
151
+ def analysis_payload(document)
152
+ {
153
+ dialect: 'yaml',
154
+ backend: backend_id.to_s,
155
+ document_attributes: document.document_attributes,
156
+ entries: document.entries.map do |entry|
157
+ {
158
+ key: entry.key,
159
+ value: entry.semantic,
160
+ attributes: entry.attributes,
161
+ source_lines: [entry.start_line, entry.end_line]
162
+ }
163
+ end,
164
+ structurally_unambiguous: document.issues.empty?
165
+ }
166
+ end
167
+
168
+ def analysis_extensions(_document)
169
+ []
170
+ end
171
+
172
+ def render_exact_merge2(request, documents)
173
+ current = documents.fetch(:current)
174
+ rendered = render_plan(request, [whole_source_fragment(:ours, current.source)])
175
+ verification = verify_exact(rendered.content, current, :current)
176
+ result(
177
+ :merge2,
178
+ request,
179
+ output: rendered.content,
180
+ changes: [],
181
+ render_report: render_report(rendered, :exact_revision),
182
+ verification: verification
183
+ )
184
+ end
185
+
186
+ def recursive_merge2(documents)
187
+ result = MergeResult.new(
188
+ template_analysis: documents[:incoming].analysis,
189
+ dest_analysis: documents[:current].analysis
190
+ )
191
+ SmartMerger::Resolver.new(
192
+ documents[:incoming].analysis,
193
+ documents[:current].analysis,
194
+ preference: :destination,
195
+ add_template_only_nodes: true,
196
+ merge_sequences: false
197
+ ).resolve(result)
198
+ result
199
+ end
200
+
201
+ def render_recursive_merge2(request, documents, merged)
202
+ fragments = merged.lines.map do |line|
203
+ recursive_merge2_line_fragment(line, documents)
204
+ end
205
+ render_plan(request, fragments)
206
+ end
207
+
208
+ def recursive_merge2_line_fragment(line, documents)
209
+ role = line.fetch(:source) == :template ? :theirs : :ours
210
+ original_line = line[:original_line]
211
+ content = "#{line.fetch(:content)}\n"
212
+ source = documents.fetch(role == :theirs ? :incoming : :current).source
213
+ if original_line && source.lines[original_line - 1] == content
214
+ return range_fragment(role, original_line, original_line)
215
+ end
216
+
217
+ Ast::Merge::SourceRender::SynthesizedFragment.new(
218
+ content: content,
219
+ reason: :structured_emitter_line,
220
+ producer: provider_id,
221
+ metadata: { source_role: role, original_line: original_line }.compact
222
+ )
223
+ end
224
+
225
+ def verify_recursive_merge2(rendered, documents)
226
+ parsed = analyze_role(:output, rendered.content)
227
+ expected = Yaml::Merge.send(
228
+ :merge_yaml_mappings,
229
+ document_semantics(documents[:incoming]),
230
+ document_semantics(documents[:current])
231
+ )
232
+ actual = document_semantics(parsed)
233
+ {
234
+ output_reparsed: true,
235
+ semantic_match: parsed.issues.empty? && actual == expected,
236
+ ast_attributes_match: parsed.issues.empty?,
237
+ source_match: rendered.synthesized_fragments.empty?,
238
+ ordered_entries: actual.to_a,
239
+ line_sources: rendered.line_records.map { |record| record.slice(:source_role, :original_line) }
240
+ }
241
+ end
242
+
243
+ def document_semantics(document)
244
+ document.entries.to_h { |entry| [entry.key, entry.semantic] }
245
+ end
246
+
247
+ def entry_state(entry)
248
+ entry && [entry.semantic, entry.attributes, entry.fragment]
249
+ end
250
+
251
+ def exact_revision_role(documents)
252
+ return :ours if documents[:ours].source == documents[:theirs].source
253
+ return :ours if documents[:base].source == documents[:theirs].source
254
+
255
+ :theirs if documents[:base].source == documents[:ours].source
256
+ end
257
+
258
+ def render_exact(request, documents, role)
259
+ winner = documents.fetch(role)
260
+ return structural_conflict(request, documents, [winner]) unless fatal_issues(winner).empty?
261
+
262
+ rendered = render_plan(request, [whole_source_fragment(role, winner.source)])
263
+ verification = verify_exact(rendered.content, winner, role)
264
+ return verification_failure(:merge3, request, rendered, [], verification) unless verification[:semantic_match]
265
+
266
+ result(
267
+ :merge3,
268
+ request,
269
+ output: rendered.content,
270
+ changes: mapping_changes(documents[:base], winner),
271
+ diagnostics: diagnostics_for(*documents.values),
272
+ render_report: render_report(rendered, :exact_revision),
273
+ verification: verification.merge(base_participated: true)
274
+ )
275
+ end
276
+
277
+ def verify_exact(output, expected, role)
278
+ parsed = analyze_role(:output, output)
279
+ {
280
+ output_reparsed: true,
281
+ byte_exact: output == expected.source,
282
+ source_match: output == expected.source,
283
+ semantic_match: fatal_issues(parsed).empty? && document_signature(parsed) == document_signature(expected),
284
+ ast_attributes_match: parsed.document_attributes == expected.document_attributes &&
285
+ parsed.entries.map(&:attributes) == expected.entries.map(&:attributes),
286
+ ordered_entries: ordered_semantics(parsed),
287
+ source_role: role
288
+ }
289
+ end
290
+
291
+ def mapping_changes(before, after)
292
+ ordered_keys(before, after).filter_map do |key|
293
+ left = before.by_key[key]
294
+ right = after.by_key[key]
295
+ next if semantic_entry_state(left) == semantic_entry_state(right)
296
+
297
+ { path: "/#{key}", ours: :unchanged, theirs: change_kind(left, right) }.freeze
298
+ end.freeze
299
+ end
300
+
301
+ def semantic_entry_state(entry)
302
+ entry && [entry.semantic, entry.attributes]
303
+ end
304
+
305
+ def ordered_keys(*documents)
306
+ documents.flat_map { |document| document.entries.map(&:key) }.uniq
307
+ end
308
+
309
+ def change_kind(before, after)
310
+ return :added unless before
311
+ return :deleted unless after
312
+
313
+ :edited
314
+ end
315
+
316
+ def three_way_decisions(documents)
317
+ ordered_keys(documents[:ours], documents[:theirs], documents[:base]).map do |key|
318
+ base = documents[:base].by_key[key]
319
+ ours = documents[:ours].by_key[key]
320
+ theirs = documents[:theirs].by_key[key]
321
+ selected_role =
322
+ if entry_state(ours) == entry_state(theirs)
323
+ :ours
324
+ elsif entry_state(ours) == entry_state(base)
325
+ :theirs
326
+ elsif entry_state(theirs) == entry_state(base)
327
+ :ours
328
+ end
329
+ classification = {
330
+ path: "/#{key}",
331
+ ours: side_change(base, ours),
332
+ theirs: side_change(base, theirs)
333
+ }.freeze
334
+ Decision.new(
335
+ key: key,
336
+ base: base,
337
+ ours: ours,
338
+ theirs: theirs,
339
+ selected_role: selected_role,
340
+ classification: classification,
341
+ conflict: selected_role.nil?
342
+ )
343
+ end.freeze
344
+ end
345
+
346
+ def side_change(base, side)
347
+ entry_state(base) == entry_state(side) ? :unchanged : change_kind(base, side)
348
+ end
349
+
350
+ def unmanaged_unchanged?(documents)
351
+ anchor_keys = documents.values.map { |document| document.by_key.keys }.reduce(&:intersection)
352
+ signatures = documents.transform_values { |document| unmanaged_signature(document, anchor_keys) }
353
+ signatures[:base] == signatures[:ours] && signatures[:base] == signatures[:theirs]
354
+ end
355
+
356
+ def unmanaged_signature(document, anchor_keys)
357
+ entries = document.entries.sort_by(&:start_line)
358
+ signature = []
359
+ cursor = 1
360
+ entries.each do |entry|
361
+ if cursor < entry.start_line
362
+ signature << [:source, source_lines(document.source, cursor, entry.start_line - 1)]
363
+ end
364
+ signature << [:entry, entry.key] if anchor_keys.include?(entry.key)
365
+ cursor = entry.end_line + 1
366
+ end
367
+ signature << [:source, source_lines(document.source, cursor, document.source.lines.length)]
368
+ signature
369
+ end
370
+
371
+ def render_composite(request, documents, decisions)
372
+ by_key = decisions.to_h { |decision| [decision.key, decision] }
373
+ fragments = []
374
+ selected = []
375
+ cursor = 1
376
+ documents[:ours].entries.sort_by(&:start_line).each do |ours_entry|
377
+ fragments << range_fragment(:ours, cursor, ours_entry.start_line - 1) if cursor < ours_entry.start_line
378
+ decision = by_key.fetch(ours_entry.key)
379
+ selected_entry_value = decision.selected_role && decision.public_send(decision.selected_role)
380
+ if selected_entry_value
381
+ fragments << entry_fragment(decision.selected_role, selected_entry_value)
382
+ selected << selected_entry(selected_entry_value, decision.selected_role)
383
+ end
384
+ cursor = ours_entry.end_line + 1
385
+ end
386
+ ours_line_count = documents[:ours].source.lines.length
387
+ fragments << range_fragment(:ours, cursor, ours_line_count) if cursor <= ours_line_count
388
+ decisions.each do |decision|
389
+ next if decision.ours || decision.selected_role != :theirs || !decision.theirs
390
+
391
+ fragments << entry_fragment(:theirs, decision.theirs)
392
+ selected << selected_entry(decision.theirs, :theirs)
393
+ end
394
+
395
+ rendered = render_plan(request, separated_fragments(fragments.compact, documents))
396
+ verification = verify_composite(rendered.content, selected)
397
+ unless verification[:semantic_match]
398
+ return verification_failure(:merge3, request, rendered, decisions, verification)
399
+ end
400
+
401
+ result(
402
+ :merge3,
403
+ request,
404
+ output: rendered.content,
405
+ changes: decisions.map(&:classification),
406
+ render_report: render_report(rendered, :exact_mapping_entry_composite),
407
+ verification: verification.merge(base_participated: true)
408
+ )
409
+ end
410
+
411
+ def selected_entry(entry, role)
412
+ {
413
+ key: entry.key,
414
+ semantic: entry.semantic,
415
+ attributes: entry.attributes,
416
+ source_role: role,
417
+ source_lines: [entry.start_line, entry.end_line],
418
+ fragment: entry.fragment
419
+ }.freeze
420
+ end
421
+
422
+ def verify_composite(output, selected)
423
+ parsed = analyze_role(:output, output)
424
+ expected_semantics = selected.map { |entry| [entry[:key], entry[:semantic]] }
425
+ expected_attributes = selected.map { |entry| [entry[:key], entry[:attributes]] }
426
+ actual_semantics = parsed.entries.map { |entry| [entry.key, entry.semantic] }
427
+ actual_attributes = parsed.entries.map { |entry| [entry.key, entry.attributes] }
428
+ source_match = parsed.entries.zip(selected).all? do |actual, expected|
429
+ actual.fragment == expected[:fragment] ||
430
+ (!expected[:fragment].end_with?("\n") && actual.fragment == "#{expected[:fragment]}\n")
431
+ end
432
+ {
433
+ output_reparsed: true,
434
+ semantic_match: parsed.issues.empty? && actual_semantics == expected_semantics &&
435
+ actual_attributes == expected_attributes && source_match,
436
+ ast_attributes_match: actual_attributes == expected_attributes,
437
+ source_match: source_match,
438
+ ordered_entries: actual_semantics,
439
+ entry_sources: selected.map { |entry| entry.slice(:key, :source_role, :source_lines) }
440
+ }
441
+ end
442
+
443
+ def ordered_semantics(document)
444
+ document.entries.map { |entry| [entry.key, entry.semantic] }
445
+ end
446
+
447
+ def document_signature(document)
448
+ [ordered_semantics(document), document.entries.map(&:attributes)]
449
+ end
450
+
451
+ def fatal_issues(document)
452
+ document.issues.select { |item| %i[parse_error structural_error].include?(item[:category]) }
453
+ end
454
+
455
+ def render_conflicts(request, documents, decisions, conflicts)
456
+ unless conflicts.all? { |conflict| conflict.ours&.ownership_provable }
457
+ return full_file_conflict(request, documents, decisions, conflicts, :ownership_unproven)
458
+ end
459
+ unless unmanaged_unchanged?(documents)
460
+ return full_file_conflict(request, documents, decisions, conflicts, :unmanaged_source_change)
461
+ end
462
+
463
+ by_key = conflicts.to_h { |conflict| [conflict.key, conflict] }
464
+ fragments = []
465
+ cursor = 1
466
+ documents[:ours].entries.sort_by(&:start_line).each do |entry|
467
+ fragments << range_fragment(:ours, cursor, entry.start_line - 1) if cursor < entry.start_line
468
+ conflict = by_key[entry.key]
469
+ fragments << (conflict ? conflict_fragment(request, conflict) : entry_fragment(:ours, entry))
470
+ cursor = entry.end_line + 1
471
+ end
472
+ ours_line_count = documents[:ours].source.lines.length
473
+ fragments << range_fragment(:ours, cursor, ours_line_count) if cursor <= ours_line_count
474
+ rendered = render_plan(request, fragments.compact)
475
+ conflict_failure(request, decisions, conflicts, rendered, :mapping_entry_localized_conflict)
476
+ end
477
+
478
+ def conflict_fragment(request, decision)
479
+ Ast::Merge::SourceRender::ConflictFragment.new(
480
+ conflict_id: conflict_id(decision.key),
481
+ base: conflict_side(:base, decision.base),
482
+ ours: conflict_side(:ours, decision.ours),
483
+ theirs: conflict_side(:theirs, decision.theirs),
484
+ labels: request.fetch(:labels, {}),
485
+ marker_size: request.fetch(:conflict_marker_size, 7),
486
+ metadata: { path: decision.classification[:path], category: conflict_category(decision) }
487
+ )
488
+ end
489
+
490
+ def conflict_side(role, entry)
491
+ return [] unless entry
492
+
493
+ [entry_fragment(role, entry, line_bounded: true)]
494
+ end
495
+
496
+ def conflict_category(decision)
497
+ decision.ours.nil? || decision.theirs.nil? ? :delete_edit : :edit_edit
498
+ end
499
+
500
+ def conflict_id(key)
501
+ "yaml-mapping-#{Digest::SHA256.hexdigest(key)[0, 16]}"
502
+ end
503
+
504
+ def conflict_failure(request, decisions, conflicts, rendered, strategy)
505
+ failure(
506
+ :merge3,
507
+ request,
508
+ category: :merge_conflict,
509
+ message: 'YAML mapping entries changed incompatibly.',
510
+ changes: decisions.map(&:classification),
511
+ conflicts: conflicts.map { |decision| conflict_payload(decision) },
512
+ conflicted_output: rendered.content,
513
+ conflicted_source: rendered.content,
514
+ render_report: render_report(rendered, strategy),
515
+ verification: { base_participated: true }
516
+ )
517
+ end
518
+
519
+ def structural_conflict(request, documents, unsafe)
520
+ source_role = unsafe.first.role
521
+ full_file_conflict(
522
+ request,
523
+ documents,
524
+ safe_decisions(documents),
525
+ [],
526
+ :structural_ambiguity,
527
+ diagnostics: [
528
+ {
529
+ category: unsafe.first.issues.first[:category],
530
+ severity: :error,
531
+ message: "#{source_role} YAML input is unsafe for structural merge.",
532
+ blocking: true,
533
+ source_role: source_role
534
+ },
535
+ *diagnostics_for(*unsafe)
536
+ ]
537
+ )
538
+ end
539
+
540
+ def safe_decisions(documents)
541
+ return [] unless documents.values.all? { |document| document.issues.empty? }
542
+
543
+ three_way_decisions(documents)
544
+ end
545
+
546
+ def unmanaged_conflict(request, documents)
547
+ full_file_conflict(
548
+ request,
549
+ documents,
550
+ three_way_decisions(documents),
551
+ [],
552
+ :unmanaged_source_change,
553
+ diagnostics: [{
554
+ category: :unmanaged_source_change,
555
+ severity: :error,
556
+ message: 'Unmanaged YAML layout or comments changed independently.',
557
+ blocking: true
558
+ }]
559
+ )
560
+ end
561
+
562
+ def full_file_conflict(request, documents, decisions, conflicts, reason, diagnostics: nil)
563
+ conflict = { conflict_id: 'yaml-conflict-root', category: reason, path: '<document>' }.freeze
564
+ rendered = render_plan(
565
+ request,
566
+ [
567
+ Ast::Merge::SourceRender::ConflictFragment.new(
568
+ conflict_id: conflict[:conflict_id],
569
+ base: conflict_source(:base, documents[:base].source),
570
+ ours: conflict_source(:ours, documents[:ours].source),
571
+ theirs: conflict_source(:theirs, documents[:theirs].source),
572
+ labels: request.fetch(:labels, {}),
573
+ marker_size: request.fetch(:conflict_marker_size, 7),
574
+ metadata: { category: reason }
575
+ )
576
+ ]
577
+ )
578
+ failure(
579
+ :merge3,
580
+ request,
581
+ category: reason,
582
+ message: 'YAML source cannot be safely combined structurally.',
583
+ diagnostics: diagnostics,
584
+ changes: decisions.map(&:classification),
585
+ conflicts: conflicts.empty? ? [conflict] : conflicts.map { |item| conflict_payload(item) },
586
+ fallbacks: [{ from: :mapping_entry_composite, to: :full_file_conflict, reason: reason }],
587
+ conflicted_output: rendered.content,
588
+ conflicted_source: rendered.content,
589
+ render_report: render_report(rendered, :full_file_conflict),
590
+ verification: { base_participated: true },
591
+ source_role: diagnostics&.first&.fetch(:source_role, nil)
592
+ )
593
+ end
594
+
595
+ def conflict_payload(decision)
596
+ {
597
+ conflict_id: conflict_id(decision.key),
598
+ category: conflict_category(decision),
599
+ path: decision.classification[:path],
600
+ change_classification: decision.classification
601
+ }.freeze
602
+ end
603
+
604
+ def conflict_source(role, source)
605
+ return [] if source.empty?
606
+
607
+ [whole_source_fragment(role, source, line_bounded: true)]
608
+ end
609
+
610
+ def separated_fragments(fragments, documents)
611
+ fragments.each_with_index.flat_map do |fragment, index|
612
+ next [fragment] if index == fragments.length - 1
613
+ next [fragment] if fragment.is_a?(Ast::Merge::SourceRender::ConflictFragment)
614
+
615
+ content = fragment_content(fragment, documents)
616
+ next [fragment] if content.empty? || content.end_with?("\n")
617
+
618
+ [
619
+ Ast::Merge::SourceRender::SynthesizedFragment.new(
620
+ content: "#{content}\n",
621
+ reason: :mapping_entry_separator,
622
+ producer: provider_id,
623
+ metadata: fragment.metadata.merge(copied_source: true)
624
+ )
625
+ ]
626
+ end
627
+ end
628
+
629
+ def fragment_content(fragment, documents)
630
+ return fragment.content if fragment.is_a?(Ast::Merge::SourceRender::SynthesizedFragment)
631
+
632
+ document = documents.fetch(fragment.revision)
633
+ source_lines(document.source, fragment.start_line, fragment.end_line)
634
+ end
635
+
636
+ def whole_source_fragment(role, source, line_bounded: false)
637
+ if source.empty?
638
+ return Ast::Merge::SourceRender::SynthesizedFragment.new(
639
+ content: '',
640
+ reason: :exact_empty_source,
641
+ producer: provider_id,
642
+ metadata: { source_role: role }
643
+ )
644
+ end
645
+ fragment = range_fragment(role, 1, source.lines.length)
646
+ return fragment unless line_bounded && !source.end_with?("\n")
647
+
648
+ Ast::Merge::SourceRender::SynthesizedFragment.new(
649
+ content: "#{source}\n",
650
+ reason: :conflict_line_boundary,
651
+ producer: provider_id,
652
+ metadata: { source_role: role, copied_source: true }
653
+ )
654
+ end
655
+
656
+ def entry_fragment(role, entry, line_bounded: false)
657
+ unless line_bounded && !entry.fragment.end_with?("\n")
658
+ return range_fragment(role, entry.start_line, entry.end_line, yaml_key: entry.key)
659
+ end
660
+
661
+ Ast::Merge::SourceRender::SynthesizedFragment.new(
662
+ content: "#{entry.fragment}\n",
663
+ reason: :conflict_line_boundary,
664
+ producer: provider_id,
665
+ metadata: { source_role: role, yaml_key: entry.key, copied_source: true }
666
+ )
667
+ end
668
+
669
+ def range_fragment(role, start_line, end_line, **metadata)
670
+ return if end_line < start_line
671
+
672
+ Ast::Merge::SourceRender::SourceFragment.new(
673
+ revision: role,
674
+ start_line: start_line,
675
+ end_line: end_line,
676
+ metadata: { source_role: role }.merge(metadata)
677
+ )
678
+ end
679
+
680
+ def source_lines(source, start_line, end_line)
681
+ return '' if end_line < start_line
682
+
683
+ source.lines.slice(start_line - 1, end_line - start_line + 1).to_a.join
684
+ end
685
+
686
+ def render_plan(request, fragments)
687
+ Ast::Merge::SourceRender::Renderer.new.render(
688
+ Ast::Merge::SourceRender::Plan.new(
689
+ sources: {
690
+ base: request[:base_source].to_s,
691
+ ours: request[:ours_source] || request[:current_source].to_s,
692
+ theirs: request[:theirs_source] || request[:incoming_source].to_s
693
+ },
694
+ fragments: fragments,
695
+ metadata: { provider_id: provider_id }
696
+ )
697
+ )
698
+ end
699
+
700
+ def render_report(rendered, strategy)
701
+ {
702
+ strategy: strategy,
703
+ line_records: rendered.line_records,
704
+ synthesized_fragments: rendered.synthesized_fragments,
705
+ conflicts: rendered.conflicts,
706
+ verification_input: rendered.verification_input
707
+ }
708
+ end
709
+
710
+ def issue(category, message, **metadata)
711
+ { category: category, message: message, **metadata }.freeze
712
+ end
713
+
714
+ def diagnostics_for(*documents)
715
+ documents.flat_map do |document|
716
+ document.issues.map do |item|
717
+ item.merge(severity: :warning, blocking: false, source_role: document.role).freeze
718
+ end
719
+ end.freeze
720
+ end
721
+
722
+ def invalid_document_failure(operation, request, documents)
723
+ first = documents.first
724
+ failure(
725
+ operation,
726
+ request,
727
+ category: first.issues.first[:category],
728
+ message: "#{first.role} YAML input is unsafe",
729
+ diagnostics: diagnostics_for(*documents).map { |diagnostic| diagnostic.merge(blocking: true) },
730
+ source_role: first.role
731
+ )
732
+ end
733
+
734
+ def verification_failure(operation, request, rendered, decisions, verification)
735
+ failure(
736
+ operation,
737
+ request,
738
+ category: :verification_failure,
739
+ message: 'Rendered YAML did not preserve planned ordered semantics, AST attributes, and source fragments.',
740
+ changes: decisions.map(&:classification),
741
+ render_report: render_report(rendered, :exact_mapping_entry_composite),
742
+ verification: operation == :merge3 ? verification.merge(base_participated: true) : verification
743
+ )
744
+ end
745
+
746
+ def result(operation, request, diagnostics: [], changes: [], conflicts: [], fallbacks: [],
747
+ render_report: {}, verification: {}, **payload)
748
+ Ast::Merge::ProviderResult.build(
749
+ operation: operation,
750
+ success: true,
751
+ envelope: envelope(
752
+ request,
753
+ diagnostics: diagnostics,
754
+ changes: changes,
755
+ conflicts: conflicts,
756
+ fallbacks: fallbacks,
757
+ render_report: render_report,
758
+ verification: verification
759
+ ),
760
+ **payload
761
+ )
762
+ end
763
+
764
+ def failure(operation, request, category:, message:, diagnostics: nil, changes: [], conflicts: [],
765
+ fallbacks: [], render_report: {}, verification: {}, **payload)
766
+ diagnostics ||= [{ category: category, severity: :error, message: message, blocking: true }]
767
+ Ast::Merge::ProviderResult.build(
768
+ operation: operation,
769
+ success: false,
770
+ envelope: envelope(
771
+ request,
772
+ diagnostics: diagnostics,
773
+ changes: changes,
774
+ conflicts: conflicts,
775
+ fallbacks: fallbacks,
776
+ render_report: render_report,
777
+ verification: verification
778
+ ),
779
+ **payload
780
+ )
781
+ end
782
+
783
+ def envelope(request, **fields)
784
+ {
785
+ provider: {
786
+ provider_id: provider_id,
787
+ family: family,
788
+ dialect: request[:dialect] || :yaml,
789
+ backend: request[:backend] || backend_id,
790
+ package: package_name,
791
+ package_version: package_version
792
+ },
793
+ profile: { profile_id: request[:profile_id] || DEFAULT_PROFILE }
794
+ }.merge(fields)
795
+ end
796
+ end
797
+ # rubocop:enable Metrics/AbcSize, Metrics/ClassLength, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/ParameterLists, Metrics/PerceivedComplexity
798
+ end
799
+ end
@@ -5,7 +5,7 @@ module Yaml
5
5
  # Version namespace for this gem.
6
6
  module Version
7
7
  # Current gem version.
8
- VERSION = '7.1.6'
8
+ VERSION = '7.1.8'
9
9
  end
10
10
  # Current gem version exposed at the traditional constant location.
11
11
  VERSION = Version::VERSION # Traditional Constant Location
data/lib/yaml/merge.rb CHANGED
@@ -45,6 +45,8 @@ module Yaml
45
45
  autoload :MergeResult, 'yaml/merge/merge_result'
46
46
  autoload :NodeWrapper, 'yaml/merge/node_wrapper'
47
47
  autoload :SmartMerger, 'yaml/merge/smart_merger'
48
+ autoload :SourcePreservingProvider, 'yaml/merge/source_preserving_provider'
49
+ autoload :Provider, 'yaml/merge/provider'
48
50
 
49
51
  module_function
50
52
 
@@ -589,3 +591,5 @@ module Yaml
589
591
  end
590
592
 
591
593
  Yaml::Merge.register_backend!
594
+ require_relative 'merge/provider'
595
+ Yaml::Merge.register_provider!
data/sig/yaml/merge.rbs CHANGED
@@ -1,5 +1,21 @@
1
1
  module Yaml
2
2
  module Merge
3
+ class SourcePreservingProvider
4
+ def provider_id: () -> String
5
+ def family: () -> String
6
+ def capabilities: () -> Hash[Symbol, untyped]
7
+ def analyze: (Hash[Symbol, untyped] request) -> Hash[Symbol, untyped]
8
+ def diff2: (Hash[Symbol, untyped] request) -> Hash[Symbol, untyped]
9
+ def merge2: (Hash[Symbol, untyped] request) -> Hash[Symbol, untyped]
10
+ def merge3: (Hash[Symbol, untyped] request) -> Hash[Symbol, untyped]
11
+ end
12
+
13
+ class Provider < SourcePreservingProvider
14
+ end
15
+
16
+ def self.merge_provider: () -> Provider
17
+ def self.register_provider!: (?replace: bool) -> untyped
18
+
3
19
  module Version
4
20
  VERSION: String
5
21
  end
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: yaml-merge
3
3
  version: !ruby/object:Gem::Version
4
- version: 7.1.6
4
+ version: 7.1.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter H. Boling
@@ -43,28 +43,28 @@ dependencies:
43
43
  requirements:
44
44
  - - '='
45
45
  - !ruby/object:Gem::Version
46
- version: 7.1.6
46
+ version: 7.1.8
47
47
  type: :runtime
48
48
  prerelease: false
49
49
  version_requirements: !ruby/object:Gem::Requirement
50
50
  requirements:
51
51
  - - '='
52
52
  - !ruby/object:Gem::Version
53
- version: 7.1.6
53
+ version: 7.1.8
54
54
  - !ruby/object:Gem::Dependency
55
55
  name: tree_haver
56
56
  requirement: !ruby/object:Gem::Requirement
57
57
  requirements:
58
58
  - - '='
59
59
  - !ruby/object:Gem::Version
60
- version: 7.1.6
60
+ version: 7.1.8
61
61
  type: :runtime
62
62
  prerelease: false
63
63
  version_requirements: !ruby/object:Gem::Requirement
64
64
  requirements:
65
65
  - - '='
66
66
  - !ruby/object:Gem::Version
67
- version: 7.1.6
67
+ version: 7.1.8
68
68
  - !ruby/object:Gem::Dependency
69
69
  name: version_gem
70
70
  requirement: !ruby/object:Gem::Requirement
@@ -94,7 +94,7 @@ dependencies:
94
94
  version: '3.0'
95
95
  - - ">="
96
96
  - !ruby/object:Gem::Version
97
- version: 3.0.17
97
+ version: 3.0.31
98
98
  type: :development
99
99
  prerelease: false
100
100
  version_requirements: !ruby/object:Gem::Requirement
@@ -104,7 +104,7 @@ dependencies:
104
104
  version: '3.0'
105
105
  - - ">="
106
106
  - !ruby/object:Gem::Version
107
- version: 3.0.17
107
+ version: 3.0.31
108
108
  - !ruby/object:Gem::Dependency
109
109
  name: bundler-audit
110
110
  requirement: !ruby/object:Gem::Requirement
@@ -182,7 +182,7 @@ dependencies:
182
182
  version: '3.2'
183
183
  - - ">="
184
184
  - !ruby/object:Gem::Version
185
- version: 3.2.2
185
+ version: 3.2.3
186
186
  type: :development
187
187
  prerelease: false
188
188
  version_requirements: !ruby/object:Gem::Requirement
@@ -192,7 +192,7 @@ dependencies:
192
192
  version: '3.2'
193
193
  - - ">="
194
194
  - !ruby/object:Gem::Version
195
- version: 3.2.2
195
+ version: 3.2.3
196
196
  - !ruby/object:Gem::Dependency
197
197
  name: kettle-test
198
198
  requirement: !ruby/object:Gem::Requirement
@@ -222,7 +222,7 @@ dependencies:
222
222
  version: '3.2'
223
223
  - - ">="
224
224
  - !ruby/object:Gem::Version
225
- version: 3.2.6
225
+ version: 3.2.7
226
226
  type: :development
227
227
  prerelease: false
228
228
  version_requirements: !ruby/object:Gem::Requirement
@@ -232,7 +232,7 @@ dependencies:
232
232
  version: '3.2'
233
233
  - - ">="
234
234
  - !ruby/object:Gem::Version
235
- version: 3.2.6
235
+ version: 3.2.7
236
236
  - !ruby/object:Gem::Dependency
237
237
  name: ruby-progressbar
238
238
  requirement: !ruby/object:Gem::Requirement
@@ -304,7 +304,9 @@ files:
304
304
  - lib/yaml/merge/file_analysis.rb
305
305
  - lib/yaml/merge/merge_result.rb
306
306
  - lib/yaml/merge/node_wrapper.rb
307
+ - lib/yaml/merge/provider.rb
307
308
  - lib/yaml/merge/smart_merger.rb
309
+ - lib/yaml/merge/source_preserving_provider.rb
308
310
  - lib/yaml/merge/version.rb
309
311
  - sig/yaml/merge.rbs
310
312
  homepage: https://github.com/structuredmerge/structuredmerge-ruby
@@ -313,10 +315,10 @@ licenses:
313
315
  - PolyForm-Small-Business-1.0.0
314
316
  metadata:
315
317
  homepage_uri: https://structuredmerge.org
316
- source_code_uri: https://github.com/structuredmerge/structuredmerge-ruby/tree/v7.1.6
317
- changelog_uri: https://github.com/structuredmerge/structuredmerge-ruby/blob/v7.1.6/CHANGELOG.md
318
+ source_code_uri: https://github.com/structuredmerge/structuredmerge-ruby/tree/v7.1.8
319
+ changelog_uri: https://github.com/structuredmerge/structuredmerge-ruby/blob/v7.1.8/CHANGELOG.md
318
320
  bug_tracker_uri: https://github.com/structuredmerge/structuredmerge-ruby/issues
319
- documentation_uri: https://www.rubydoc.info/gems/yaml-merge/7.1.6
321
+ documentation_uri: https://www.rubydoc.info/gems/yaml-merge/7.1.8
320
322
  funding_uri: https://github.com/sponsors/pboling
321
323
  wiki_uri: https://github.com/structuredmerge/structuredmerge-ruby/wiki
322
324
  news_uri: https://www.railsbling.com/tags/yaml-merge
metadata.gz.sig CHANGED
Binary file