prism-merge 7.1.1 → 7.1.4

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: e746e33165b81c8213b46fb68de60386a0bb0bd88ad29e4abeb4bc0e9d9a7050
4
- data.tar.gz: fec3c1ad285a6cea78b69c68d499fdca8856113abb3c2c79e22a72c565773042
3
+ metadata.gz: 7d32fbdcd833e576a54414849501db40ddacd349498c05e4d233159f2e5521c1
4
+ data.tar.gz: 063f4adb55b355f984f09eb376652dded9e8a8d2451d993ebc4a954eb403ca0c
5
5
  SHA512:
6
- metadata.gz: 94060d0dd8879f84e4516e118b606454f14df7203cacad358392d1f8e666b0282757ee9044bdc047ad8f2c612437d8066d16be7d0a0803ce5d4eb31415c242f2
7
- data.tar.gz: 34d11e92d49b0fa616604f8c6396100d21d7ca3d7d69549978835bec42145c211e9aa085b2fae58a2322990ac910dff0c68591b97096cda7854db98409d9814a
6
+ metadata.gz: 291ed360148759fb7f32d3c28da2d0c8da02733022b60b9ada62781cd0cfc61d0295164e43f84985816d2416f92f433862e04010ec2ac59c200f953918e8c540
7
+ data.tar.gz: 75c2f6a59e5910332a80945f0f12426d5a019872d4e128834cebabfc496db6c30d2ab389940a7bc10910a084694bf68d59e9bcd81281dbbc173857d86b6a59a5
checksums.yaml.gz.sig CHANGED
Binary file
data/README.md CHANGED
@@ -83,7 +83,7 @@ File.write("merged.rb", result)
83
83
 
84
84
  ### Compatibility
85
85
 
86
- Compatible with MRI Ruby 4.0.0+, and concordant releases of JRuby, and TruffleRuby.
86
+ Compatible with MRI Ruby 4.0.0+, JRuby, and TruffleRuby.
87
87
  CI workflows and Appraisals are generated for MRI Ruby 4.0.0+.
88
88
  This test floor is configured by `ruby.test_minimum` in `.kettle-jem.yml` and
89
89
  may be higher than the gem's runtime compatibility floor when legacy Rubies are
@@ -1099,20 +1099,12 @@ See [SECURITY.md][🔐security].
1099
1099
  ## 🤝 Contributing
1100
1100
 
1101
1101
  If you need some ideas of where to help, you could work on adding more code coverage,
