ast-merge 1.1.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +198 -7
  4. data/README.md +208 -39
  5. data/exe/ast-merge-recipe +366 -0
  6. data/lib/ast/merge/conflict_resolver_base.rb +8 -1
  7. data/lib/ast/merge/content_match_refiner.rb +278 -0
  8. data/lib/ast/merge/debug_logger.rb +2 -1
  9. data/lib/ast/merge/detector/base.rb +193 -0
  10. data/lib/ast/merge/detector/fenced_code_block.rb +227 -0
  11. data/lib/ast/merge/detector/mergeable.rb +369 -0
  12. data/lib/ast/merge/detector/toml_frontmatter.rb +82 -0
  13. data/lib/ast/merge/detector/yaml_frontmatter.rb +82 -0
  14. data/lib/ast/merge/merge_result_base.rb +4 -1
  15. data/lib/ast/merge/navigable_statement.rb +630 -0
  16. data/lib/ast/merge/partial_template_merger.rb +432 -0
  17. data/lib/ast/merge/recipe/config.rb +198 -0
  18. data/lib/ast/merge/recipe/preset.rb +171 -0
  19. data/lib/ast/merge/recipe/runner.rb +254 -0
  20. data/lib/ast/merge/recipe/script_loader.rb +181 -0
  21. data/lib/ast/merge/recipe.rb +26 -0
  22. data/lib/ast/merge/rspec/dependency_tags.rb +252 -0
  23. data/lib/ast/merge/rspec/shared_examples/reproducible_merge.rb +3 -2
  24. data/lib/ast/merge/rspec.rb +33 -2
  25. data/lib/ast/merge/smart_merger_base.rb +86 -3
  26. data/lib/ast/merge/version.rb +1 -1
  27. data/lib/ast/merge.rb +10 -6
  28. data/sig/ast/merge.rbs +389 -2
  29. data.tar.gz.sig +0 -0
  30. metadata +60 -16
  31. metadata.gz.sig +0 -0
  32. data/lib/ast/merge/fenced_code_block_detector.rb +0 -313
  33. data/lib/ast/merge/region.rb +0 -124
  34. data/lib/ast/merge/region_detector_base.rb +0 -114
  35. data/lib/ast/merge/region_mergeable.rb +0 -364
  36. data/lib/ast/merge/toml_frontmatter_detector.rb +0 -88
  37. data/lib/ast/merge/yaml_frontmatter_detector.rb +0 -88
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 86acfad0e867d5098d34fa4ec30fca07677e8b094a4bbeee27afa4dd236f6ba8
4
- data.tar.gz: 61b3c5e9b24b4bc396dfa6562ebdf85d7a27153b67f96d53e977d04cc668c653
3
+ metadata.gz: 77a90e163c91b44ac5b425a36dc12643f44dcbb67512f1687aa356a25b88aad8
4
+ data.tar.gz: 13791029a6b5c52e02d679720342aa291221d6b57c2b952a9affe7096630b639
5
5
  SHA512:
6
- metadata.gz: d610aa4ba48cd7b4476c041412dcb661761464a49966af6045af8f3c0e0a6f87cb75b89d1f706caa58505fab9cdbee9aa3681f9b615a79f6db3fb31f2f25aabb
7
- data.tar.gz: 5dfbdb16cebda587a4c2a460b71a3a6a794ab6dcee048d293f1b39aa679291368e221348439974782ec2cfc5cfca3b9d4f62cb12b566931e34913dfc3a8790dc
6
+ metadata.gz: 4cd7681688742d99e088033bc186b576834ea36f3b6a75184761cc0ec762dabe6fdd880829fa97df13dfea7867acd879d1cde372a3b03f7206a84d94770dec32
7
+ data.tar.gz: 0523adc4e7a51ff72efe423bfd01d42f9829c1371c1c92e65bc3eea492f52e1096992ed56766c461aa88b037db328758a4b09c7ee0d74c548457449b49ba2b9b
checksums.yaml.gz.sig CHANGED
Binary file
data/CHANGELOG.md CHANGED
@@ -20,6 +20,197 @@ Please file a bug if you notice a violation of semantic versioning.
20
20
 
21
21
  ### Added
22
22
 
