json-merge 1.1.2 → 7.1.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.
@@ -2,137 +2,95 @@
2
2
 
3
3
  module Json
4
4
  module Merge
5
- # Wraps TreeHaver nodes with comment associations, line information, and signatures.
6
- # This provides a unified interface for working with JSON AST nodes during merging.
7
- #
8
- # Inherits common functionality from Ast::Merge::NodeWrapperBase:
9
- # - Source context (lines, source, comments)
10
- # - Line info extraction
11
- # - Basic methods: #type, #type?, #text, #content, #signature
12
- #
13
- # @example Basic usage
14
- # parser = TreeHaver::Parser.new
15
- # parser.language = TreeHaver::Language.json
16
- # tree = parser.parse(source)
17
- # wrapper = NodeWrapper.new(tree.root_node, lines: source.lines, source: source)
18
- # wrapper.signature # => [:object, ...]
19
- #
20
- # @see Ast::Merge::NodeWrapperBase
5
+ # Wraps TreeHaver nodes with line information and signatures for JSON /
6
+ # JSONC merging.
21
7
  class NodeWrapper < Ast::Merge::NodeWrapperBase
22
- # Check if this is a JSON object
23
- # @return [Boolean]
8
+ attr_reader :dialect
9
+
10
+ def initialize(node, lines:, source: nil, dialect: :jsonc, **options)
11
+ @dialect = dialect.to_sym
12
+ super(node, lines: lines, source: source, **options)
13
+ end
14
+
24
15
  def object?
25
- @node.type.to_s == "object"
16
+ @node.type.to_s == 'object'
26
17
  end
27
18
 
28
- # Check if this is a JSON array
29
- # @return [Boolean]
30
19
  def array?
31
- @node.type.to_s == "array"
20
+ @node.type.to_s == 'array'
32
21
  end
33
22
 
34
- # Check if this is a JSON string
35
- # @return [Boolean]
36
23
  def string?
37
- @node.type.to_s == "string"
24
+ @node.type.to_s == 'string'
38
25
  end
39
26
 
40
- # Check if this is a JSON number
41
- # @return [Boolean]
42
27
  def number?
43
- @node.type.to_s == "number"
28
+ @node.type.to_s == 'number'
44
29
  end
45
30
 
46
- # Check if this is a JSON boolean (true/false)
47
- # @return [Boolean]
48
31
  def boolean?
49
32
  %w[true false].include?(@node.type.to_s)
50
33
  end
51
34
 
52
- # Check if this is a JSON null
53
- # @return [Boolean]
54
35
  def null?
55
- @node.type.to_s == "null"
36
+ @node.type.to_s == 'null'
56
37
  end
57
38
 
58
- # Check if this is a key-value pair
59
- # @return [Boolean]
60
39
  def pair?
61
- @node.type.to_s == "pair"
40
+ @node.type.to_s == 'pair'
62
41
  end
63
42
 
64
- # Check if this is a comment
65
- # @return [Boolean]
66
43
  def comment?
67
- @node.type.to_s == "comment"
44
+ @node.type.to_s == 'comment'
68
45
  end
69
46
 
70
- # Get the key name if this is a pair node
71
- # @return [String, nil]
72
47
  def key_name
73
48
  return unless pair?
74
49
 
75
- # In JSON tree-sitter, pair has key and value children
76
- key_node = find_child_by_field("key")
77
-
50
+ key_node = find_child_by_field('key') || semantic_children.first
78
51
  return unless key_node
79
52
 
80
- # Key is typically a string, extract its content without quotes using byte positions
81
- key_text = node_text(key_node)
82
- # Remove surrounding quotes if present
83
- key_text&.gsub(/\A"|"\z/, "")
53
+ Json::Merge.object_key_for_literal(node_text(key_node), dialect: dialect)
84
54
  end
85
55
 
86
- # Get the value node if this is a pair
87
- # @return [NodeWrapper, nil]
88
56
  def value_node