1102
- or if it is already 💯 (see [below](#code-coverage)) check [issues][🤝gh-issues] or [PRs][🤝gh-pulls],
1103
- or use the gem and think about how it could be better.
1102
+ check [issues][🤝gh-issues] or [PRs][🤝gh-pulls], or use the gem and think about how it could be better.
1104
1103
 
1105
1104
  We [![Keep A Changelog][📗keep-changelog-img]][📗keep-changelog] so if you make changes, remember to update it.
1106
1105
 
1107
1106
  See [CONTRIBUTING.md][🤝contributing] for more detailed instructions.
1108
1107
 
1109
- ### Code Coverage
1110
-
1111
- <details markdown="1">
1112
- <summary>Coverage service badges</summary>
1113
-
1114
- </details>
1115
-
1116
1108
  ## 📌 Versioning
1117
1109
 
1118
1110
  This library follows [![Semantic Versioning 2.0.0][📌semver-img]][📌semver] for its public API where practical.
@@ -1252,7 +1244,7 @@ If none of the available licenses suit your use case, please [contact us](mailto
1252
1244
  [📌gitmoji]: https://gitmoji.dev
1253
1245
  [📌gitmoji-img]: https://img.shields.io/badge/gitmoji_commits-%20%F0%9F%98%9C%20%F0%9F%98%8D-34495e.svg?style=flat-square
1254
1246
  [🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
1255
- [🧮kloc-img]: https://img.shields.io/badge/KLOC-4.246-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
1247
+ [🧮kloc-img]: https://img.shields.io/badge/KLOC-4.550-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
1256
1248
  [🔐security]: https://github.com/structuredmerge/structuredmerge-ruby/blob/main/SECURITY.md
1257
1249
  [🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
1258
1250
  [📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
@@ -0,0 +1,717 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'digest'
4
+
5
+ module Prism
6
+ module Merge
7
+ # Base-aware, source-preserving Ruby provider backed by Prism top-level owners.
8
+ # rubocop:disable Metrics/AbcSize, Metrics/ClassLength, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/ParameterLists, Metrics/PerceivedComplexity -- provider decisions, source plans, and verification form one boundary
9
+ class Provider
10
+ DEFAULT_PROFILE = :source_preserving
11
+ Owner = Data.define(:id, :signature, :fingerprint, :start_line, :end_line, :role)
12
+ Document = Data.define(:source, :analysis, :owners, :by_id, :unmanaged_fingerprint)
13
+ Decision = Data.define(:changes, :conflicts, :choices)
14
+
15
+ def provider_id = 'ruby.ruby.prism'
16
+ def family = 'ruby'
17
+
18
+ def capabilities
19
+ {
20
+ operations: Ast::Merge::ProviderContract::OPERATIONS,
21
+ dialects: %i[ruby],
22
+ backends: %i[prism],
23
+ profiles: [DEFAULT_PROFILE],
24
+ role: :backend,
25
+ source_preservation: %i[exact_source owner_fragments line_provenance reparse semantic_verification]
26
+ }.freeze
27
+ end
28
+
29
+ def analyze(request)
30
+ document = parse_document(:analyze, request, :source)
31
+ return document if provider_failure?(document)
32
+
33
+ result(
34
+ :analyze,
35
+ request,
36
+ analysis: {
37
+ backend: 'prism',
38
+ valid: true,
39
+ owners: document.owners.map { |owner| owner_description(owner) }
40
+ },
41
+ verification: { source_parsed: true }
42
+ )
43
+ end
44
+
45
+ def diff2(request)
46
+ before = parse_document(:diff2, request, :before)
47
+ return before if provider_failure?(before)
48
+
49
+ after = parse_document(:diff2, request, :after)
50
+ return after if provider_failure?(after)
51
+
52
+ changes = diff_documents(before, after)
53
+ result(
54
+ :diff2,
55
+ request,
56
+ diff: { changes: changes },
57
+ changes: changes,
58
+ verification: { before_parsed: true, after_parsed: true }
59
+ )
60
+ end
61
+
62
+ def merge2(request)
63
+ merged = Prism::Merge.merge_ruby(
64
+ request.fetch(:incoming_source),
65
+ request.fetch(:current_source),
66
+ 'ruby',
67
+ backend: request[:backend],
68
+ add_template_only_nodes: true
69
+ )
70
+ return merge2_failure(request, merged) unless merged[:ok]
71
+
72
+ output = merged.fetch(:output)
73
+ parsed = parse_source(output, :output)
74
+ return render_failure(:merge2, request, parsed) if provider_failure?(parsed)
75
+
76
+ result(
77
+ :merge2,
78
+ request,
79
+ output: output,
80
+ render_report: { strategy: :prism_smart_merger, synthesis: :family_emission },
81
+ verification: { output_reparsed: true }
82
+ )
83
+ end
84
+
85
+ def merge3(request)
86
+ documents = parse_merge3_documents(request)
87
+ return documents if provider_failure?(documents)
88
+
89
+ exact_role = exact_revision_role(request)
90
+ decision = decide(documents, include_unmanaged_conflict: exact_role.nil?)
91
+ return render_conflicts(request, documents, decision) unless decision.conflicts.empty?
92
+
93
+ return render_exact(request, documents, decision, exact_role) if exact_role
94
+
95
+ render_composite(request, documents, decision)
96
+ end
97
+
98
+ private
99
+
100
+ def parse_merge3_documents(request)
101
+ %i[base ours theirs].each_with_object({}) do |role, documents|
102
+ document = parse_document(:merge3, request, role)
103
+ return document if provider_failure?(document)
104
+
105
+ documents[role] = document
106
+ end
107
+ end
108
+
109
+ def parse_document(operation, request, role)
110
+ source = request.fetch(role == :source ? :source : :"#{role}_source")
111
+ parsed = parse_source(source, role)
112
+ return parse_failure(operation, request, role, parsed) if provider_failure?(parsed)
113
+
114
+ parsed
115
+ end
116
+
117
+ def parse_source(source, role)
118
+ analysis = Prism::Merge::FileAnalysis.new(source, source_label: role.to_s)
119
+ unless analysis.valid?
120
+ return {
121
+ parse_error: analysis.errors.map(&:message).join('; '),
122
+ source_role: role
123
+ }
124
+ end
125
+
126
+ owners = analysis.nodes_with_comments.map do |node_info|
127
+ signature = node_info.fetch(:signature)
128
+ Owner.new(
129
+ id: owner_id(signature),
130
+ signature: signature,
131
+ fingerprint: owner_fingerprint(source, node_info),
132
+ start_line: node_info.fetch(:line_range).begin,
133
+ end_line: node_info.fetch(:line_range).end,
134
+ role: role
135
+ )
136
+ end
137
+ duplicate = owners.group_by(&:id).find { |_id, matches| matches.length > 1 }
138
+ return { ambiguous_owner: duplicate.first, source_role: role } if duplicate
139
+
140
+ Document.new(
141
+ source: source,
142
+ analysis: analysis,
143
+ owners: owners.freeze,
144
+ by_id: owners.to_h { |owner| [owner.id, owner] }.freeze,
145
+ unmanaged_fingerprint: unmanaged_fingerprint(source, owners)
146
+ )
147
+ end
148
+
149
+ def unmanaged_fingerprint(source, owners)
150
+ owned_lines = owners.each_with_object({}) do |owner, lines|
151
+ (owner.start_line..owner.end_line).each { |line| lines[line] = true }
152
+ end
153
+ source.lines.each_with_index.filter_map do |line, index|
154
+ line unless owned_lines[index + 1]
155
+ end.join
156
+ end
157
+
158
+ def semantic_fingerprint(value)
159
+ case value
160
+ when ::Prism::Node
161
+ fields = value.deconstruct_keys(nil).reject do |key, _item|
162
+ key == :node_id || key == :location || key.to_s.end_with?('_loc')
163
+ end
164
+ [value.type, fields.map { |key, item| [key, semantic_fingerprint(item)] }]
165
+ when Array
166
+ value.map { |item| semantic_fingerprint(item) }
167
+ when Hash
168
+ value.map { |key, item| [key, semantic_fingerprint(item)] }
169
+ else
170
+ value
171
+ end
172
+ end
173
+
174
+ def owner_fingerprint(source, node_info)
175
+ range = node_info.fetch(:line_range)
176
+ {
177
+ ast: semantic_fingerprint(node_info.fetch(:node)),
178
+ source: source.lines.slice(range.begin - 1, range.end - range.begin + 1).join.delete_suffix("\n"),
179
+ leading_comments: node_info.fetch(:leading_comments).map(&:slice),
180
+ inline_comments: node_info.fetch(:inline_comments).map(&:slice)
181
+ }.freeze
182
+ end
183
+
184
+ def owner_id(signature)
185
+ Digest::SHA256.hexdigest(Marshal.dump(signature))
186
+ end
187
+
188
+ def owner_path(owner)
189
+ owner ? owner.signature.inspect : '<document>'
190
+ end
191
+
192
+ def owner_description(owner)
193
+ {
194
+ path: owner_path(owner),
195
+ signature: owner.signature,
196
+ line_range: [owner.start_line, owner.end_line]
197
+ }
198
+ end
199
+
200
+ def diff_documents(before, after)
201
+ ordered_ids(before, after).filter_map do |id|
202
+ left = before.by_id[id]
203
+ right = after.by_id[id]
204
+ next if left && right && left.fingerprint == right.fingerprint
205
+
206
+ {
207
+ path: owner_path(left || right),
208
+ ours: :unchanged,
209
+ theirs: change_kind(left, right)
210
+ }.freeze
211
+ end.freeze
212
+ end
213
+
214
+ def ordered_ids(*documents)
215
+ documents.flat_map { |document| document.owners.map(&:id) }.uniq
216
+ end
217
+
218
+ def change_kind(before, after)
219
+ return :added unless before
220
+ return :deleted unless after
221
+
222
+ :edited
223
+ end
224
+
225
+ def decide(documents, include_unmanaged_conflict:)
226
+ changes = []
227
+ conflicts = []
228
+ choices = {}
229
+ append_unmanaged_conflict(changes, conflicts, documents) if include_unmanaged_conflict
230
+ ordered_ids(*documents.values).each do |id|
231
+ base = documents.fetch(:base).by_id[id]
232
+ ours = documents.fetch(:ours).by_id[id]
233
+ theirs = documents.fetch(:theirs).by_id[id]
234
+ ours_change = side_change(base, ours)
235
+ theirs_change = side_change(base, theirs)
236
+ next choices[id] = :ours if ours_change == :unchanged && theirs_change == :unchanged
237
+
238
+ change = {
239
+ path: owner_path(base || ours || theirs),
240
+ ours: ours_change,
241
+ theirs: theirs_change
242
+ }.freeze
243
+ changes << change
244
+ choice = owner_choice(base, ours, theirs)
245
+ if choice == :conflict
246
+ conflicts << conflict_for(id, base, ours, theirs, change)
247
+ else
248
+ choices[id] = choice
249
+ end
250
+ end
251
+ Decision.new(changes: changes.freeze, conflicts: conflicts.freeze, choices: choices.freeze)
252
+ end
253
+
254
+ def append_unmanaged_conflict(changes, conflicts, documents)
255
+ base = documents.fetch(:base).unmanaged_fingerprint
256
+ ours = documents.fetch(:ours).unmanaged_fingerprint
257
+ theirs = documents.fetch(:theirs).unmanaged_fingerprint
258
+ return if ours == theirs || base == theirs
259
+
260
+ change = {
261
+ path: '<unmanaged-source>',
262
+ ours: base == ours ? :unchanged : :edited,
263
+ theirs: base == theirs ? :unchanged : :edited
264
+ }.freeze
265
+ changes << change
266
+ conflicts << {
267
+ conflict_id: "ruby-unmanaged-#{Digest::SHA256.hexdigest([base, ours, theirs].join("\0"))[0, 16]}",
268
+ category: :unmanaged_source_change,
269
+ path: change.fetch(:path),
270
+ owner_id: nil,
271
+ change_classification: change
272
+ }.freeze
273
+ end
274
+
275
+ def side_change(base, side)
276
+ return :unchanged if base && side && base.fingerprint == side.fingerprint
277
+ return :added if !base && side
278
+ return :deleted if base && !side
279
+ return :unchanged unless base || side
280
+
281
+ :edited
282
+ end
283
+
284
+ def owner_choice(base, ours, theirs)
285
+ return :ours if equivalent?(ours, theirs)
286
+ return :theirs if equivalent?(base, ours)
287
+ return :ours if equivalent?(base, theirs)
288
+ return nil if ours.nil? && theirs.nil?
289
+
290
+ :conflict
291
+ end
292
+
293
+ def equivalent?(left, right)
294
+ return true if left.nil? && right.nil?
295
+ return false unless left && right
296
+
297
+ left.fingerprint == right.fingerprint
298
+ end
299
+
300
+ def conflict_for(id, base, ours, theirs, change)
301
+ {
302
+ conflict_id: "ruby-owner-#{id[0, 16]}",
303
+ category: base && (!ours || !theirs) ? :delete_edit : :edit_edit,
304
+ path: change.fetch(:path),
305
+ owner_id: id,
306
+ base: owner_state(base),
307
+ ours: owner_state(ours),
308
+ theirs: owner_state(theirs),
309
+ change_classification: change
310
+ }.freeze
311
+ end
312
+
313
+ def owner_state(owner)
314
+ { present: !owner.nil?, fingerprint: owner&.fingerprint }.freeze
315
+ end
316
+
317
+ def exact_revision_role(request)
318
+ base = request.fetch(:base_source)
319
+ ours = request.fetch(:ours_source)
320
+ theirs = request.fetch(:theirs_source)
321
+ return :ours if ours == theirs || base == theirs
322
+
323
+ :theirs if base == ours
324
+ end
325
+
326
+ def render_exact(request, documents, decision, role)
327
+ rendered = render_plan(request, [whole_source_fragment(role, request.fetch(:"#{role}_source"))])
328
+ verification = verify_rendered(rendered.content, documents.fetch(role).owners, role: :output)
329
+ result(
330
+ :merge3,
331
+ request,
332
+ output: rendered.content,
333
+ changes: decision.changes,
334
+ render_report: render_report(rendered, :exact_revision),
335
+ verification: verification.merge(base_participated: true)
336
+ )
337
+ end
338
+
339
+ def render_composite(request, documents, decision)
340
+ fragments = composite_fragments(documents, decision)
341
+ rendered = render_plan(request, fragments)
342
+ expected = expected_owners(documents, decision)
343
+ verification = verify_rendered(rendered.content, expected, role: :output)
344
+ unless verification[:semantic_match]
345
+ return failure(
346
+ :merge3,
347
+ request,
348
+ category: :render_failure,
349
+ message: 'Prism composite did not match the planned top-level owner structure.',
350
+ changes: decision.changes,
351
+ render_report: render_report(rendered, :exact_owner_composite),
352
+ verification: verification.merge(base_participated: true)
353
+ )
354
+ end
355
+
356
+ result(
357
+ :merge3,
358
+ request,
359
+ output: rendered.content,
360
+ changes: decision.changes,
361
+ render_report: render_report(rendered, :exact_owner_composite),
362
+ verification: verification.merge(base_participated: true)
363
+ )
364
+ end
365
+
366
+ def composite_fragments(documents, decision)
367
+ ours = documents.fetch(:ours)
368
+ edits = replacement_edits(documents, decision)
369
+ fragments = []
370
+ cursor = 1
371
+ edits.sort_by { |edit| edit.fetch(:start_line) }.each do |edit|
372
+ append_range(fragments, :ours, cursor, edit.fetch(:start_line) - 1)
373
+ replacement = edit[:owner]
374
+ append_owner(fragments, replacement) if replacement
375
+ cursor = edit.fetch(:end_line) + 1
376
+ end
377
+ append_range(fragments, :ours, cursor, ours.source.lines.length)
378
+ append_additions(fragments, documents, decision)
379
+ fragments
380
+ end
381
+
382
+ def replacement_edits(documents, decision)
383
+ ours = documents.fetch(:ours)
384
+ decision.choices.filter_map do |id, role|
385
+ ours_owner = ours.by_id[id]
386
+ next unless ours_owner
387
+
388
+ chosen = role && documents.fetch(role).by_id[id]
389
+ next if role == :ours
390
+
391
+ { start_line: ours_owner.start_line, end_line: ours_owner.end_line, owner: chosen }
392
+ end
393
+ end
394
+
395
+ def append_additions(fragments, documents, decision)
396
+ ours = documents.fetch(:ours)
397
+ additions = documents.fetch(:theirs).owners.select do |owner|
398
+ !ours.by_id.key?(owner.id) && decision.choices[owner.id] == :theirs
399
+ end
400
+ return if additions.empty?
401
+
402
+ ensure_plan_boundary!(fragments, documents)
403
+ additions.each_with_index do |owner, index|
404
+ append_owner(fragments, owner)
405
+ ensure_plan_boundary!(fragments, documents) if index < additions.length - 1
406
+ end
407
+ end
408
+
409
+ def ensure_plan_boundary!(fragments, documents)
410
+ fragment = fragments.last
411
+ return if fragment.nil? || fragment_content(fragment, documents).end_with?("\n")
412
+
413
+ if fragment.is_a?(Ast::Merge::SourceRender::SourceFragment) && fragment.start_line < fragment.end_line
414
+ fragments[-1] = source_range_fragment(fragment.revision, fragment.start_line, fragment.end_line - 1)
415
+ else
416
+ fragments.pop
417
+ end
418
+ content = if fragment.is_a?(Ast::Merge::SourceRender::SourceFragment)
419
+ documents.fetch(fragment.revision).source.lines.fetch(fragment.end_line - 1)
420
+ else
421
+ fragment.content
422
+ end
423
+ fragments << Ast::Merge::SourceRender::SynthesizedFragment.new(
424
+ content: "#{content}\n",
425
+ reason: :owner_separator,
426
+ producer: provider_id,
427
+ metadata: { copied_source: true }
428
+ )
429
+ end
430
+
431
+ def fragment_content(fragment, documents)
432
+ return fragment.content if fragment.is_a?(Ast::Merge::SourceRender::SynthesizedFragment)
433
+
434
+ document = documents.fetch(fragment.revision)
435
+ document.source.lines.slice(fragment.start_line - 1, fragment.end_line - fragment.start_line + 1).join
436
+ end
437
+
438
+ def append_owner(fragments, owner)
439
+ fragments << source_range_fragment(owner.role, owner.start_line, owner.end_line)
440
+ end
441
+
442
+ def append_range(fragments, role, start_line, end_line)
443
+ return if start_line > end_line
444
+
445
+ fragments << source_range_fragment(role, start_line, end_line)
446
+ end
447
+
448
+ def expected_owners(documents, decision)
449
+ ours = documents.fetch(:ours)
450
+ expected = ours.owners.filter_map do |owner|
451
+ role = decision.choices[owner.id]
452
+ role && documents.fetch(role).by_id[owner.id]
453
+ end
454
+ documents.fetch(:theirs).owners.each do |owner|
455
+ next if ours.by_id.key?(owner.id)
456
+ next unless decision.choices[owner.id] == :theirs
457
+
458
+ expected << owner
459
+ end
460
+ expected
461
+ end
462
+
463
+ def verify_rendered(output, expected, role:)
464
+ parsed = parse_source(output, role)
465
+ if provider_failure?(parsed)
466
+ return {
467
+ output_reparsed: false,
468
+ semantic_match: false,
469
+ parse_error: parsed[:parse_error]
470
+ }
471
+ end
472
+
473
+ actual = parsed.owners.map { |owner| [owner.id, owner.fingerprint] }
474
+ planned = expected.map { |owner| [owner.id, owner.fingerprint] }
475
+ {
476
+ output_reparsed: true,
477
+ semantic_match: actual == planned,
478
+ planned_owner_count: planned.length,
479
+ output_owner_count: actual.length
480
+ }
481
+ end
482
+
483
+ def render_conflicts(request, documents, decision)
484
+ localized = localized_conflict_fragments(request, documents, decision)
485
+ if localized
486
+ rendered = render_plan(request, localized)
487
+ return conflict_failure(request, decision, rendered, :owner_localized_conflict)
488
+ end
489
+
490
+ rendered = render_plan(request, [whole_document_conflict(request, decision)])
491
+ conflict_failure(
492
+ request,
493
+ decision,
494
+ rendered,
495
+ :full_file_conflict,
496
+ fallbacks: [{ from: :owner_localization, to: :full_file_conflict, reason: :owner_not_addressable }]
497
+ )
498
+ end
499
+
500
+ def localized_conflict_fragments(request, documents, decision)
501
+ ours = documents.fetch(:ours)
502
+ return unless decision.conflicts.all? do |conflict|
503
+ ours.by_id.key?(conflict.fetch(:owner_id))
504
+ end
505
+
506
+ ranges = decision.conflicts.map do |conflict|
507
+ [conflict, ours.by_id.fetch(conflict.fetch(:owner_id))]
508
+ end
509
+ return unless ranges.sort_by { |_conflict, owner| owner.start_line }.each_cons(2).none? do |left, right|
510
+ left.last.end_line >= right.last.start_line
511
+ end
512
+
513
+ fragments = []
514
+ cursor = 1
515
+ ranges.sort_by { |_conflict, owner| owner.start_line }.each do |conflict, owner|
516
+ append_range(fragments, :ours, cursor, owner.start_line - 1)
517
+ fragments << owner_conflict_fragment(request, documents, conflict)
518
+ cursor = owner.end_line + 1
519
+ end
520
+ append_range(fragments, :ours, cursor, ours.source.lines.length)
521
+ fragments
522
+ end
523
+
524
+ def owner_conflict_fragment(request, documents, conflict)
525
+ Ast::Merge::SourceRender::ConflictFragment.new(
526
+ conflict_id: conflict.fetch(:conflict_id),
527
+ base: conflict_owner_side(documents, conflict, :base),
528
+ ours: conflict_owner_side(documents, conflict, :ours),
529
+ theirs: conflict_owner_side(documents, conflict, :theirs),
530
+ labels: request.fetch(:labels, {}),
531
+ marker_size: request.fetch(:conflict_marker_size, 7),
532
+ metadata: { path: conflict.fetch(:path), category: conflict.fetch(:category) }
533
+ )
534
+ end
535
+
536
+ def conflict_owner_side(documents, conflict, role)
537
+ document = documents.fetch(role)
538
+ owner = document.by_id[conflict.fetch(:owner_id)]
539
+ owner ? [conflict_source_fragment(owner, document.source)] : []
540
+ end
541
+
542
+ def whole_document_conflict(request, decision)
543
+ Ast::Merge::SourceRender::ConflictFragment.new(
544
+ conflict_id: decision.conflicts.first.fetch(:conflict_id),
545
+ base: [conflict_whole_source_fragment(:base, request.fetch(:base_source))],
546
+ ours: [conflict_whole_source_fragment(:ours, request.fetch(:ours_source))],
547
+ theirs: [conflict_whole_source_fragment(:theirs, request.fetch(:theirs_source))],
548
+ labels: request.fetch(:labels, {}),
549
+ marker_size: request.fetch(:conflict_marker_size, 7),
550
+ metadata: { conflicts: decision.conflicts.map { |conflict| conflict[:conflict_id] } }
551
+ )
552
+ end
553
+
554
+ def conflict_source_fragment(owner, source)
555
+ fragment_source = source.lines.slice(owner.start_line - 1, owner.end_line - owner.start_line + 1).join
556
+ return source_range_fragment(owner.role, owner.start_line, owner.end_line) if fragment_source.end_with?("\n")
557
+
558
+ Ast::Merge::SourceRender::SynthesizedFragment.new(
559
+ content: "#{fragment_source}\n",
560
+ reason: :conflict_line_boundary,
561
+ producer: provider_id,
562
+ metadata: { source_role: owner.role, copied_source: true }
563
+ )
564
+ end
565
+
566
+ def conflict_whole_source_fragment(role, source)
567
+ return whole_source_fragment(role, source) if source.empty? || source.end_with?("\n")
568
+
569
+ Ast::Merge::SourceRender::SynthesizedFragment.new(
570
+ content: "#{source}\n",
571
+ reason: :conflict_line_boundary,
572
+ producer: provider_id,
573
+ metadata: { source_role: role, copied_source: true }
574
+ )
575
+ end
576
+
577
+ def whole_source_fragment(role, source)
578
+ return empty_source_fragment(role) if source.empty?
579
+
580
+ source_range_fragment(role, 1, source.lines.length)
581
+ end
582
+
583
+ def empty_source_fragment(role)
584
+ Ast::Merge::SourceRender::SynthesizedFragment.new(
585
+ content: '',
586
+ reason: :exact_empty_source,
587
+ producer: provider_id,
588
+ metadata: { source_role: role }
589
+ )
590
+ end
591
+
592
+ def source_range_fragment(role, start_line, end_line)
593
+ Ast::Merge::SourceRender::SourceFragment.new(
594
+ revision: role,
595
+ start_line: start_line,
596
+ end_line: end_line,
597
+ metadata: { source_role: role }
598
+ )
599
+ end
600
+
601
+ def render_plan(request, fragments)
602
+ Ast::Merge::SourceRender::Renderer.new.render(
603
+ Ast::Merge::SourceRender::Plan.new(
604
+ sources: {
605
+ base: request[:base_source].to_s,
606
+ ours: request[:ours_source].to_s,
607
+ theirs: request[:theirs_source].to_s
608
+ },
609
+ fragments: fragments,
610
+ metadata: { provider_id: provider_id }
611
+ )
612
+ )
613
+ end
614
+
615
+ def render_report(rendered, strategy)
616
+ {
617
+ strategy: strategy,
618
+ line_records: rendered.line_records,
619
+ synthesized_fragments: rendered.synthesized_fragments,
620
+ conflicts: rendered.conflicts,
621
+ verification_input: rendered.verification_input
622
+ }
623
+ end
624
+
625
+ def conflict_failure(request, decision, rendered, strategy, fallbacks: [])
626
+ failure(
627
+ :merge3,
628
+ request,
629
+ category: :merge_conflict,
630
+ message: 'Ruby top-level owner changed incompatibly on both sides.',
631
+ changes: decision.changes,
632
+ conflicts: decision.conflicts,
633
+ conflicted_output: rendered.content,
634
+ render_report: render_report(rendered, strategy),
635
+ verification: { base_participated: true },
636
+ fallbacks: fallbacks
637
+ )
638
+ end
639
+
640
+ def merge2_failure(request, merged)
641
+ failure(
642
+ :merge2,
643
+ request,
644
+ category: :merge_failure,
645
+ message: Array(merged[:diagnostics]).map { |item| item[:message] }.compact.join('; ')
646
+ )
647
+ end
648
+
649
+ def parse_failure(operation, request, role, parsed)
650
+ detail = parsed[:parse_error] || "ambiguous duplicate owner #{parsed[:ambiguous_owner].inspect}"
651
+ failure(
652
+ operation,
653
+ request,
654
+ category: parsed[:parse_error] ? :parse_error : :ambiguous_owner,
655
+ message: "#{role} parse error: #{detail}",
656
+ source_role: role
657
+ )
658
+ end
659
+
660
+ def render_failure(operation, request, parsed)
661
+ failure(
662
+ operation,
663
+ request,
664
+ category: :render_failure,
665
+ message: "Prism emitter produced invalid output: #{parsed[:parse_error]}",
666
+ source_role: :output
667
+ )
668
+ end
669
+
670
+ def provider_failure?(value)
671
+ value.is_a?(Hash) && (value[:ok] == false || value.key?(:parse_error) || value.key?(:ambiguous_owner))
672
+ end
673
+
674
+ def result(operation, request, changes: [], render_report: {}, verification: {}, **payload)
675
+ Ast::Merge::ProviderResult.build(
676
+ operation: operation,
677
+ success: true,
678
+ envelope: envelope(request, changes: changes, render_report: render_report, verification: verification),
679
+ **payload
680
+ )
681
+ end
682
+
683
+ def failure(operation, request, category:, message:, changes: [], conflicts: [], fallbacks: [],
684
+ render_report: {}, verification: {}, **payload)
685
+ Ast::Merge::ProviderResult.build(
686
+ operation: operation,
687
+ success: false,
688
+ envelope: envelope(
689
+ request,
690
+ changes: changes,
691
+ conflicts: conflicts,
692
+ fallbacks: fallbacks,
693
+ diagnostics: [{ category: category, severity: :error, message: message, blocking: true }],
694
+ render_report: render_report,
695
+ verification: verification
696
+ ),
697
+ **payload
698
+ )
699
+ end
700
+
701
+ def envelope(request, **fields)
702
+ {
703
+ provider: {
704
+ provider_id: provider_id,
705
+ family: family,
706
+ dialect: request[:dialect] || :ruby,
707
+ backend: request[:backend] || :prism,
708
+ package: Prism::Merge::PACKAGE_NAME,
709
+ package_version: Prism::Merge::Version::VERSION
710
+ },
711
+ profile: { profile_id: request[:profile_id] || DEFAULT_PROFILE }
712
+ }.merge(fields)
713
+ end
714
+ end
715
+ # rubocop:enable Metrics/AbcSize, Metrics/ClassLength, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/ParameterLists, Metrics/PerceivedComplexity
716
+ end
717
+ end
@@ -5,7 +5,7 @@ module Prism
5
5
  # Version namespace for this gem.
6
6
  module Version
7
7
  # Current gem version.
8
- VERSION = '7.1.1'
8
+ VERSION = '7.1.4'
9
9
  end
10
10
  # Current gem version exposed at the traditional constant location.
11
11
  VERSION = Version::VERSION # Traditional Constant Location
data/lib/prism/merge.rb CHANGED
@@ -7,6 +7,9 @@ require 'tree_haver'
7
7
  require_relative 'merge/version'
8
8
 
9
9
  module Prism
10
+ # This public facade keeps provider protocol and backend registration
11
+ # entrypoints together; implementation details live in autoloaded files.
12
+ # rubocop:disable Metrics/ModuleLength
10
13
  module Merge
11
14
  module_function
12
15
 
@@ -62,6 +65,7 @@ module Prism
62
65
  autoload :NodeWrapper, 'prism/merge/node_wrapper'
63
66
  autoload :PartialTemplateMerger, 'prism/merge/partial_template_merger'
64
67
  autoload :PartialTemplateNode, 'prism/merge/partial_template_node'
68
+ autoload :Provider, 'prism/merge/provider'
65
69
  autoload :RecursiveMergePolicy, 'prism/merge/recursive_merge_policy'
66
70
  autoload :RecursiveNodeBodyMerger, 'prism/merge/recursive_node_body_merger'
67
71
  autoload :RubyDocSurfaceAnalyzer, 'prism/merge/ruby_doc_surface_analyzer'
@@ -90,6 +94,14 @@ module Prism
90
94
  Ruby::Merge.ruby_feature_profile
91
95
  end
92
96
 
97
+ def merge_provider
98
+ @merge_provider ||= Provider.new
99
+ end
100
+
101
+ def register_provider!(replace: false)
102
+ Ast::Merge.register_provider(merge_provider, replace: replace)
103
+ end
104
+
93
105
  def available_ruby_backends
94
106
  [BACKEND_REFERENCE]
95
107
  end
@@ -300,6 +312,8 @@ module Prism
300
312
  }
301
313
  end
302
314
 
315
+ # This projection mirrors the nested public schema intentionally.
316
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
303
317
  def ruby_structured_edit_batch_report_projection
304
318
  content = "class App\n # managed snippet\n old_call\n\n anchor_call\n\n # obsolete snippet\n obsolete_call\nend\n"
305
319
  {
@@ -441,7 +455,10 @@ module Prism
441
455
  )
442
456
  }
443
457
  end
458
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
444
459
 
460
+ # Keep parser validation and diagnostics in one boundary method.
461
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
445
462
  def parse_ruby(source, dialect, backend: nil)
446
463
  requested = backend.to_s.empty? ? BACKEND_REFERENCE.id : backend.to_s
447
464
  return unsupported_feature_result("Unsupported Ruby dialect #{dialect}.") unless dialect == 'ruby'
@@ -469,6 +486,7 @@ module Prism
469
486
  policies: []
470
487
  }
471
488
  end
489
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
472
490
 
473
491
  def analyze_ruby_document(source, parse_result)
474
492
  normalized_source = prism_ruby_normalize_source(source)
@@ -485,6 +503,8 @@ module Prism
485
503
  }
486
504
  end
487
505
 
506
+ # Normalization builds the complete TreeHaver contract in one pass.
507
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
488
508
  def parse_ruby_normalized(source, dialect = 'ruby', backend: nil)
489
509
  requested = backend.to_s.empty? ? BACKEND_REFERENCE.id : backend.to_s
490
510
  return unsupported_feature_result("Unsupported Ruby dialect #{dialect}.") unless dialect == 'ruby'
@@ -566,7 +586,10 @@ module Prism
566
586
  metadata: prism_normalized_metadata
567
587
  ).to_h
568
588
  end
589
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
569
590
 
591
+ # This adapter validates, edits, reparses, and reports one projection atomically.
592
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
570
593
  def apply_edit_projection(request)
571
594
  source = request.fetch(:source)
572
595
  provider_id = request.fetch(:provider_id)
@@ -638,6 +661,7 @@ module Prism
638
661
  []
639
662
  ).to_h