23
+ ### Changed
24
+
25
+ ### Deprecated
26
+
27
+ ### Removed
28
+
29
+ ### Fixed
30
+
31
+ ### Security
32
+
33
+ ## [2.0.1] - 2025-12-29
34
+
35
+ - TAG: [v2.0.1][2.0.1t]
36
+ - COVERAGE: 88.47% -- 2894/3271 lines in 53 files
37
+ - BRANCH COVERAGE: 67.83% -- 698/1029 branches in 53 files
38
+ - 98.82% documented
39
+
40
+ ### Changed
41
+
42
+ - Upgraded dependencies
43
+ - yard-fence v0.8.1
44
+ - tree_haver v3.1.2
45
+ - kettle-test v1.0.7
46
+
47
+ ### Fixed
48
+
49
+ - Documentation cleanup (via yard-fence)
50
+
51
+ ## [2.0.0] - 2025-12-28
52
+
53
+ - TAG: [v2.0.0][2.0.0t]
54
+ - COVERAGE: 88.47% -- 2894/3271 lines in 53 files
55
+ - BRANCH COVERAGE: 67.83% -- 698/1029 branches in 53 files
56
+ - 98.82% documented
57
+
58
+ ### Added
59
+
60
+ - **RSpec Dependency Tags**: Conditional test execution based on available merge gems
61
+ - New `lib/ast/merge/rspec/dependency_tags.rb` provides automatic test filtering
62
+ - Tags for all merge gems: `:markly_merge`, `:prism_merge`, `:json_merge`, `:toml_merge`, etc.
63
+ - Composite tag `:any_markdown_merge` for tests that work with any markdown merger
64
+ - Negated tags (e.g., `:not_prism_merge`) for testing fallback behavior
65
+ - `AST_MERGE_DEBUG=1` environment variable prints dependency summary
66
+ - Eliminates need for `require` statements inside spec files
67
+ - See [lib/ast/merge/rspec/README.md](lib/ast/merge/rspec/README.md) for full documentation
68
+
69
+ - **Recipe::Preset**: Base class for merge configuration presets
70
+ - Provides merge configuration (signature generators, node typing, preferences) without requiring template files
71
+ - `Recipe::Config` now inherits from `Preset`, adding template/target handling
72
+ - `to_h` method converts preset to SmartMerger-compatible options hash
73
+ - Enables kettle-jem and other libraries to define reusable merge presets
74
+ - Supports script loading for signature generators and node typing via `ScriptLoader`
75
+
76
+ - **exe/ast-merge-recipe**: Shipped executable for running merge recipes
77
+ - Uses `bundler/inline` for dependency management
78
+ - Supports `--dry-run`, `--verbose`, `--parser`, `--base-dir` options
79
+ - Uses `optparse` for proper option parsing
80
+ - Loads merge gems from recipe YAML `merge_gems` section
81
+ - Development mode via `KETTLE_RB_DEV=true` or `--dev-root` option
82
+ - All gems sourced from gem.coop
83
+
84
+ - **PartialTemplateMerger**: Section-based merging for partial templates
85
+ - Find and merge specific sections in destination documents
86
+ - Support for both `replace_mode` (full replacement) and merge mode (intelligent merging)
87
+ - Configurable anchor and boundary matchers for section detection
88
+ - Custom `signature_generator` and `node_typing` support for advanced matching
89
+ - `when_missing` behavior: `:skip`, `:append`, `:prepend`
90
+
91
+ - **Recipe**: YAML-based recipe system for defining merge operations
92
+ - Load recipes from YAML files with `Recipe.load(path)`
93
+ - Define template, targets, injection point, merge preferences
94
+ - Support for `when_missing: skip|add|error` behavior
95
+ - Support for `replace_mode` option
96
+ - Automatic path resolution relative to recipe file location
97
+
98
+ - **RecipeRunner**: Execute recipes against multiple target files
99
+ - Uses PartialTemplateMerger for section-based merging
100
+ - Dry-run mode with `--dry-run` flag
101
+ - Results tracking with status, stats, and error reporting
102
+ - TableTennis integration for formatted output
103
+ - Support for different parsers (markly, commonmarker, prism, psych)
104
+
105
+ - **RecipeScriptLoader**: Load Ruby scripts referenced by recipes
106
+ - Convention: scripts in folder matching recipe basename (e.g., `my_recipe/` for `my_recipe.yml`)
107
+ - Support for inline lambda expressions in YAML
108
+ - Script caching for performance
109
+ - Validation that scripts return callable objects
110
+
111
+ - **bin/ast-merge-recipe**: CLI for running merge recipes
112
+ - `bin/ast-merge-recipe RECIPE_FILE [--dry-run] [--verbose]`
113
+ - Color-coded output with status symbols
114
+ - Summary table with counts
115
+
116
+ - **NavigableStatement**: New wrapper class for uniform node navigation (language-agnostic)
117
+ - Provides flat list navigation (`next`, `previous`, `index`) for all nodes
118
+ - Tree depth calculation with `tree_depth` method
119
+ - `same_or_shallower_than?` for level-based boundary detection
120
+ - Language-agnostic section boundaries using tree hierarchy
121
+ - Provides tree navigation (`tree_parent`, `tree_next`, `tree_previous`) for parser-backed nodes
122
+ - `synthetic?` method to detect nodes without tree navigation (GapLineNode, LinkDefinitionNode, etc.)
123
+ - `type?` and `text_matches?` helpers for node matching
124
+ - `node_attribute` for accessing parser-specific attributes with fallbacks
125
+ - `each_following` and `take_until` for sequential traversal
126
+ - `find_matching` and `find_first` class methods for querying statement lists
127
+ - `build_list` class method to create linked statement lists from raw statements
128
+
129
+ - **InjectionPoint**: New class for defining where content can be injected (language-agnostic)
130
+ - Supports positions: `:before`, `:after`, `:first_child`, `:last_child`, `:replace`
131
+ - Optional boundary for replacement ranges
132
+ - `replacement?`, `child_injection?`, `sibling_injection?` predicates
133
+ - `replaced_statements` to get all statements in a replacement range
134
+
135
+ - **InjectionPointFinder**: New class for finding injection points by matching criteria
136
+ - `find` to locate a single injection point by type/text pattern
137
+ - `find_all` to locate all matching injection points
138
+ - Works with any `*-merge` gem (prism-merge, markly-merge, psych-merge, etc.)
139
+
140
+ - **SmartMergerBase**: `add_template_only_nodes` now accepts a callable filter
141
+ - Boolean `true`/`false` still works as before (add all or none)
142
+ - Callable (Proc/Lambda) receives `(node, entry)` and returns truthy to add the node
143
+ - Enables selective addition of template-only nodes based on signature, type, or content
144
+ - Example use case: Add missing link reference definitions while skipping other template content
145
+ - Entry hash includes `:template_node`, `:signature`, `:template_index` for filtering decisions
146
+
147
+ - **ContentMatchRefiner**: New match refiner for fuzzy text content matching
148
+ - Uses Levenshtein distance to pair nodes with similar (but not identical) text
149
+ - Configurable scoring weights for content similarity, length, and position
150
+ - Custom content extractor support for parser-specific text extraction
151
+ - Node type filtering to limit which types are processed
152
+ - Can be combined with other refiners (e.g., TableMatchRefiner)
153
+ - Useful for matching paragraphs, headings, comments with minor edits
154
+
155
+ - **SmartMergerBase**: Added validity check after FileAnalysis creation
156
+ - Checks `valid?` after creating FileAnalysis and raises appropriate parse error if invalid
157
+ - Catches silent failures like grammar not available or parse errors
158
+ - Documented the FileAnalysis error handling pattern for all *-merge gems
159
+
160
+ - **SmartMergerBase**: Added explicit `node_typing` parameter
161
+ - All child SmartMergers were already using `node_typing` via `**format_options`
162
+ - Now explicitly documented and accessible via `attr_reader :node_typing`
163
+ - Validates node_typing configuration via `NodeTyping.validate!` if provided
164
+ - Enables per-node-type merge preferences across all `*-merge` gems
165
+
166
+ - **ConflictResolverBase**: Added `match_refiner` parameter and `**options` for forward compatibility
167
+ - All batch-strategy resolvers were storing `match_refiner` locally
168
+ - Now explicitly accepted in base class with `attr_reader :match_refiner`
169
+ - Added `**options` catch-all for future parameters without breaking child classes
170
+
171
+ - **MergeResultBase**: Added `**options` for forward compatibility
172
+ - Allows subclasses to accept new parameters without modification
173
+ - Maintains backward compatibility with existing no-arg and keyword-arg constructors
174
+
175
+ - **RBS Signatures**: Added comprehensive type signatures for base classes
176
+ - `SmartMergerBase` with all standard options and abstract method declarations
177
+ - `ConflictResolverBase` with strategy-based resolution methods
178
+ - `MergeResultBase` with unified constructor and decision tracking
179
+ - `MatchRefinerBase` with similarity computation interface
180
+ - `RegionMergeable` module for nested content merging
181
+ - `NodeTyping` module with `Wrapper` class for typed nodes
182
+ - Type aliases: `node_typing_callable`, `node_typing_hash`, `preference_type`
183
+
184
+ - **Documentation**: Updated README with comprehensive base class documentation
185
+ - Standard options table with all `SmartMergerBase` parameters
186
+ - Forward compatibility section explaining the `**options` pattern
187
+ - Complete "Creating a New Merge Gem" example with all base classes
188
+ - Base Classes Reference table
189
+
190
+ ### Changed
191
+
192
+ - **BREAKING - Namespace Reorganization**: Major restructuring for better organization
193
+ - `Ast::Merge::Region` → `Ast::Merge::Detector::Region` (Struct moved into Detector namespace)
194
+ - `Ast::Merge::RegionDetectorBase` → `Ast::Merge::Detector::Base`
195
+ - `Ast::Merge::RegionMergeable` → `Ast::Merge::Detector::Mergeable`
196
+ - `Ast::Merge::FencedCodeBlockDetector` → `Ast::Merge::Detector::FencedCodeBlock`
197
+ - `Ast::Merge::YamlFrontmatterDetector` → `Ast::Merge::Detector::YamlFrontmatter`
198
+ - `Ast::Merge::TomlFrontmatterDetector` → `Ast::Merge::Detector::TomlFrontmatter`
199
+ - `Ast::Merge::Recipe` class → `Ast::Merge::Recipe::Config`
200
+ - `Ast::Merge::RecipeRunner` → `Ast::Merge::Recipe::Runner`
201
+ - `Ast::Merge::RecipeScriptLoader` → `Ast::Merge::Recipe::ScriptLoader`
202
+ - `Ast::Merge::RegionMergeable::RegionConfig` → `Ast::Merge::Detector::Mergeable::Config`
203
+ - `Ast::Merge::RegionMergeable::ExtractedRegion` → `Ast::Merge::Detector::Mergeable::ExtractedRegion`
204
+
205
+ ## [1.1.0] - 2025-12-18
206
+
207
+ - TAG: [v1.1.0][1.1.0t]
208
+ - COVERAGE: 95.16% -- 2338/2457 lines in 44 files
209
+ - BRANCH COVERAGE: 82.59% -- 517/626 branches in 44 files
210
+ - 98.45% documented
211
+
212
+ ### Added
213
+
23
214
  - **tree_haver Integration**: Major architectural enhancement
