AGENTS.md - toml-merge Development Guide
๐ฏ Project Overview
toml-merge is a format-specific implementation of the *-merge gem family for TOML files. It provides intelligent TOML file merging using AST analysis with support for multiple parser backends (tree-sitter, Citrus, Parslet).
Core Philosophy: Intelligent TOML merging that preserves structure, comments, and formatting while applying updates from templates.
Repository: https://github.com/kettle-rb/toml-merge
Current Version: 2.0.1
Required Ruby: >= 3.2.0 (currently developed against Ruby 4.0.1)
๐๏ธ Architecture: Format-Specific Implementation
What toml-merge Provides
-
Toml::Merge::SmartMergerโ TOML-specific SmartMerger implementation -
Toml::Merge::FileAnalysisโ TOML file analysis with table/pair extraction -
Toml::Merge::NodeWrapperโ Wrapper for TOML AST nodes (tables, pairs, arrays) -
Toml::Merge::MergeResultโ TOML-specific merge result -
Toml::Merge::ConflictResolverโ TOML conflict resolution -
Toml::Merge::FreezeNodeโ TOML freeze block support -
Toml::Merge::TableMatchRefinerโ TOML table matching refinement -
Toml::Merge::NodeTypeNormalizerโ Cross-backend type normalization
Key Dependencies
| Gem | Role |
|---|---|
ast-merge (~> 4.0) |
Base classes and shared infrastructure |
tree_haver (~> 5.0) |
Unified parser adapter (tree-sitter, Citrus, Parslet) |
version_gem (~> 1.1) |
Version management |
Parser Backend Support
toml-merge works with multiple TOML parser backends via TreeHaver:
| Backend | Parser | Platform | Notes |
|---|---|---|---|
:mri |
tree-sitter-toml | MRI only | Best performance, requires native library |
:rust |
tree-sitter-toml | MRI only | Rust implementation via tree_stump |
:citrus |
citrus + toml-rb | All platforms | Pure Ruby, good compatibility |
:parslet |
parslet + toml | All platforms | Pure Ruby, alternative parser |
๐ Project Structure
lib/toml/merge/
โโโ smart_merger.rb # Main SmartMerger implementation
โโโ file_analysis.rb # TOML file analysis (tables, pairs)
โโโ node_wrapper.rb # AST node wrapper
โโโ merge_result.rb # Merge result object
โโโ conflict_resolver.rb # Conflict resolution
โโโ freeze_node.rb # Freeze block support
โโโ table_match_refiner.rb # Table matching refinement
โโโ node_type_normalizer.rb # Cross-backend type mapping
โโโ debug_logger.rb # Debug logging
โโโ version.rb
spec/toml/merge/
โโโ smart_merger_spec.rb
โโโ file_analysis_spec.rb
โโโ node_wrapper_spec.rb
โโโ table_match_refiner_spec.rb
โโโ integration/
๐ง Development Workflows
Running Tests
# Full suite (required for coverage thresholds)
bundle exec rspec
# Single file (disable coverage threshold check)
K_SOUP_COV_MIN_HARD=false bundle exec rspec spec/toml/merge/smart_merger_spec.rb
# Specific backend tests
bundle exec rspec --tag mri_backend
bundle exec rspec --tag citrus_backend
bundle exec rspec --tag parslet_backend
Note: Always run commands in the project root (/home/pboling/src/kettle-rb/ast-merge/vendor/toml-merge). Allow direnv to load environment variables first by doing a plain cd before running commands.
Coverage Reports
cd /home/pboling/src/kettle-rb/ast-merge/vendor/toml-merge
bin/rake coverage && bin/kettle-soup-cover -d
Key ENV variables (set in .envrc, loaded via direnv allow):
-
K_SOUP_COV_DO=trueโ Enable coverage -
K_SOUP_COV_MIN_LINE=100โ Line coverage threshold -
K_SOUP_COV_MIN_BRANCH=82โ Branch coverage threshold -
K_SOUP_COV_MIN_HARD=trueโ Fail if thresholds not met
Code Quality
bundle exec rake reek
bundle exec rake rubocop_gradual
๐ Project Conventions
API Conventions
SmartMerger API
-
mergeโ Returns a String (the merged TOML content) -
merge_resultโ Returns a MergeResult object -
to_son MergeResult returns the merged content as a string
TOML-Specific Features
Table Matching:
merger = Toml::Merge::SmartMerger.new(template_toml, dest_toml)
result = merger.merge
Freeze Blocks:
[server]
# toml-merge:freeze
port = 8080 # Custom port, don't override
# toml-merge:unfreeze
host = "localhost"
Node Type Normalization:
- Handles differences between tree-sitter (
table), Citrus (table), and Parslet (table) backends - Provides canonical type names for consistent matching
kettle-dev Tooling
This project uses kettle-dev for gem maintenance automation:
- Rakefile: Sourced from kettle-dev template
- CI Workflows: GitHub Actions and GitLab CI managed via kettle-dev
-
Releases: Use
kettle-releasefor automated release process
Version Requirements
- Ruby >= 3.2.0 (gemspec), developed against Ruby 4.0.1 (
.tool-versions) -
ast-merge>= 4.0.0 required -
tree_haver>= 5.0.3 required
๐งช Testing Patterns
TreeHaver Dependency Tags
All spec files use TreeHaver RSpec dependency tags for conditional execution:
Available tags:
-
:toml_grammarโ Requires TOML grammar (any backend) -
:mri_backendโ Requires tree-sitter MRI backend -
:rust_backendโ Requires tree-sitter Rust backend -
:citrus_backendโ Requires Citrus backend -
:parslet_backendโ Requires Parslet backend -
:toml_parsingโ Requires any TOML parser
โ CORRECT โ Use dependency tag on describe/context/it:
RSpec.describe Toml::Merge::SmartMerger, :toml_grammar do
# Skipped if no TOML parser available
end
it "parses with tree-sitter", :mri_backend, :toml_grammar do
# Skipped if tree-sitter not available
end
context "when comparing backends", :citrus_backend, :parslet_backend do
# Skipped unless both backends available
end
โ WRONG โ Never use manual skip checks:
before do
skip "Requires tree-sitter" unless tree_sitter_available? # DO NOT DO THIS
end
Backend Isolation
CRITICAL: Tests must respect backend isolation to prevent FFI/MRI conflicts:
# Use TreeHaver.with_backend to ensure backend isolation
TreeHaver.with_backend(:mri) do
analysis = Toml::Merge::FileAnalysis.new(toml_source)
end
Shared Examples
toml-merge uses shared examples from ast-merge:
it_behaves_like "Ast::Merge::FileAnalyzable"
it_behaves_like "Ast::Merge::ConflictResolverBase"
it_behaves_like "a reproducible merge", "scenario_name", { preference: :template }
๐ Critical Files
| File | Purpose |
|---|---|
lib/toml/merge/smart_merger.rb |
Main TOML SmartMerger implementation |
lib/toml/merge/file_analysis.rb |
TOML file analysis and table extraction |
lib/toml/merge/node_wrapper.rb |
TOML node wrapper with type-specific methods |
lib/toml/merge/table_match_refiner.rb |
TOML table matching refinement |
lib/toml/merge/node_type_normalizer.rb |
Cross-backend type normalization |
spec/spec_thin_helper.rb |
Test suite entry point with TreeHaver integration |
spec/support/dependency_tags.rb |
TreeHaver dependency tag integration |
.envrc |
Coverage thresholds and backend configuration |
๐ Common Tasks
# Run all specs with coverage
bundle exec rake spec
# Generate coverage report
bundle exec rake coverage
# Check code quality
bundle exec rake reek
bundle exec rake rubocop_gradual
# Run with specific backend
TREE_HAVER_BACKEND=citrus bundle exec rspec
# Prepare and release
kettle-changelog && kettle-release
๐ Integration Points
-
ast-merge: Inherits base classes (SmartMergerBase,FileAnalyzable, etc.) -
tree_haver: Multi-backend TOML parsing (tree-sitter, Citrus, Parslet) -
RSpec: Full integration via
ast/merge/rspecandtree_haver/rspec -
SimpleCov: Coverage tracked for
lib/**/*.rb; spec directory excluded
๐ก Key Insights
- Multi-backend support: toml-merge works with 4 different TOML parsers; use backend tags to test all
-
Backend isolation is critical: Always use
TreeHaver.with_backendto prevent FFI/MRI conflicts -
Node type normalization: Different backends use different node type names;
NodeTypeNormalizerprovides canonical types - Table matching: TOML tables are matched by name; nested tables are handled hierarchically
-
Freeze blocks use
# toml-merge:freeze: Language-specific comment syntax -
Never use manual skip checks: Always use TreeHaver dependency tags (
:toml_grammar,:mri_backend, etc.) - Backend conflicts can cause segfaults: The TreeHaver backend protection system prevents mixing FFI and MRI backends
๐ซ Common Pitfalls
-
NEVER mix FFI and MRI backends โ Use
TreeHaver.with_backendfor isolation -
NEVER use manual skip checks โ Use dependency tags (
:toml_grammar,:mri_backend) - NEVER assume a specific backend โ Write tests that work with any TOML parser
- Do NOT load vendor gems โ They are not part of this project; they do not exist in CI
-
Use
tmp/for temporary files โ Never use/tmpor other system directories -
Do NOT chain
cdwith&&โ Runcdas a separate command sodirenvloads ENV
๐ง TOML-Specific Notes
Table Structures
# Simple table
[server]
port = 8080
# Nested table (dot notation)
[server.ssl]
enabled = true
# Nested table (bracket notation)
[database]
[database.connection]
timeout = 30
# Array of tables
[[servers]]
name = "alpha"
[[servers]]
name = "beta"
Merge Behavior
-
Tables: Matched by full path (
server.ssl) - Pairs: Matched by key name within table
- Arrays: Can be merged or replaced based on preference
- Comments: Preserved when attached to tables/pairs
- Freeze blocks: Protect customizations from template updates