640
663
  end
664
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
641
665
 
642
666
  def match_ruby_owners(template, destination)
643
667
  Ruby::Merge.match_ruby_owners(template, destination)
@@ -721,6 +745,8 @@ module Prism
721
745
  end
722
746
  end
723
747
 
748
+ # Alias normalization intentionally handles the supported input shapes inline.
749
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
724
750
  def normalize_ruby_require_aliases(require_aliases)
725
751
  Array(require_aliases).each_with_object({}) do |entry, aliases|
726
752
  case entry
@@ -741,6 +767,7 @@ module Prism
741
767
  aliases[to] = canonical
742
768
  end
743
769
  end
770
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
744
771
 
745
772
  def ruby_dsl_call_node(node)
746
773
  return node if node.is_a?(::Prism::CallNode)
@@ -765,6 +792,8 @@ module Prism
765
792
  end
766
793
  end
767
794
 
795
+ # The reviewed merge boundary must retain all protocol roles for replay.
796
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/ParameterLists
768
797
  def merge_ruby_with_reviewed_nested_outputs(template_source, destination_source, dialect, review_state,
769
798
  applied_children, backend: nil)
770
799
  requested = backend.to_s.empty? ? BACKEND_REFERENCE.id : backend.to_s
@@ -797,6 +826,7 @@ module Prism
797
826
  }
