ast-merge 1.0.0 → 2.0.0

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.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +194 -1
  4. data/README.md +235 -53
  5. data/exe/ast-merge-recipe +366 -0
  6. data/lib/ast/merge/ast_node.rb +224 -24
  7. data/lib/ast/merge/comment/block.rb +6 -0
  8. data/lib/ast/merge/comment/empty.rb +6 -0
  9. data/lib/ast/merge/comment/line.rb +6 -0
  10. data/lib/ast/merge/comment/parser.rb +9 -7
  11. data/lib/ast/merge/conflict_resolver_base.rb +8 -1
  12. data/lib/ast/merge/content_match_refiner.rb +278 -0
  13. data/lib/ast/merge/debug_logger.rb +6 -1
  14. data/lib/ast/merge/detector/base.rb +193 -0
  15. data/lib/ast/merge/detector/fenced_code_block.rb +227 -0
  16. data/lib/ast/merge/detector/mergeable.rb +369 -0
  17. data/lib/ast/merge/detector/toml_frontmatter.rb +82 -0
  18. data/lib/ast/merge/detector/yaml_frontmatter.rb +82 -0
  19. data/lib/ast/merge/file_analyzable.rb +5 -3
  20. data/lib/ast/merge/freeze_node_base.rb +1 -1
  21. data/lib/ast/merge/match_refiner_base.rb +1 -1
  22. data/lib/ast/merge/match_score_base.rb +1 -1
  23. data/lib/ast/merge/merge_result_base.rb +4 -1
  24. data/lib/ast/merge/merger_config.rb +33 -31
  25. data/lib/ast/merge/navigable_statement.rb +630 -0
  26. data/lib/ast/merge/partial_template_merger.rb +432 -0
  27. data/lib/ast/merge/recipe/config.rb +198 -0
  28. data/lib/ast/merge/recipe/preset.rb +171 -0
  29. data/lib/ast/merge/recipe/runner.rb +254 -0
  30. data/lib/ast/merge/recipe/script_loader.rb +181 -0
  31. data/lib/ast/merge/recipe.rb +26 -0
  32. data/lib/ast/merge/rspec/dependency_tags.rb +252 -0
  33. data/lib/ast/merge/rspec/shared_examples/reproducible_merge.rb +3 -2
  34. data/lib/ast/merge/rspec.rb +33 -2
  35. data/lib/ast/merge/section_typing.rb +52 -50
  36. data/lib/ast/merge/smart_merger_base.rb +86 -3
  37. data/lib/ast/merge/text/line_node.rb +42 -9
  38. data/lib/ast/merge/text/section_splitter.rb +12 -10
  39. data/lib/ast/merge/text/word_node.rb +47 -14
  40. data/lib/ast/merge/version.rb +1 -1
  41. data/lib/ast/merge.rb +10 -6
  42. data/sig/ast/merge.rbs +389 -2
  43. data.tar.gz.sig +0 -0
  44. metadata +76 -12
  45. metadata.gz.sig +0 -0
  46. data/lib/ast/merge/fenced_code_block_detector.rb +0 -211
  47. data/lib/ast/merge/region.rb +0 -124
  48. data/lib/ast/merge/region_detector_base.rb +0 -114
  49. data/lib/ast/merge/region_mergeable.rb +0 -364
  50. data/lib/ast/merge/toml_frontmatter_detector.rb +0 -88
  51. data/lib/ast/merge/yaml_frontmatter_detector.rb +0 -108
@@ -1,23 +1,27 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../ast_node"
4
+
3
5
  module Ast
4
6
  module Merge
5
7
  module Text
6
8
  # Represents a line of text in the text-based AST.
7
9
  # Lines are top-level nodes, with words as nested children.
8
10
  #
11
+ # Inherits from AstNode (SyntheticNode) to implement the TreeHaver::Node
12
+ # protocol, making it compatible with all tree_haver-based merge operations.
13
+ #
9
14
  # @example
10
15
  # line = LineNode.new("Hello world!", line_number: 1)