89
57
  return unless pair?
90
58
 
91
- value = find_child_by_field("value")
92
-
59
+ value = find_child_by_field('value') || semantic_children[1]
93
60
  return unless value
94
61
 
95
- NodeWrapper.new(value, lines: @lines, source: @source)
62
+ NodeWrapper.new(value, lines: @lines, source: @source, dialect: dialect)
96
63
  end
97
64
 
98
- # Get key-value pairs if this is an object
99
- # @return [Array<NodeWrapper>]
100
65
  def pairs
101
66
  return [] unless object?
102
67
 
103
68
  result = []
104
69
  @node.each do |child|
105
- next if child.type.to_s == "comment"
106
- next unless child.type.to_s == "pair"
70
+ next if child.type.to_s == 'comment'
71
+ next unless child.type.to_s == 'pair'
107
72
 
108
- result << NodeWrapper.new(child, lines: @lines, source: @source)
73
+ result << NodeWrapper.new(child, lines: @lines, source: @source, dialect: dialect)
109
74
  end
110
75
  result
111
76
  end
112
77
 
113
- # Get array elements if this is an array
114
- # @return [Array<NodeWrapper>]
115
78
  def elements
116
79
  return [] unless array?
117
80
 
118
81
  result = []
119
82
  @node.each do |child|
120
83
  child_type = child.type.to_s
121
- # Skip punctuation and comments
122
- next if child_type == "comment"
123
- next if child_type == ","
124
- next if child_type == "["
125
- next if child_type == "]"
84
+ next if child_type == 'comment'
85
+ next if child_type == ','
86
+ next if child_type == '['
87
+ next if child_type == ']'
126
88
 
127
- result << NodeWrapper.new(child, lines: @lines, source: @source)
89
+ result << NodeWrapper.new(child, lines: @lines, source: @source, dialect: dialect)
128
90
  end
129
91
  result
130
92
  end
131
93
 
132
- # Get mergeable children - the semantically meaningful children for tree merging
133
- # For objects, returns pairs. For arrays, returns elements.
134
- # For other node types, returns empty array (leaf nodes).
135
- # @return [Array<NodeWrapper>]
136
94
  def mergeable_children
137
95
  case type
138
96
  when :object
@@ -144,73 +102,55 @@ module Json
144
102
  end
145
103
  end
146
104
 
147
- # Check if this node is a container (has mergeable children)
148
- # @return [Boolean]
149
105
  def container?
150
106
  object? || array?
151
107
  end
152
108
 
153
- # Check if this is a root-level container (direct child of document)
154
- # Root-level containers get a generic signature so they always match.
155
- # @return [Boolean]
156
109
  def root_level_container?
157
110
  return false unless container?
158
111
 
159
- # Check if parent is a document node
160
112
  parent_node = @node.parent if @node.respond_to?(:parent)
161
113
  return false unless parent_node
162
114
 
163
- parent_node.type.to_s == "document"
115
+ parent_node.type.to_s == 'document'
164
116
  end
165
117
 
166
- # Get the opening line for a container node (the line with { or [)
167
- # Returns the full line content including any leading whitespace
168
- # @return [String, nil]
169
118
  def opening_line
170
119
  return unless container? && @start_line
171
120
 
121
+ return opening_bracket if @start_line == @end_line
122
+
172
123
  @lines[@start_line - 1]
173
124
  end
174
125
 
175
- # Get the closing line for a container node (the line with } or ])
176
- # Returns the full line content including any leading whitespace
177
- # @return [String, nil]
178
126
  def closing_line
179
127
  return unless container? && @end_line
180
128
 
129
+ return closing_bracket if @start_line == @end_line
130
+
181
131
  @lines[@end_line - 1]
182
132
  end
183
133
 
184
- # Get the opening bracket character for this container
185
- # @return [String, nil]
186
134
  def opening_bracket