24
215
  - Added `tree_haver` (~> 3.1) as a runtime dependency
25
216
  - `Ast::Merge::AstNode` now implements the TreeHaver::Node protocol for compatibility with tree_haver-based merge operations
@@ -52,10 +243,6 @@ Please file a bug if you notice a violation of semantic versioning.
52
243
  - markdown-merge description updated to "**Foundation**: Shared base for Markdown mergers with inner code block merging"
53
244
  - **Configuration Documentation**: Enhanced backend selection documentation
54
245
 
55
- ### Deprecated
56
-
57
- ### Removed
58
-
59
246
  ### Fixed
60
247
 
61
248
  - Fixed gemspec and Appraisals alignment with tree_haver requirements
@@ -63,8 +250,6 @@ Please file a bug if you notice a violation of semantic versioning.
63
250
  - Fixed badge rendering in documentation
64
251
  - Fixed README structure issues (removed H3 duplicates, standardized gem family tables)
65
252
 
66
- ### Security
67
-
68
253
  ## [1.0.0] - 2025-12-12
69
254
 
70
255
  - TAG: [v1.0.0][1.0.0t]
@@ -76,6 +261,12 @@ Please file a bug if you notice a violation of semantic versioning.
76
261
 
77
262
  - Initial release
78
263
 