11
16
  # line.content # => "Hello world!"
12
17
  # line.words.size # => 2
13
18
  # line.signature # => [:line, "Hello world!"]
14
- class LineNode
19
+ # line.type # => "line_node" (TreeHaver protocol)
20
+ # line.text # => "Hello world!" (TreeHaver protocol)
21
+ class LineNode < AstNode
15
22
  # @return [String] The full line content (without trailing newline)
16
23
  attr_reader :content
17
24
 
18
- # @return [Integer] 1-based line number
19
- attr_reader :line_number
20
-
21
25
  # @return [Array<WordNode>] Words contained in this line
22
26
  attr_reader :words
23
27
 
@@ -27,10 +31,33 @@ module Ast
27
31
  # @param line_number [Integer] 1-based line number
28
32
  def initialize(content, line_number:)
29
33
  @content = content
30
- @line_number = line_number
34
+
35
+ location = AstNode::Location.new(
36
+ start_line: line_number,
37
+ end_line: line_number,
38
+ start_column: 0,
39
+ end_column: content.length,
40
+ )
41
+
42
+ super(slice: content, location: location)
43
+
44
+ # Parse words AFTER super sets up location
31
45
  @words = parse_words
32
46
  end
33
47
 
48
+ # TreeHaver::Node protocol: type
49
+ # @return [String] "line_node"
50
+ def type
51
+ "line_node"
52
+ end
53
+
54
+ # TreeHaver::Node protocol: children
55
+ # Returns word nodes as children
56
+ # @return [Array<WordNode>]
57
+ def children
58
+ @words
59
+ end
60
+
34
61
  # Generate a signature for this line node.
35
62
  # The signature is used for matching lines across template/destination.
36
63
  #
@@ -61,18 +88,24 @@ module Ast
61
88
  @content.strip.start_with?("#")
62
89
  end
63
90
 
91
+ # Get the 1-based line number
92
+ # @return [Integer] 1-based line number
93
+ def line_number
94
+ location.start_line
95
+ end
96
+
64
97
  # Get the starting line (for compatibility with AST node interface)
65
98
  #
66
99
  # @return [Integer] 1-based start line
67
100
  def start_line
68
- @line_number
101
+ location.start_line
69
102
  end
70
103
 
71
104
  # Get the ending line (for compatibility with AST node interface)
72
105
  #
73
106
  # @return [Integer] 1-based end line (same as start for single line)
74
107
  def end_line
75
- @line_number
108
+ location.end_line
76
109
  end
77
110
 
78
111
  # Check equality with another LineNode
@@ -96,7 +129,7 @@ module Ast
96
129
  #
97
130
  # @return [String] Debug representation
98
131
  def inspect
99
- "#<LineNode line=#{@line_number} #{@content.inspect} words=#{@words.size}>"
132
+ "#<LineNode line=#{line_number} #{@content.inspect} words=#{@words.size}>"
100
133
  end
101
134
 
102
135
  # Convert to string (returns content)
@@ -126,7 +159,7 @@ module Ast
126
159
 