187
- return "{" if object?
188
- return "[" if array?
135
+ return '{' if object?
136
+ return '[' if array?
189
137
 
190
138
  nil
191
139
  end
192
140
 
193
- # Get the closing bracket character for this container
194
- # @return [String, nil]
195
141
  def closing_bracket
196
- return "}" if object?
197
- return "]" if array?
142
+ return '}' if object?
143
+ return ']' if array?
198
144
 
199
145
  nil
200
146
  end
201
147
 
202
- # Find a child by field name
203
- # @param field_name [String] Field name to look for
204
- # @return [TreeSitter::Node, nil]
205
148
  def find_child_by_field(field_name)
206
149
  return unless @node.respond_to?(:child_by_field_name)
207
150
 
208
151
  @node.child_by_field_name(field_name)
209
152
  end
210
153
 
211
- # Find a child by type
212
- # @param type_name [String] Type name to look for
213
- # @return [TreeSitter::Node, nil]
214
154
  def find_child_by_type(type_name)
215
155
  return unless @node.respond_to?(:each)
216
156
 
@@ -220,67 +160,63 @@ module Json
220
160
  nil
221
161
  end
222
162
 
163
+ def semantic_children
164
+ return [] unless @node.respond_to?(:each)
165
+
166
+ @node.each.filter_map do |child|
167
+ child_type = child.type.to_s
168
+ next if %w[comment , : { } [ ]].include?(child_type)
169
+
170
+ child
171
+ end
172
+ end
173
+
223
174
  protected
224
175
 
225
- # Override wrap_child to use Json::Merge::NodeWrapper
226
176
  def wrap_child(child)
227
- NodeWrapper.new(child, lines: @lines, source: @source)
177
+ NodeWrapper.new(child, lines: @lines, source: @source, dialect: dialect)
228
178
  end
229
179
 
230
180
  def compute_signature(node)
231
181
  node_type = node.type.to_s
232
182
 
233
183
  case node_type
234
- when "document"
235
- # Root document - signature based on root content type
184
+ when 'document'
236
185
  child = nil
237
- node.each { |c|
238
- child = c unless c.type.to_s == "comment"
186
+ node.each do |candidate|
187
+ child = candidate unless candidate.type.to_s == 'comment'
239
188
  break if child
240
- }
189
+ end
241
190
  child_type = child&.type&.to_s
242
191
  [:document, child_type]
243
- when "object"
244
- # For root-level objects (direct child of document), use a generic signature
245
- # that always matches so merging happens at the pair level.
246
- # This is critical for JSON merging - there's typically only one root object/array.
192
+ when 'object'
247
193
  if root_level_container?
248
194
  [:root_object]
249
195
  else
250
- # Nested objects identified by their keys
251
196
  keys = extract_object_keys(node)
252
197
  [:object, keys.sort]
253
198
  end
254
- when "array"
255
- # For root-level arrays (direct child of document), use a generic signature
199
+ when 'array'
256
200
  if root_level_container?
257
201
  [:root_array]
258
202
  else
259
- # Nested arrays identified by their length and first few elements
260
203
  elements_count = 0
261
- node.each { |c| elements_count += 1 unless %w[comment , \[ \]].include?(c.type.to_s) }
204
+ node.each { |candidate| elements_count += 1 unless %w[comment , \[ \]].include?(candidate.type.to_s) }
262
205
  [:array, elements_count]
263
206
  end
264
- when "pair"
265
- # Pairs identified by their key name
266
- key = key_name
267
- [:pair, key]
268
- when "string"
269
- # Strings identified by their content
207
+ when 'pair'
208
+ [:pair, key_name]
209
+ when 'string'
270
210
  [:string, node_text(node)]
271
- when "number"
272
- # Numbers identified by their value
211
+ when 'number'
273
212
  [:number, node_text(node)]