79
- [Unreleased]: https://github.com/kettle-rb/ast-merge/compare/v1.0.0...HEAD
264
+ [Unreleased]: https://github.com/kettle-rb/ast-merge/compare/v2.0.1...HEAD
265
+ [2.0.1]: https://github.com/kettle-rb/ast-merge/compare/v2.0.0...v2.0.1
266
+ [2.0.1t]: https://github.com/kettle-rb/ast-merge/releases/tag/v2.0.1
267
+ [2.0.0]: https://github.com/kettle-rb/ast-merge/compare/v1.1.0...v2.0.0
268
+ [2.0.0t]: https://github.com/kettle-rb/ast-merge/releases/tag/v2.0.0
269
+ [1.1.0]: https://github.com/kettle-rb/ast-merge/compare/v1.0.0...v1.1.0
270
+ [1.1.0t]: https://github.com/kettle-rb/ast-merge/releases/tag/v1.1.0
80
271
  [1.0.0]: https://github.com/kettle-rb/ast-merge/compare/a63a4858cb229530c1706925bb209546695e8b3a...v1.0.0
81
272
  [1.0.0t]: https://github.com/kettle-rb/ast-merge/tags/v1.0.0
data/README.md CHANGED
@@ -60,21 +60,21 @@ Ast::Merge is **not typically used directly** - instead, use one of the format-s
60
60
 