127
160
  words << WordNode.new(
128
161
  word,
129
- line_number: @line_number,
162
+ line_number: line_number,
130
163
  word_index: word_index,
131
164
  start_col: start_col,
132
165
  end_col: end_col,
@@ -265,16 +265,18 @@ module Ast
265
265
  normalize_name(section.name)
266
266
  end
267
267
 
268
- # Validate splitter configuration.
269
- #
270
- # @param config [Hash, nil] Configuration to validate
271
- # @raise [ArgumentError] If configuration is invalid
272
- # @return [void]
273
- def self.validate!(config)
274
- return if config.nil?
275
-
276
- unless config.is_a?(Hash)
277
- raise ArgumentError, "splitter config must be a Hash, got #{config.class}"
268
+ class << self
269
+ # Validate a splitter configuration.
270
+ #
271
+ # @param config [Hash, nil] Configuration to validate
272
+ # @raise [ArgumentError] If configuration is invalid
273
+ # @return [void]
274
+ def validate!(config)
275
+ return if config.nil?
276
+
277
+ unless config.is_a?(Hash)
278
+ raise ArgumentError, "splitter config must be a Hash, got #{config.class}"
279
+ end
278
280
  end
279
281
  end
280
282
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../ast_node"
4
+
3
5
  module Ast
4
6
  module Merge
5
7
  module Text
@@ -7,26 +9,21 @@ module Ast
7
9
  # Words are the nested level of the text-based AST.
8
10
  # They are identified by word boundaries (regex \b).
9
11
  #
12
+ # Inherits from AstNode (SyntheticNode) to implement the TreeHaver::Node
13
+ # protocol, making it compatible with all tree_haver-based merge operations.
14
+ #
10
15
  # @example
11
16
  # word = WordNode.new("hello", line_number: 1, word_index: 0, start_col: 0, end_col: 5)
12
17
  # word.content # => "hello"
13
18
  # word.signature # => [:word, "hello"]
14
- class WordNode
19
+ # word.type # => "word_node" (TreeHaver protocol)
20
+ class WordNode < AstNode
15
21
  # @return [String] The word content
16
22
  attr_reader :content
17
23
 
18
- # @return [Integer] 1-based line number containing this word
19
- attr_reader :line_number
20
-
21
24
  # @return [Integer] 0-based index of this word within the line
22
25
  attr_reader :word_index
23
26
 
24
- # @return [Integer] 0-based starting column position
25
- attr_reader :start_col
26
-
27
- # @return [Integer] 0-based ending column position (exclusive)
28
- attr_reader :end_col
29
-
30
27
  # Initialize a new WordNode
31
28
  #
32
29
  # @param content [String] The word content
@@ -36,10 +33,22 @@ module Ast
36
33
  # @param end_col [Integer] 0-based end column (exclusive)
37
34
  def initialize(content, line_number:, word_index:, start_col:, end_col:)
38
35
  @content = content
39
- @line_number = line_number
40
36
  @word_index = word_index
41
- @start_col = start_col
42
- @end_col = end_col
37
+
38
+ location = AstNode::Location.new(
39
+ start_line: line_number,
40
+ end_line: line_number,
41
+ start_column: start_col,
42
+ end_column: end_col,
43
+ )
44
+
45
+ super(slice: content, location: location)
46
+ end
47
+
48
+ # TreeHaver::Node protocol: type
49
+ # @return [String] "word_node"
50
+ def type
51
+ "word_node"
43
52
  end
44
53
 
45
54
  # Generate a signature for this word node.
@@ -50,6 +59,30 @@ module Ast
50
59
  [:word, @content]
51
60
  end
52
61
 
62
+ # Get normalized content (the word itself for words)
63
+ # @return [String]
64
+ def normalized_content
65
+ @content
66
+ end
67
+
68
+ # Get the 1-based line number
69
+ # @return [Integer]
70
+ def line_number
71
+ location.start_line
72
+ end
73
+
74
+ # Get start column (0-based)
75
+ # @return [Integer]
76
+ def start_col
77
+ location.start_column
78
+ end
79
+
80
+ # Get end column (0-based, exclusive)
81
+ # @return [Integer]
82
+ def end_col
83
+ location.end_column
84
+ end
85
+
53
86
  # Check equality with another WordNode
54
87
  #
55
88
  # @param other [WordNode] Other node to compare
@@ -71,7 +104,7 @@ module Ast
71
104
  #
72
105
  # @return [String] Debug representation
73
106
  def inspect
74
- "#<WordNode #{@content.inspect} line=#{@line_number} col=#{@start_col}..#{@end_col}>"
107
+ "#<WordNode #{@content.inspect} line=#{line_number} col=#{start_col}..#{end_col}>"
75
108
  end
76
109
 
77
110
  # Convert to string (returns content)
@@ -5,7 +5,7 @@ module Ast
5
5
  # Version information for Ast::Merge
6
6
  module Version
7
7
  # Current version of the ast-merge gem
8
- VERSION = "1.0.0"
8
+ VERSION = "2.0.0"
9
9
  end
10
10
  VERSION = Version::VERSION # traditional location
11
11
  end
data/lib/ast/merge.rb CHANGED
@@ -136,27 +136,31 @@ module Ast
136
136
  end
137
137
  end
138
138
 
139
+ # Core classes
139
140
  autoload :AstNode, "ast/merge/ast_node"
140
141
  autoload :Comment, "ast/merge/comment"
141
142
  autoload :ConflictResolverBase, "ast/merge/conflict_resolver_base"
143
+ autoload :ContentMatchRefiner, "ast/merge/content_match_refiner"
142
144
  autoload :DebugLogger, "ast/merge/debug_logger"
143
- autoload :FencedCodeBlockDetector, "ast/merge/fenced_code_block_detector"
144
145
  autoload :FileAnalyzable, "ast/merge/file_analyzable"
145
146
  autoload :Freezable, "ast/merge/freezable"
146
147
  autoload :FreezeNodeBase, "ast/merge/freeze_node_base"
148
+ autoload :InjectionPoint, "ast/merge/navigable_statement"
149
+ autoload :InjectionPointFinder, "ast/merge/navigable_statement"
147
150
  autoload :MatchRefinerBase, "ast/merge/match_refiner_base"
148
151
  autoload :MatchScoreBase, "ast/merge/match_score_base"
149
152
  autoload :MergeResultBase, "ast/merge/merge_result_base"
150
153
  autoload :MergerConfig, "ast/merge/merger_config"
154
+ autoload :NavigableStatement, "ast/merge/navigable_statement"
151
155
  autoload :NodeTyping, "ast/merge/node_typing"
152
- autoload :Region, "ast/merge/region"
153
- autoload :RegionDetectorBase, "ast/merge/region_detector_base"
154
- autoload :RegionMergeable, "ast/merge/region_mergeable"
156
+ autoload :PartialTemplateMerger, "ast/merge/partial_template_merger"
155
157
  autoload :SectionTyping, "ast/merge/section_typing"
156
158
  autoload :SmartMergerBase, "ast/merge/smart_merger_base"
157
159
  autoload :Text, "ast/merge/text"
158
- autoload :TomlFrontmatterDetector, "ast/merge/toml_frontmatter_detector"
159
- autoload :YamlFrontmatterDetector, "ast/merge/yaml_frontmatter_detector"
160
+
161
+ # Namespaces
162
+ autoload :Detector, "ast/merge/detector/base" # Detector::Region, Detector::Base, Detector::Mergeable, etc.
163
+ autoload :Recipe, "ast/merge/recipe"
160
164
  end
161
165
  end
162
166
 
data/sig/ast/merge.rbs CHANGED
@@ -167,13 +167,13 @@ module Ast
167
167
 
168
168
  attr_reader signature_match_preference: Symbol | Hash[Symbol, Symbol]
169
169
  attr_reader node_splitter: Hash[Symbol, untyped]?
170
- attr_reader add_template_only_nodes: bool
170
+ attr_reader add_template_only_nodes: add_template_only_nodes_type
171
171
  attr_reader freeze_token: String?
172
172
  attr_reader signature_generator: (^(untyped) -> (Array[untyped] | untyped | nil))?
173
173
 
174
174
  def initialize: (
175
175
  ?signature_match_preference: (Symbol | Hash[Symbol, Symbol]),
176
- ?add_template_only_nodes: bool,
176
+ ?add_template_only_nodes: add_template_only_nodes_type,
177
177
  ?freeze_token: String?,
178
178
  ?signature_generator: (^(untyped) -> (Array[untyped] | untyped | nil))?,
179
179
  ?node_splitter: Hash[Symbol, untyped]?
@@ -191,5 +191,392 @@ module Ast
191
191
 
192
192
  def validate_preference!: (Symbol preference) -> void
193
193
  end
194
+
195
+ # Type alias for node typing callables
196
+ type node_typing_callable = ^(untyped) -> untyped?
197
+ type node_typing_hash = Hash[Symbol | String, node_typing_callable]
198
+ type preference_type = Symbol | Hash[Symbol, Symbol]
199
+
200
+ # Type alias for add_template_only_nodes filter
201
+ # Can be: Boolean, or callable that receives (node, entry) and returns truthy/falsey
202
+ # Entry hash contains: { template_node:, signature:, template_index:, dest_index: nil }
203
+ type add_template_only_filter = ^(untyped node, Hash[Symbol, untyped] entry) -> boolish
204
+ type add_template_only_nodes_type = bool | add_template_only_filter
205
+
206
+ # Abstract base class for SmartMerger implementations
207
+ class SmartMergerBase
208
+ include Detector::Mergeable
209
+
210
+ attr_reader template_content: String
211
+ attr_reader dest_content: String
212
+ attr_reader template_analysis: untyped
213
+ attr_reader dest_analysis: untyped
214
+ attr_reader resolver: untyped
215
+ attr_reader result: untyped
216
+ attr_reader preference: preference_type
217
+ attr_reader add_template_only_nodes: add_template_only_nodes_type
218
+ attr_reader freeze_token: String
219
+ attr_reader signature_generator: (^(untyped) -> (Array[untyped] | untyped | nil))?
220
+ attr_reader match_refiner: untyped?
221
+ attr_reader node_typing: node_typing_hash?
222
+
223
+ def initialize: (
224
+ String template_content,
225
+ String dest_content,
226
+ ?signature_generator: (^(untyped) -> (Array[untyped] | untyped | nil))?,
227
+ ?preference: preference_type,
228
+ ?add_template_only_nodes: add_template_only_nodes_type,
229
+ ?freeze_token: String?,
230
+ ?match_refiner: untyped?,
231
+ ?regions: Array[Hash[Symbol, untyped]]?,
232
+ ?region_placeholder: String?,
233
+ ?node_typing: node_typing_hash?,
234
+ **untyped format_options
235
+ ) -> void
236
+
237
+ def merge: () -> String
238
+ def merge_result: () -> untyped
239
+
240
+ # Class method for convenient merging
241
+ def self.merge: (
242
+ String template_content,
243
+ String dest_content,
244
+ **untyped options
245
+ ) -> String
246
+
247
+ private
248
+
249
+ # Abstract methods that subclasses must implement
250
+ def analysis_class: () -> Class
251
+ def perform_merge: () -> untyped
252
+
253
+ # Optional hooks for subclasses
254
+ def default_freeze_token: () -> String
255
+ def resolver_class: () -> Class?
256
+ def result_class: () -> Class?
257
+ def aligner_class: () -> Class?
258
+ def build_analysis_options: () -> Hash[Symbol, untyped]
259
+ def build_resolver_options: () -> Hash[Symbol, untyped]
260
+ def build_full_analysis_options: (Symbol source) -> Hash[Symbol, untyped]
261
+ def update_result_content: (untyped result, String content) -> void
262
+ def template_parse_error_class: () -> Class
263
+ def destination_parse_error_class: () -> Class
264
+ end
265
+
266
+ # Abstract base class for ConflictResolver implementations
267
+ class ConflictResolverBase
268
+ # Decision constants
269
+ DECISION_KEPT_TEMPLATE: Symbol
270
+ DECISION_KEPT_DEST: Symbol
271
+ DECISION_MERGED: Symbol
272
+ DECISION_ADDED: Symbol
273
+ DECISION_FREEZE_BLOCK: Symbol
274
+ DECISION_APPENDED: Symbol
275
+ DECISION_REPLACED: Symbol
276
+
277
+ attr_reader strategy: Symbol
278
+ attr_reader preference: preference_type
279
+ attr_reader template_analysis: untyped
280
+ attr_reader dest_analysis: untyped
281
+ attr_reader add_template_only_nodes: bool
282
+ attr_reader match_refiner: untyped?
283
+
284
+ def initialize: (
285
+ strategy: Symbol,
286
+ preference: preference_type,
287
+ template_analysis: untyped,
288
+ dest_analysis: untyped,
289
+ ?add_template_only_nodes: bool,
290
+ ?match_refiner: untyped?,
291
+ **untyped options
292
+ ) -> void
293
+
294
+ def resolve: (*untyped args, **untyped kwargs) -> untyped
295
+
296
+ # Get preference for a specific node (supports Hash preference)
297
+ def preference_for: (untyped node) -> Symbol
298
+
299
+ private
300
+
301
+ def validate_preference!: (preference_type preference) -> void
302
+ def resolve_node_pair: (untyped template_node, untyped dest_node, **untyped kwargs) -> untyped
303
+ def resolve_batch: (*untyped args) -> untyped
304
+ def resolve_boundary: (*untyped args) -> untyped
305
+ end
306
+
307
+ # Abstract base class for MergeResult implementations
308
+ class MergeResultBase
309
+ # Decision constants
310
+ DECISION_KEPT_TEMPLATE: Symbol
311
+ DECISION_KEPT_DEST: Symbol
312
+ DECISION_MERGED: Symbol
313
+ DECISION_ADDED: Symbol
314
+ DECISION_FREEZE_BLOCK: Symbol
315
+ DECISION_APPENDED: Symbol
316
+ DECISION_REPLACED: Symbol
317
+
318
+ attr_reader template_analysis: untyped?
319
+ attr_reader dest_analysis: untyped?
320
+ attr_reader lines: Array[String]
321
+ attr_reader decisions: Array[Hash[Symbol, untyped]]
322
+ attr_reader conflicts: Array[Hash[Symbol, untyped]]
323
+ attr_reader frozen_blocks: Array[untyped]
324
+ attr_reader stats: Hash[Symbol, untyped]
325
+
326
+ def initialize: (
327
+ ?template_analysis: untyped?,
328
+ ?dest_analysis: untyped?,
329
+ ?conflicts: Array[Hash[Symbol, untyped]],
330
+ ?frozen_blocks: Array[untyped],
331
+ ?stats: Hash[Symbol, untyped],
332
+ **untyped options
333
+ ) -> void
334
+
335
+ def content: () -> Array[String]
336
+ def content?: () -> bool
337
+ def content_string: () -> String
338
+ def to_s: () -> String
339
+ def success?: () -> bool
340
+ def conflicts?: () -> bool
341
+ def track_decision: (Symbol decision, Symbol source, **untyped metadata) -> void
342
+ end
343
+
344
+ # Abstract base class for MatchRefiner implementations
345
+ class MatchRefinerBase
346
+ # Default similarity threshold
347
+ DEFAULT_THRESHOLD: Float
348
+
349
+ attr_reader threshold: Float
350
+ attr_reader node_types: Array[Symbol]?
351
+
352
+ def initialize: (
353
+ ?threshold: Float,
354
+ ?node_types: Array[Symbol]?,
355
+ **untyped options
356
+ ) -> void
357
+
358
+ # Find matches between unmatched nodes
359
+ def call: (
360
+ Array[untyped] template_nodes,
361
+ Array[untyped] dest_nodes,
362
+ ?Hash[Symbol, untyped] context
363
+ ) -> Array[MatchResult]
364
+
365
+ # Compute similarity score between two nodes
366
+ def similarity: (untyped template_node, untyped dest_node) -> Float
367
+
368
+ # Check if a node matches the configured types
369
+ def matches_type?: (untyped node) -> bool
370
+
371
+ private
372
+
373
+ # Levenshtein distance for string similarity
374
+ def levenshtein_distance: (String s1, String s2) -> Integer
375
+ def string_similarity: (String s1, String s2) -> Float
376
+ end
377
+
378
+ # Result of a match refinement operation
379
+ class MatchResult
380
+ attr_reader template_node: untyped
381
+ attr_reader dest_node: untyped
382
+ attr_reader score: Float
383
+ attr_reader metadata: Hash[Symbol, untyped]
384
+
385
+ def initialize: (
386
+ template_node: untyped,
387
+ dest_node: untyped,
388
+ score: Float,
389
+ ?metadata: Hash[Symbol, untyped]
390
+ ) -> void
391
+
392
+ def to_h: () -> Hash[Symbol, untyped]
393
+ end
394
+
395
+ # Detector namespace for region detection and merging
396
+ module Detector
397
+ # Represents a detected region within a document
398
+ class Region < Struct[untyped]
399
+ attr_accessor type: Symbol
400
+ attr_accessor content: String
401
+ attr_accessor start_line: Integer
402
+ attr_accessor end_line: Integer
403
+ attr_accessor delimiters: Array[String]?
404
+ attr_accessor metadata: Hash[Symbol, untyped]?
405
+
406
+ def line_range: () -> Range[Integer]
407
+ def line_count: () -> Integer
408
+ def full_text: () -> String
409
+ def contains_line?: (Integer line) -> bool
410
+ def overlaps?: (Region other) -> bool
411
+ def to_s: () -> String
412
+ def inspect: () -> String
413
+ end
414
+
415
+ # Abstract base class for region detectors
416
+ class Base
417
+ def region_type: () -> Symbol
418
+ def detect_all: (String source) -> Array[Region]
419
+ def strip_delimiters?: () -> bool
420
+ def name: () -> String
421
+ def inspect: () -> String
422
+
423
+ private
424
+
425
+ def build_region: (
426
+ type: Symbol,
427
+ content: String,
428
+ start_line: Integer,
429
+ end_line: Integer,
430
+ ?delimiters: Array[String]?,
431
+ ?metadata: Hash[Symbol, untyped]?
432
+ ) -> Region
433
+ end
434
+
435
+ # Detects fenced code blocks
436
+ class FencedCodeBlock < Base
437
+ attr_reader language: String
438
+ attr_reader aliases: Array[String]
439
+
440
+ def initialize: (String language, ?aliases: Array[String]) -> void
441
+ def self.ruby: () -> FencedCodeBlock
442
+ def self.yaml: () -> FencedCodeBlock
443
+ def self.json: () -> FencedCodeBlock
444
+ def self.bash: () -> FencedCodeBlock
445
+ end
446
+
447
+ # Detects YAML frontmatter
448
+ class YamlFrontmatter < Base
449
+ FRONTMATTER_PATTERN: Regexp
450
+ end
451
+
452
+ # Detects TOML frontmatter
453
+ class TomlFrontmatter < Base
454
+ FRONTMATTER_PATTERN: Regexp
455
+ end
456
+
457
+ # Mixin for region-aware merging
458
+ module Mergeable
459
+ DEFAULT_PLACEHOLDER_PREFIX: String
460
+ DEFAULT_PLACEHOLDER_SUFFIX: String
461
+
462
+ # Configuration for a region type
463
+ class Config < Struct[untyped]
464
+ attr_accessor detector: Base
465
+ attr_accessor merger_class: Class?
466
+ attr_accessor merger_options: Hash[Symbol, untyped]
467
+ attr_accessor regions: Array[Hash[Symbol, untyped]]
468
+ end
469
+
470
+ # Extracted region with placeholder
471
+ class ExtractedRegion < Struct[untyped]
472
+ attr_accessor region: Region
473
+ attr_accessor config: Config
474
+ attr_accessor placeholder: String
475
+ attr_accessor merged_content: String?
476
+ end
477
+
478
+ def regions_configured?: () -> bool
479
+ def setup_regions: (regions: Array[Hash[Symbol, untyped]], ?region_placeholder: String?) -> void
480
+ def extract_template_regions: (String content) -> String
481
+ def extract_dest_regions: (String content) -> String
482
+ def substitute_merged_regions: (String content) -> String
483
+ end
484
+ end
485
+
486
+ # Recipe namespace for YAML-based merge recipes
487
+ module Recipe
488
+ # Recipe configuration loaded from YAML
489
+ class Config
490
+ attr_reader name: String
491
+ attr_reader description: String?
492
+ attr_reader template_path: String
493
+ attr_reader targets: Array[String]
494
+ attr_reader injection: Hash[String, untyped]
495
+ attr_reader merge_options: Hash[String, untyped]
496
+ attr_reader when_missing: Symbol
497
+ attr_reader recipe_path: String?
498
+
499
+ def self.load: (String path) -> Config
500
+ def initialize: (Hash[String, untyped] config, ?recipe_path: String?) -> void
501
+ def anchor_config: () -> Hash[String, untyped]
502
+ def boundary_config: () -> Hash[String, untyped]?
503
+ def position: () -> Symbol
504
+ def preference: () -> Symbol
505
+ def add_missing?: () -> bool
506
+ def replace_mode?: () -> bool
507
+ end
508
+
509
+ # Executes recipes against target files
510
+ class Runner
511
+ # Result of processing a single file
512
+ class Result < Struct[untyped]
513
+ attr_accessor path: String
514
+ attr_accessor relative_path: String
515
+ attr_accessor status: Symbol
516
+ attr_accessor changed: bool
517
+ attr_accessor has_anchor: bool
518
+ attr_accessor message: String?
519
+ attr_accessor stats: Hash[Symbol, untyped]?
520
+ attr_accessor error: String?
521
+ end
522
+
523
+ attr_reader recipe: Config
524
+ attr_reader dry_run: bool
525
+ attr_reader verbose: bool
526
+ attr_reader parser: Symbol
527
+ attr_reader base_dir: String
528
+ attr_reader results: Array[Result]
529
+
530
+ def initialize: (
531
+ Config recipe,
532
+ ?dry_run: bool,
533
+ ?verbose: bool,
534
+ ?parser: Symbol,
535
+ ?base_dir: String
536
+ ) -> void
537
+
538
+ def run: () ?{ (Result) -> void } -> Array[Result]
539
+ def summary: () -> Hash[Symbol, Integer]
540
+ def summary_table: () -> Array[Hash[Symbol, untyped]]
541
+ end
542
+
543
+ # Loads Ruby scripts referenced by recipes
544
+ class ScriptLoader
545
+ attr_reader base_dir: String?
546
+ attr_reader cache: Hash[String, untyped]
547
+
548
+ def initialize: (?recipe_path: String?, ?base_dir: String?) -> void
549
+ def load_callable: (String script_ref) -> (^(*untyped) -> untyped)?
550
+ def resolve_path: (String script_ref) -> String?
551
+
552
+ private
553
+
554
+ def load_from_file: (String path) -> untyped
555
+ def parse_inline_lambda: (String code) -> (^(*untyped) -> untyped)?
556
+ end
557
+ end
558
+
559
+ # Module for node typing support
560
+ module NodeTyping
561
+ # Wrapper class for typed nodes
562
+ class Wrapper
563
+ attr_reader node: untyped
564
+ attr_reader merge_type: Symbol
565
+
566
+ def initialize: (untyped node, Symbol merge_type) -> void
567
+ def method_missing: (Symbol method, *untyped args) ?{ (*untyped) -> untyped } -> untyped
568
+ def respond_to_missing?: (Symbol method, ?bool include_private) -> bool
569
+ def typed_node?: () -> bool
570
+ def unwrap: () -> untyped
571
+ def ==: (untyped other) -> bool
572
+ def hash: () -> Integer
573
+ def eql?: (untyped other) -> bool
574
+ def inspect: () -> String
575
+ end
576
+
577
+ def self.with_merge_type: (untyped node, Symbol merge_type) -> Wrapper
578
+ def self.validate!: (node_typing_hash? node_typing) -> void
579
+ def self.apply: (untyped node, node_typing_hash? node_typing) -> untyped
580
+ end
194
581
  end
195
582
  end