798
827
  )
799
828
  end
829
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength, Metrics/ParameterLists
800
830
 
801
831
  def merge_ruby_with_reviewed_nested_outputs_from_replay_bundle(template_source, destination_source, dialect,
802
832
  replay_bundle, backend: nil)
@@ -924,6 +954,8 @@ module Prism
924
954
  end.sort_by { |owner| owner[:path] }
925
955
  end
926
956
 
957
+ # Surface discovery intentionally keeps line ownership and comment state together.
958
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
927
959
  def prism_ruby_discovered_surfaces(source, body)
928
960
  owners_by_line = body.filter_map do |node|
929
961
  name = prism_ruby_declaration_name(node)
@@ -954,7 +986,10 @@ module Prism
954
986
 
955
987
  surfaces
956
988
  end
989
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
957
990
 
991
+ # A surface record is assembled as one schema-shaped value.
992
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
958
993
  def prism_ruby_surfaces_for_owner(owner_name, comment_entries)
959
994
  filtered_entries = comment_entries.filter { |entry| prism_ruby_doc_comment_content?(entry.fetch(:raw)) }
960
995
  return [] if filtered_entries.empty?
@@ -979,7 +1014,10 @@ module Prism
979
1014
 
980
1015
  [doc_surface] + prism_ruby_example_surfaces(doc_surface)