274
- when "true", "false"
275
- # Booleans
213
+ when 'true', 'false'
276
214
  [:boolean, node.type.to_s]
277
- when "null"
215
+ when 'null'
278
216
  [:null]
279
- when "comment"
280
- # Comments identified by their content
217
+ when 'comment'
281
218
  [:comment, node_text(node)&.strip]
282
219
  else
283
- # Generic fallback
284
220
  content_preview = node_text(node)&.slice(0, 50)&.strip
285
221
  [node_type.to_sym, content_preview]
286
222
  end
@@ -291,13 +227,28 @@ module Json
291
227
  def extract_object_keys(object_node)
292
228
  keys = []
293
229
  object_node.each do |child|
294
- next unless child.type.to_s == "pair"
295
-
296
- key_node = child.respond_to?(:child_by_field_name) ? child.child_by_field_name("key") : nil
230
+ next unless child.type.to_s == 'pair'
231
+
232
+ key_node = child.respond_to?(:child_by_field_name) ? child.child_by_field_name('key') : nil
233
+
234
+ unless key_node
235
+ begin
236
+ child_each = child.method(:each)
237
+ child_each.call do |pair_child|
238
+ pair_child_type = pair_child.type.to_s
239
+ next if [':', 'comment'].include?(pair_child_type)
240
+
241
+ key_node = pair_child
242
+ break
243
+ end
244
+ rescue StandardError
245
+ next
246
+ end
247
+ end
297
248
 
298
249
  next unless key_node
299
250
 
300
- key_text = node_text(key_node)&.gsub(/\A"|"\z/, "")
251
+ key_text = Json::Merge.object_key_for_literal(node_text(key_node), dialect: dialect)
301
252
  keys << key_text if key_text
302
253
  end
303
254
  keys
@@ -44,7 +44,8 @@ module Json
44
44
  # @param threshold [Float] Minimum score to accept a match (default: 0.5)
45
45
  # @param key_weight [Float] Weight for key similarity (default: 0.7)
46
46
  # @param value_weight [Float] Weight for value similarity (default: 0.3)
47
- def initialize(threshold: DEFAULT_THRESHOLD, key_weight: DEFAULT_KEY_WEIGHT, value_weight: DEFAULT_VALUE_WEIGHT, **options)
47
+ def initialize(threshold: DEFAULT_THRESHOLD, key_weight: DEFAULT_KEY_WEIGHT, value_weight: DEFAULT_VALUE_WEIGHT,
48
+ **options)
48
49
  super(threshold: threshold, **options)
49
50
  @key_weight = key_weight
50
51
  @value_weight = value_weight
@@ -58,7 +59,7 @@ module Json
58
59
  # @param dest_nodes [Array] Unmatched nodes from destination
59
60
  # @param context [Hash] Additional context
60
61
  # @return [Array<MatchResult>] Array of node matches
61
- def call(template_nodes, dest_nodes, context = {})
62
+ def call(template_nodes, dest_nodes, _context = {})
62
63
  # Match object pairs (key-value entries)
63
64
  pair_matches = match_pairs(template_nodes, dest_nodes)
64
65
 
@@ -212,9 +213,9 @@ module Json
212
213
  # @return [String] Normalized key
213
214
  def normalize_key(key)
214
215
  # Convert camelCase to snake_case first, then normalize
215
- key.gsub(/([A-Z])/) { "_#{$1.downcase}" }
216
- .downcase
217
- .gsub(/[-_]/, "")
216
+ key.gsub(/([A-Z])/) { "_#{::Regexp.last_match(1).downcase}" }
217
+ .downcase
218
+ .gsub(/[-_]/, '')
218
219
  end
219
220
 
220
221
  # Compute similarity between two values.
@@ -260,8 +261,8 @@ module Json
260
261
  # @return [String]
261
262
  def extract_string_value(node)
262
263
  # String nodes include quotes, remove them