61
61
  The `*-merge` gem family provides intelligent, AST-based merging for various file formats. At the foundation is [tree_haver][tree_haver], which provides a unified cross-Ruby parsing API that works seamlessly across MRI, JRuby, and TruffleRuby.
62
62
 
63
- | Gem | Format | Parser Backend(s) | Description |
64
- |-----|--------|-------------------|-------------|
65
- | [tree_haver][tree_haver] | Multi | MRI C, Rust, FFI, Java, Prism, Psych, Commonmarker, Markly, Citrus | **Foundation**: Cross-Ruby adapter for parsing libraries (like Faraday for HTTP) |
66
- | [ast-merge][ast-merge] | Text | internal | **Infrastructure**: Shared base classes and merge logic for all `*-merge` gems |
67
- | [prism-merge][prism-merge] | Ruby | [Prism][prism] (via tree_haver) | Smart merge for Ruby source files |
68
- | [psych-merge][psych-merge] | YAML | [Psych][psych] (via tree_haver) | Smart merge for YAML files |
69
- | [json-merge][json-merge] | JSON | [tree-sitter-json][ts-json] (via tree_haver) | Smart merge for JSON files |
70
- | [jsonc-merge][jsonc-merge] | JSONC | [tree-sitter-jsonc][ts-jsonc] (via tree_haver) | ⚠️ Proof of concept; Smart merge for JSON with Comments |
71
- | [bash-merge][bash-merge] | Bash | [tree-sitter-bash][ts-bash] (via tree_haver) | Smart merge for Bash scripts |
72
- | [rbs-merge][rbs-merge] | RBS | [RBS][rbs] | Smart merge for Ruby type signatures |
73
- | [dotenv-merge][dotenv-merge] | Dotenv | internal ([dotenv][dotenv]) | Smart merge for `.env` files |
74
- | [toml-merge][toml-merge] | TOML | [tree-sitter-toml][ts-toml] (via tree_haver) | Smart merge for TOML files |
75
- | [markdown-merge][markdown-merge] | Markdown | [Commonmarker][commonmarker] / [Markly][markly] (via tree_haver) | **Foundation**: Shared base for Markdown mergers with inner code block merging |
76
- | [markly-merge][markly-merge] | Markdown | [Markly][markly] (via tree_haver) | Smart merge for Markdown (CommonMark via cmark-gfm C) |
77
- | [commonmarker-merge][commonmarker-merge] | Markdown | [Commonmarker][commonmarker] (via tree_haver) | Smart merge for Markdown (CommonMark via comrak Rust) |
63
+ | Gem | Format | Parser Backend(s) | Description |
64
+ |------------------------------------------|----------|-----------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------|
65
+ | [tree_haver][tree_haver] | Multi | MRI C, Rust, FFI, Java, Prism, Psych, Commonmarker, Markly, Citrus | **Foundation**: Cross-Ruby adapter for parsing libraries (like Faraday for HTTP) |
66
+ | [ast-merge][ast-merge] | Text | internal | **Infrastructure**: Shared base classes and merge logic for all `*-merge` gems |
67
+ | [prism-merge][prism-merge] | Ruby | [Prism][prism] | Smart merge for Ruby source files |
68
+ | [psych-merge][psych-merge] | YAML | [Psych][psych] | Smart merge for YAML files |
69
+ | [json-merge][json-merge] | JSON | [tree-sitter-json][ts-json] (via tree_haver) | Smart merge for JSON files |
70
+ | [jsonc-merge][jsonc-merge] | JSONC | [tree-sitter-jsonc][ts-jsonc] (via tree_haver) | ⚠️ Proof of concept; Smart merge for JSON with Comments |
71
+ | [bash-merge][bash-merge] | Bash | [tree-sitter-bash][ts-bash] (via tree_haver) | Smart merge for Bash scripts |
72
+ | [rbs-merge][rbs-merge] | RBS | [RBS][rbs] | Smart merge for Ruby type signatures |
73
+ | [dotenv-merge][dotenv-merge] | Dotenv | internal | Smart merge for `.env` files |
74
+ | [toml-merge][toml-merge] | TOML | [Citrus + toml-rb][toml-rb] (default, via tree_haver), [tree-sitter-toml][ts-toml] (via tree_haver) | Smart merge for TOML files |
75
+ | [markdown-merge][markdown-merge] | Markdown | [Commonmarker][commonmarker] / [Markly][markly] (via tree_haver) | **Foundation**: Shared base for Markdown mergers with inner code block merging |
76
+ | [markly-merge][markly-merge] | Markdown | [Markly][markly] (via tree_haver) | Smart merge for Markdown (CommonMark via cmark-gfm C) |
77
+ | [commonmarker-merge][commonmarker-merge] | Markdown | [Commonmarker][commonmarker] (via tree_haver) | Smart merge for Markdown (CommonMark via comrak Rust) |
78
78
 