981
1016
  end
1017
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
982
1018
 
1019
+ # Example surfaces preserve the complete delegated-child metadata contract.
1020
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
983
1021
  def prism_ruby_example_surfaces(surface)
984
1022
  entries = Array(surface.dig(:metadata, :entries))
985
1023
  Ruby::Merge::DocCommentSupport.example_blocks(entries).map do |block|
@@ -1004,6 +1042,7 @@ module Prism
1004
1042
  )
1005
1043
  end
1006
1044
  end
1045
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
1007
1046
 
1008
1047
  def prism_ruby_declaration_name(node)
1009
1048
  case node
@@ -1162,6 +1201,8 @@ module Prism
1162
1201
  end
1163
1202
  end
1164
1203
 
1204
+ # Comment nodes retain backend metadata in a single normalized schema value.
1205
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
1165
1206
  def prism_comment_node(source, comment, index, magic_comment, parent_id:)
1166
1207
  directive = prism_comment_directive(
1167
1208
  source.byteslice(comment.location.start_offset...comment.location.end_offset), magic_comment
@@ -1192,6 +1233,7 @@ module Prism
1192
1233
  }
1193
1234
  )
1194
1235
  end
1236
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
1195
1237
 
1196
1238
  def prism_comment_directive(comment_text, magic_comment)