263
- text = node.respond_to?(:text) ? node.text : ""
264
- text.gsub(/\A"|"\z/, "")
264
+ text = node.respond_to?(:text) ? node.text : ''
265
+ text.gsub(/\A"|"\z/, '')
265
266
  end
266
267
 
267
268
  # Compute similarity between two arrays.
@@ -273,8 +274,8 @@ module Json
273
274
  len1 = arr1.respond_to?(:elements) ? arr1.elements.size : 0
274
275
  len2 = arr2.respond_to?(:elements) ? arr2.elements.size : 0
275
276
 
276
- return 1.0 if len1 == 0 && len2 == 0
277
- return 0.0 if len1 == 0 || len2 == 0
277
+ return 1.0 if len1.zero? && len2.zero?
278
+ return 0.0 if len1.zero? || len2.zero?
278
279
 
279
280
  [len1, len2].min.to_f / [len1, len2].max
280
281
  end
@@ -306,9 +307,7 @@ module Json
306
307
  return str1.length if str2.empty?
307
308
 
308
309
  # Ensure str1 is the shorter string for space optimization
309
- if str1.length > str2.length
310
- str1, str2 = str2, str1
311
- end
310
+ str1, str2 = str2, str1 if str1.length > str2.length
312
311
 
313
312
  m = str1.length
314
313
  n = str2.length
@@ -321,11 +320,11 @@ module Json
321
320
  curr_row[0] = j
322
321
 
323
322
  (1..m).each do |i|
324
- cost = (str1[i - 1] == str2[j - 1]) ? 0 : 1
323
+ cost = str1[i - 1] == str2[j - 1] ? 0 : 1
325
324
  curr_row[i] = [
326
325
  prev_row[i] + 1, # deletion
327
326
  curr_row[i - 1] + 1, # insertion
328
- prev_row[i - 1] + cost, # substitution
327
+ prev_row[i - 1] + cost # substitution
329
328
  ].min
330
329
  end
331
330
 
@@ -2,8 +2,7 @@
2
2
 
3
3
  module Json
4
4
  module Merge
5
- # High-level merger for JSON/JSONC content.
6
- # Orchestrates parsing, analysis, and conflict resolution.
5
+ # High-level merger for JSON / JSONC content.
7
6
  #
8
7
  # @example Basic usage
9
8
  # merger = SmartMerger.new(template_content, dest_content)
@@ -23,6 +22,10 @@ module Json
23
22
  # merger = SmartMerger.new(template, dest,
24
23
  # regions: [{ detector: SomeDetector.new, merger_class: SomeMerger }])
25
24
  class SmartMerger < ::Ast::Merge::SmartMergerBase
25
+ include ::Ast::Merge::Runtime::RootSessionSupport
26
+
27
+ attr_reader :runtime_session, :corruption_handling, :dialect
28
+
26
29
  # Creates a new SmartMerger
27
30
  #
28
31
  # @param template_content [String] Template JSON content
@@ -30,6 +33,7 @@ module Json
30
33
  # @param signature_generator [Proc, nil] Custom signature generator
31
34
  # @param preference [Symbol, Hash] :destination, :template, or per-type Hash
32
35
  # @param add_template_only_nodes [Boolean] Whether to add nodes only found in template
36
+ # @param remove_template_missing_nodes [Boolean] Whether to remove nodes missing from template
33
37
  # @param freeze_token [String, nil] Token for freeze block markers
34
38
  # @param match_refiner [#call, nil] Match refiner for fuzzy matching
35
39
  # @param regions [Array<Hash>, nil] Region configurations for nested merging
@@ -43,24 +47,42 @@ module Json
43
47
  signature_generator: nil,
44
48
  preference: :destination,
45
49
  add_template_only_nodes: false,
50
+ remove_template_missing_nodes: false,
51
+ corruption_handling: :heal,
46
52
  freeze_token: nil,
47
53
  match_refiner: nil,