79
79
  **Example implementations** for the gem templating use case:
80
80
 
@@ -101,11 +101,10 @@ The `*-merge` gem family provides intelligent, AST-based merging for various fil
101
101
  [prism]: https://github.com/ruby/prism
102
102
  [psych]: https://github.com/ruby/psych
103
103
  [ts-json]: https://github.com/tree-sitter/tree-sitter-json
104
- [ts-jsonc]: https://gitlab.com/WhyNotHugo/tree-sitter-jsonc
105
104
  [ts-bash]: https://github.com/tree-sitter/tree-sitter-bash
106
105
  [ts-toml]: https://github.com/tree-sitter-grammars/tree-sitter-toml
107
106
  [rbs]: https://github.com/ruby/rbs
108
- [dotenv]: https://github.com/bkeepers/dotenv
107
+ [toml-rb]: https://github.com/emancu/toml-rb
109
108
  [markly]: https://github.com/ioquatix/markly
110
109
  [commonmarker]: https://github.com/gjtorikian/commonmarker
111
110
 
@@ -143,34 +142,168 @@ require "ast/merge"
143
142
 
144
143
  module MyFormat
145
144
  module Merge
146
- class FreezeNode < Ast::Merge::FreezeNode
147
- # Override methods as needed for your format
145
+ # Inherit from base classes and pass **options for forward compatibility
146
+
147
+ class SmartMerger < Ast::Merge::SmartMergerBase
148
+ DEFAULT_FREEZE_TOKEN = "myformat-merge"
149
+
150
+ def initialize(template, dest, my_custom_option: nil, **options)
151
+ @my_custom_option = my_custom_option
152
+ super(template, dest, **options)
153
+ end
154
+
155
+ protected
156
+
157
+ def analysis_class
158
+ FileAnalysis
159
+ end
160
+
161
+ def default_freeze_token
162
+ DEFAULT_FREEZE_TOKEN
163
+ end
164
+
165
+ def perform_merge
166
+ # Implement format-specific merge logic
167
+ # Returns a MergeResult
168
+ end
148
169
  end
149
-
150
- class MergeResult < Ast::Merge::MergeResult
151
- # Add format-specific output methods
170
+
171
+ class FileAnalysis
172
+ include Ast::Merge::FileAnalyzable
173
+
174
+ def initialize(source, freeze_token: nil, signature_generator: nil, **options)
175
+ @source = source
176
+ @freeze_token = freeze_token
177
+ @signature_generator = signature_generator
178
+ # Process source...
179
+ end
180
+
181
+ def compute_node_signature(node)
182
+ # Return signature array for node matching
183
+ end
184
+ end
185
+
186
+ class ConflictResolver < Ast::Merge::ConflictResolverBase
187
+ def initialize(template_analysis, dest_analysis, preference: :destination,
188
+ add_template_only_nodes: false, match_refiner: nil, **options)
189
+ super(
190
+ strategy: :batch, # or :node, :boundary
191
+ preference: preference,
192
+ template_analysis: template_analysis,
193
+ dest_analysis: dest_analysis,
194
+ add_template_only_nodes: add_template_only_nodes,
195
+ match_refiner: match_refiner,
196
+ **options
197
+ )
198
+ end
199
+
200
+ protected
201
+
202
+ def resolve_batch(result)
203
+ # Implement batch resolution logic
204
+ end
205
+ end
206
+
207
+ class MergeResult < Ast::Merge::MergeResultBase
208
+ def initialize(**options)
209
+ super(**options)
210
+ @statistics = { merged_count: 0 }
211
+ end
212
+
152
213
  def to_my_format
153
214
  to_s
154
215
  end
155
216
  end