1197
1239
  if comment_text.strip.match?(/\A#\s*smorg:freeze\b/)
@@ -1330,9 +1372,11 @@ module Prism
1330
1372
  :source_span_for_location
1331
1373
  )
1332
1374
  end
1375
+ # rubocop:enable Metrics/ModuleLength
1333
1376
  end
1334
1377
 
1335
1378
  Prism::Merge.register_backend!
1379
+ Prism::Merge.register_provider!
1336
1380
 
1337
1381
  if defined?(Ast::Merge::RSpec::MergeGemRegistry)
1338
1382
  Ast::Merge::RSpec::MergeGemRegistry.register(
data/sig/prism/merge.rbs CHANGED
@@ -1,5 +1,18 @@
1
1
  module Prism
2
2
  module Merge
3
+ class Provider
4
+ def provider_id: () -> String
5
+ def family: () -> String
6
+ def capabilities: () -> Ast::Merge::provider_capabilities
7
+ def analyze: (Ast::Merge::provider_request request) -> Ast::Merge::provider_result
8
+ def diff2: (Ast::Merge::provider_request request) -> Ast::Merge::provider_result
9
+ def merge2: (Ast::Merge::provider_request request) -> Ast::Merge::provider_result
10
+ def merge3: (Ast::Merge::provider_request request) -> Ast::Merge::provider_result
11
+ end
12
+
13
+ def self.merge_provider: () -> Provider
14
+ def self.register_provider!: (?replace: bool) -> Ast::Merge::_MergeProvider
15
+
3
16
  module Version
4
17
  VERSION: String
5
18
  end
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: prism-merge
3
3
  version: !ruby/object:Gem::Version
4
- version: 7.1.1
4
+ version: 7.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter H. Boling
@@ -37,6 +37,20 @@ cert_chain:
37
37
  -----END CERTIFICATE-----
38
38
  date: 1980-01-02 00:00:00.000000000 Z
39
39
  dependencies:
40
+ - !ruby/object:Gem::Dependency
41
+ name: ast-merge
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - '='
45
+ - !ruby/object:Gem::Version
46
+ version: 7.1.4
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - '='
52
+ - !ruby/object:Gem::Version
53
+ version: 7.1.4
40
54
  - !ruby/object:Gem::Dependency
41
55
  name: prism
42
56
  requirement: !ruby/object:Gem::Requirement
@@ -57,14 +71,28 @@ dependencies:
57
71
  requirements:
58
72
  - - '='
59
73
  - !ruby/object:Gem::Version
60
- version: 7.1.1
74
+ version: 7.1.4
75
+ type: :runtime
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - '='
80
+ - !ruby/object:Gem::Version
81
+ version: 7.1.4
82
+ - !ruby/object:Gem::Dependency
83
+ name: tree_haver
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - '='
87
+ - !ruby/object:Gem::Version
88
+ version: 7.1.4
61
89
  type: :runtime
62
90
  prerelease: false
63
91
  version_requirements: !ruby/object:Gem::Requirement
64
92
  requirements:
65
93
  - - '='
66
94
  - !ruby/object:Gem::Version
67
- version: 7.1.1
95
+ version: 7.1.4
68
96
  - !ruby/object:Gem::Dependency
69
97
  name: version_gem
70
98
  requirement: !ruby/object:Gem::Requirement
@@ -91,20 +119,20 @@ dependencies:
91
119
  requirements:
92
120
  - - "~>"
93
121
  - !ruby/object:Gem::Version
94
- version: '2.5'
122
+ version: '3.0'
95
123
  - - ">="
96
124
  - !ruby/object:Gem::Version
97
- version: 2.5.17
125
+ version: 3.0.7
98
126
  type: :development
99
127
  prerelease: false
100
128
  version_requirements: !ruby/object:Gem::Requirement
101
129
  requirements:
102
130
  - - "~>"
103
131
  - !ruby/object:Gem::Version
104
- version: '2.5'
132
+ version: '3.0'
105
133
  - - ">="
106
134
  - !ruby/object:Gem::Version
107
- version: 2.5.17
135
+ version: 3.0.7
108
136
  - !ruby/object:Gem::Dependency
109
137
  name: bundler-audit
110
138
  requirement: !ruby/object:Gem::Requirement
@@ -182,7 +210,7 @@ dependencies:
182
210
  version: '3.2'
183
211
  - - ">="
184
212
  - !ruby/object:Gem::Version
185
- version: 3.2.0
213
+ version: 3.2.2
186
214
  type: :development
187
215
  prerelease: false
188
216
  version_requirements: !ruby/object:Gem::Requirement
@@ -192,7 +220,7 @@ dependencies:
192
220
  version: '3.2'
193
221
  - - ">="
194
222
  - !ruby/object:Gem::Version
195
- version: 3.2.0
223
+ version: 3.2.2
196
224
  - !ruby/object:Gem::Dependency
197
225
  name: kettle-test
198
226
  requirement: !ruby/object:Gem::Requirement
@@ -202,7 +230,7 @@ dependencies:
202
230
  version: '2.0'
203
231
  - - ">="
204
232
  - !ruby/object:Gem::Version
205
- version: 2.0.19
233
+ version: 2.0.20
206
234
  type: :development
207
235
  prerelease: false
208
236
  version_requirements: !ruby/object:Gem::Requirement
@@ -212,7 +240,7 @@ dependencies:
212
240
  version: '2.0'
213
241
  - - ">="
214
242
  - !ruby/object:Gem::Version
215
- version: 2.0.19
243
+ version: 2.0.20
216
244
  - !ruby/object:Gem::Dependency
217
245
  name: turbo_tests2
218
246
  requirement: !ruby/object:Gem::Requirement
@@ -222,7 +250,7 @@ dependencies:
222
250
  version: '3.2'
223
251
  - - ">="
224
252
  - !ruby/object:Gem::Version
225
- version: 3.2.4
253
+ version: 3.2.6
226
254
  type: :development
227
255
  prerelease: false
228
256
  version_requirements: !ruby/object:Gem::Requirement
@@ -232,7 +260,7 @@ dependencies:
232
260
  version: '3.2'
233
261
  - - ">="
234
262
  - !ruby/object:Gem::Version
235
- version: 3.2.4
263
+ version: 3.2.6
236
264
  - !ruby/object:Gem::Dependency
237
265
  name: ruby-progressbar
238
266
  requirement: !ruby/object:Gem::Requirement
@@ -276,7 +304,7 @@ dependencies:
276
304
  version: '2.0'
277
305
  - - ">="
278
306
  - !ruby/object:Gem::Version
279
- version: 2.0.11
307
+ version: 2.0.12
280
308
  type: :development
281
309
  prerelease: false
282
310
  version_requirements: !ruby/object:Gem::Requirement
@@ -286,7 +314,7 @@ dependencies:
286
314
  version: '2.0'
287
315
  - - ">="
288
316
  - !ruby/object:Gem::Version
289
- version: 2.0.11
317
+ version: 2.0.12
290
318
  description: "☯️ Prism-backed Ruby provider gem for the Structured Merge Ruby family."
291
319
  email:
292
320
  - floss@galtzo.com
@@ -330,6 +358,7 @@ files:
330
358
  - lib/prism/merge/node_wrapper.rb
331
359
  - lib/prism/merge/partial_template_merger.rb
332
360
  - lib/prism/merge/partial_template_node.rb
361
+ - lib/prism/merge/provider.rb
333
362
  - lib/prism/merge/recursive_merge_policy.rb
334
363
  - lib/prism/merge/recursive_node_body_merger.rb
335
364
  - lib/prism/merge/ruby_doc_surface_analyzer.rb
@@ -346,10 +375,10 @@ licenses:
346
375
  - PolyForm-Small-Business-1.0.0
347
376
  metadata:
348
377
  homepage_uri: https://structuredmerge.org
349
- source_code_uri: https://github.com/structuredmerge/structuredmerge-ruby/tree/v7.1.1
350
- changelog_uri: https://github.com/structuredmerge/structuredmerge-ruby/blob/v7.1.1/CHANGELOG.md
378
+ source_code_uri: https://github.com/structuredmerge/structuredmerge-ruby/tree/v7.1.4
379
+ changelog_uri: https://github.com/structuredmerge/structuredmerge-ruby/blob/v7.1.4/CHANGELOG.md
351
380
  bug_tracker_uri: https://github.com/structuredmerge/structuredmerge-ruby/issues
352
- documentation_uri: https://www.rubydoc.info/gems/prism-merge/7.1.1
381
+ documentation_uri: https://www.rubydoc.info/gems/prism-merge/7.1.4
353
382
  funding_uri: https://github.com/sponsors/pboling
354
383
  wiki_uri: https://github.com/structuredmerge/structuredmerge-ruby/wiki
355
384
  news_uri: https://www.railsbling.com/tags/prism-merge
metadata.gz.sig CHANGED
Binary file