48
54
  regions: nil,
49
55
  region_placeholder: nil,
50
56
  node_typing: nil,
57
+ merge_arrays: true,
58
+ preserve_atomic_formatting: false,
59
+ dialect: :jsonc,
51
60
  **options
52
61
  )
62
+ @remove_template_missing_nodes = remove_template_missing_nodes
63
+ @corruption_handling = ::Ast::Merge::Healer.normalize_mode(corruption_handling)
64
+ @merge_arrays = merge_arrays
65
+ @preserve_atomic_formatting = preserve_atomic_formatting
66
+ @dialect = dialect.to_sym
67
+ unless %i[json jsonc json5].include?(@dialect)
68
+ raise ArgumentError, "Unsupported JSON dialect #{dialect.inspect}. Expected json, jsonc, or json5."
69
+ end
70
+
53
71
  super(
54
72
  template_content,
55
73
  dest_content,
56
74
  signature_generator: signature_generator,
57
75
  preference: preference,
58
76
  add_template_only_nodes: add_template_only_nodes,
77
+ remove_template_missing_nodes: remove_template_missing_nodes,
59
78
  freeze_token: freeze_token,
60
79
  match_refiner: match_refiner,
61
80
  regions: regions,
62
81
  region_placeholder: region_placeholder,
63
82
  node_typing: node_typing,
83
+ merge_arrays: merge_arrays,
84
+ preserve_atomic_formatting: preserve_atomic_formatting,
85
+ dialect: @dialect,
64
86
  **options
65
87
  )
66
88
  end
@@ -72,7 +94,64 @@ module Json
72
94
  {
73
95
  preference: @preference,
74
96
  add_template_only_nodes: @add_template_only_nodes,
97
+ remove_template_missing_nodes: @remove_template_missing_nodes,
98
+ resolution_mode: @resolution_mode,
99
+ unresolved_policy: @unresolved_policy.to_h,
100
+ corruption_handling: @corruption_handling,
75
101
  match_refiner: @match_refiner,
102
+ dialect: @dialect
103
+ }
104
+ end
105
+
106
+ # Perform the merge operation and return the full MergeResult object.
107
+ #
108
+ # @return [MergeResult] The merge result containing merged JSON content and metadata
109
+ def merge_result
110
+ return @merge_result if @merge_result
111
+
112
+ root_operation = start_runtime_session!
113
+ @merge_result = super
114
+ complete_runtime_session!(root_operation, @merge_result)
115
+ @merge_result
116
+ rescue StandardError => e
117
+ fail_runtime_session!(root_operation, e)
118
+ raise
119
+ end
120
+
121
+ # Perform the merge and return detailed runtime-aware debug information.
122
+ #
123
+ # @return [Hash] Hash containing :content, :debug, :runtime, :statistics, and :decisions
124
+ def merge_with_debug
125
+ result_obj = merge_result
126
+ template_analysis_debug = {
127
+ valid: @template_analysis.valid?,
128
+ nodes: @template_analysis.nodes.size,
129
+ freeze_blocks: @template_analysis.freeze_blocks.size
130
+ }
131
+ dest_analysis_debug = {
132
+ valid: @dest_analysis.valid?,
133
+ nodes: @dest_analysis.nodes.size,
134
+ freeze_blocks: @dest_analysis.freeze_blocks.size
135
+ }
136
+
137
+ {
138
+ content: result_obj.to_json,
139
+ debug: {
140
+ template_nodes: template_analysis_debug[:nodes],
141
+ dest_nodes: dest_analysis_debug[:nodes],
142
+ preference: @preference,
143
+ add_template_only_nodes: @add_template_only_nodes,
144
+ remove_template_missing_nodes: @remove_template_missing_nodes,
145
+ corruption_handling: @corruption_handling,
146
+ freeze_token: @freeze_token,
147
+ runtime_operation_count: runtime_session&.operations&.size || 0,
148
+ runtime_diagnostic_count: runtime_session&.diagnostics&.size || 0
149
+ },
150
+ runtime: runtime_session&.to_h,
151
+ statistics: result_obj.statistics,
152
+ decisions: result_obj.decision_summary,
153
+ template_analysis: template_analysis_debug,
154
+ dest_analysis: dest_analysis_debug
76
155
  }