217
+
218
+ class MatchRefiner < Ast::Merge::MatchRefinerBase
219
+ def initialize(threshold: 0.7, node_types: nil, **options)
220
+ super(threshold: threshold, node_types: node_types, **options)
221
+ end
222
+
223
+ def similarity(template_node, dest_node)
224
+ # Return similarity score between 0.0 and 1.0
225
+ end
226
+ end
227
+ end
228
+ end
229
+ ```
156
230
 
157
- class FileAnalysis
158
- include Ast::Merge::FileAnalysisBase
231
+ ### Base Classes Reference
159
232
 
160
- # Implement required methods:
161
- # - compute_node_signature(node)
162
- # - extract_freeze_blocks
163
- end
233
+ | Base Class | Purpose | Key Methods to Implement |
234
+ |------------|---------|-------------------------|
235
+ | `SmartMergerBase` | Main merge orchestration | `analysis_class`, `perform_merge` |
236
+ | `ConflictResolverBase` | Resolve node conflicts | `resolve_batch` or `resolve_node_pair` |
237
+ | `MergeResultBase` | Track merge results | `to_s`, format-specific output |
238
+ | `MatchRefinerBase` | Fuzzy node matching | `similarity` |
239
+ | `ContentMatchRefiner` | Text content fuzzy matching | Ready to use |
240
+ | `FileAnalyzable` | File parsing/analysis | `compute_node_signature` |
164
241
 
165
- class SmartMerger
166
- include Ast::Merge::MergerConfig
242
+ ### ContentMatchRefiner
167
243
 
168
- # Implement merge logic
169
- end
170
- end
171
- end
244
+ `Ast::Merge::ContentMatchRefiner` is a built-in match refiner for fuzzy text content matching using Levenshtein distance. Unlike signature-based matching which requires exact content hashes, this refiner allows matching nodes with similar (but not identical) content.
245
+
246
+ ```ruby
247
+ # Basic usage - match nodes with 70% similarity
248
+ refiner = Ast::Merge::ContentMatchRefiner.new(threshold: 0.7)
249
+
250
+ # Only match specific node types
251
+ refiner = Ast::Merge::ContentMatchRefiner.new(
252
+ threshold: 0.6,
253
+ node_types: [:paragraph, :heading]
254
+ )
255
+
256
+ # Custom weights for scoring
257
+ refiner = Ast::Merge::ContentMatchRefiner.new(
258
+ threshold: 0.7,
259
+ weights: {
260
+ content: 0.8, # Levenshtein similarity (default: 0.7)
261
+ length: 0.1, # Length similarity (default: 0.15)
262
+ position: 0.1 # Position in document (default: 0.15)
263
+ }
264
+ )
265
+
266
+ # Custom content extraction
267
+ refiner = Ast::Merge::ContentMatchRefiner.new(
268
+ threshold: 0.7,
269
+ content_extractor: ->(node) { node.text_content.downcase.strip }
270
+ )
271
+
272
+ # Use with a merger
273
+ merger = MyFormat::SmartMerger.new(
274
+ template,
275
+ destination,
276
+ preference: :template,
277
+ match_refiner: refiner
278
+ )
172
279
  ```
173
280
 
281
+ This is particularly useful for:
282
+ - Paragraphs with minor edits (typos, rewording)
283
+ - Headings with slight changes
284
+ - Comments with updated text
285
+ - Any text-based node that may have been slightly modified
286
+
287
+ ### Namespace Reference
288
+
289
+ The `Ast::Merge` module is organized into several namespaces, each with detailed documentation:
290
+
291
+ | Namespace | Purpose | Documentation |
292
+ |-----------|---------|---------------|
293
+ | `Ast::Merge::Detector` | Region detection and merging | [lib/ast/merge/detector/README.md](lib/ast/merge/detector/README.md) |
294
+ | `Ast::Merge::Recipe` | YAML-based merge recipes | [lib/ast/merge/recipe/README.md](lib/ast/merge/recipe/README.md) |
295
+ | `Ast::Merge::Comment` | Comment parsing and representation | [lib/ast/merge/comment/README.md](lib/ast/merge/comment/README.md) |
296
+ | `Ast::Merge::Text` | Plain text AST parsing | [lib/ast/merge/text/README.md](lib/ast/merge/text/README.md) |
297
+ | `Ast::Merge::RSpec` | Shared RSpec examples | [lib/ast/merge/rspec/README.md](lib/ast/merge/rspec/README.md) |
298
+
299
+ **Key Classes by Namespace:**
300
+
301
+ - **Detector**: `Region`, `Base`, `Mergeable`, `FencedCodeBlock`, `YamlFrontmatter`, `TomlFrontmatter`
302
+ - **Recipe**: `Config`, `Runner`, `ScriptLoader`
303
+ - **Comment**: `Line`, `Block`, `Empty`, `Parser`, `Style`
304
+ - **Text**: `SmartMerger`, `FileAnalysis`, `LineNode`, `WordNode`, `Section`
305
+ - **RSpec**: Shared examples and dependency tags for testing `*-merge` implementations
306
+
174
307
  ## 💡 Info you can shake a stick at
175
308
 
176
309
  | Tokens to Remember | [![Gem name][⛳️name-img]][⛳️gem-name] [![Gem namespace][⛳️namespace-img]][⛳️gem-namespace] |
@@ -297,8 +430,8 @@ merger = SomeFormat::Merge::SmartMerger.new(
297
430
  destination,
298
431
  # When conflicts occur, prefer template or destination values
299
432
  preference: :template, # or :destination (default), or a Hash for per-node-type
300
- # Add nodes that only exist in template
301
- add_template_only_nodes: true, # default: false
433
+ # Add nodes that only exist in template (Boolean or callable filter)
434
+ add_template_only_nodes: true, # default: false, or ->(node, entry) { ... }
302
435
  # Custom node type handling
303
436
  node_typing: {}, # optional, for per-node-type preference
304
437
  )
@@ -316,8 +449,41 @@ Control which source wins when both files have the same structural element:
316
449
 
317
450
  Control whether to add nodes that only exist in the template:
318
451
 
319
- - **`true`** - Add new nodes from template
452
+ - **`true`** - Add all template-only nodes
320
453
  - **`false`** (default) - Skip template-only nodes
454
+ - **Callable** - Filter which template-only nodes to add
455
+
456
+ #### Callable Filter
457
+
458
+ When you need fine-grained control over which template-only nodes are added, pass a callable (Proc/Lambda) that receives `(node, entry)` and returns truthy to add or falsey to skip:
459
+
460
+ ```ruby
461
+ # Only add nodes with gem_family signatures
462
+ merger = SomeFormat::Merge::SmartMerger.new(
463
+ template,
464
+ destination,
465
+ add_template_only_nodes: ->(node, entry) {
466
+ sig = entry[:signature]
467
+ sig.is_a?(Array) && sig.first == :gem_family
468
+ }
469
+ )
470
+
471
+ # Only add link definitions that match a pattern
472
+ merger = Markly::Merge::SmartMerger.new(
473
+ template,
474
+ destination,
475
+ add_template_only_nodes: ->(node, entry) {
476
+ entry[:template_node].type == :link_definition &&
477
+ entry[:signature]&.last&.include?("gem")
478
+ }
479
+ )
480
+ ```
481
+
482
+ The `entry` hash contains:
483
+ - `:template_node` - The node being considered for addition
484
+ - `:signature` - The node's signature (Array or other value)
485
+ - `:template_index` - Index in the template statements
486
+ - `:dest_index` - Always `nil` for template-only nodes
321
487
 
322
488
  ## 🔧 Basic Usage
323
489
 
@@ -485,7 +651,7 @@ The `MergerConfig` class provides factory methods that support all options:
485
651
  # Create config preferring destination
486
652
  config = Ast::Merge::MergerConfig.destination_wins(
487
653
  freeze_token: "my-freeze",
488
- signature_generator: my_generator,
654
+ : my_generator,
489
655
  node_typing: my_typing,
490
656
  )
491
657
 
@@ -843,7 +1009,7 @@ Thanks for RTFM. ☺️
843
1009
  [📌gitmoji]: https://gitmoji.dev
844
1010
  [📌gitmoji-img]: https://img.shields.io/badge/gitmoji_commits-%20%F0%9F%98%9C%20%F0%9F%98%8D-34495e.svg?style=flat-square
845
1011
  [🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
846
- [🧮kloc-img]: https://img.shields.io/badge/KLOC-2.382-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
1012
+ [🧮kloc-img]: https://img.shields.io/badge/KLOC-3.271-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
847
1013
  [🔐security]: SECURITY.md
848
1014
  [🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
849
1015
  [📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
@@ -863,3 +1029,6 @@ Thanks for RTFM. ☺️
863
1029
  [💎appraisal2]: https://github.com/appraisal-rb/appraisal2
864
1030
  [💎appraisal2-img]: https://img.shields.io/badge/appraised_by-appraisal2-34495e.svg?plastic&logo=ruby&logoColor=white
865
1031
  [💎d-in-dvcs]: https://railsbling.com/posts/dvcs/put_the_d_in_dvcs/
1032
+
1033
+ [ts-jsonc]: https://gitlab.com/WhyNotHugo/tree-sitter-jsonc
1034
+ [dotenv]: https://github.com/bkeepers/dotenv