77
156
  end
78
157
 
@@ -85,7 +164,7 @@ module Json
85
164
 
86
165
  # @return [String] The default freeze token (not used for JSON)
87
166
  def default_freeze_token
88
- "json-merge"
167
+ 'json-merge'
89
168
  end
90
169
 
91
170
  # @return [Class] The resolver class for JSON files
@@ -104,10 +183,10 @@ module Json
104
183
  def perform_merge
105
184
  @resolver.resolve(@result)
106
185
 
107
- DebugLogger.debug("Merge complete", {
108
- lines: @result.line_count,
109
- decisions: @result.statistics,
110
- })
186
+ DebugLogger.debug('Merge complete', {
187
+ lines: @result.line_count,
188
+ decisions: @result.statistics
189
+ })
111
190
 
112
191
  @result
113
192
  end
@@ -119,8 +198,13 @@ module Json
119
198
  @dest_analysis,
120
199
  preference: @preference,
121
200
  add_template_only_nodes: @add_template_only_nodes,
201
+ remove_template_missing_nodes: @remove_template_missing_nodes,
202
+ resolution_mode: @resolution_mode,
203
+ corruption_handling: @corruption_handling,
122
204
  match_refiner: @match_refiner,
123
205
  node_typing: @node_typing,
206
+ merge_arrays: @merge_arrays,
207
+ preserve_atomic_formatting: @preserve_atomic_formatting
124
208
  )
125
209
  end
126
210
 
@@ -141,9 +225,51 @@ module Json
141
225
 
142
226
  private
143
227
 
144
- # JSON FileAnalysis only accepts signature_generator, not freeze_token
145
- def build_full_analysis_options
146
- {signature_generator: @signature_generator}
228
+ def start_runtime_session!
229
+ start_runtime_root_session!(
230
+ surface_kind: :json_document,
231
+ declared_language: :json,
232
+ effective_language: :json,
233
+ operation_id: 'json-document-root',
234
+ delegate_name: 'json-runtime',
235
+ policy_context: {
236
+ preference: @preference,
237
+ add_template_only_nodes: @add_template_only_nodes,
238
+ remove_template_missing_nodes: @remove_template_missing_nodes,
239
+ resolution_mode: @resolution_mode,
240
+ unresolved_policy: @unresolved_policy.to_h
241
+ },
242
+ metadata: { merger: self.class.name },
243
+ options: {
244
+ preference: @preference,
245
+ add_template_only_nodes: @add_template_only_nodes,
246
+ remove_template_missing_nodes: @remove_template_missing_nodes,
247
+ resolution_mode: @resolution_mode,
248
+ unresolved_policy: @unresolved_policy.to_h
249
+ },
250
+ language_chain: [:json],
251
+ delegate_metadata: { merger: self.class.name }
252
+ )
253
+ end
254
+
255
+ def complete_runtime_session!(root_operation, merge_result)
256
+ complete_runtime_root_session!(
257
+ root_operation: root_operation,
258
+ replacement_text: merge_result.to_json,
259
+ unresolved_cases: merge_result.unresolved_cases,
260
+ metadata: {
261
+ stats: merge_result.statistics,
262
+ decisions: merge_result.decision_summary
263
+ }
264
+ )
265
+ end
266
+
267
+ def fail_runtime_session!(root_operation, error)
268
+ fail_runtime_root_session!(
269
+ root_operation: root_operation,
270
+ error: error,
271
+ kind: :merge_failed
272
+ )
147
273
  end
148
274
  end
149
275
  end