botrytis 0.1.0 → 1.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 98b2aad1fbfde0c8929d30456317928a419706b22bb3e27b04427ec31be28fd9
4
- data.tar.gz: 365b2b618522e77e4f913e03759267555880deebf27e86a2e68cc52a014a791b
3
+ metadata.gz: fdc6ddc981fa9cd6507c3534d0849cc7c2e773cadd5360e3daed1b91c4b28039
4
+ data.tar.gz: 2010aea7e2644b3e6c0cc43abd6ef73cf2f92165af64270d9085d285f539abfe
5
5
  SHA512:
6
- metadata.gz: 51d072e08697f9988c3ee17df20b484798e63c0ad7eebb12d75296de40018156a249e851fda897d939cc2e60efbaffdfbd1624d08e43b92a3a8aa58f293fbbc4
7
- data.tar.gz: 50b91ccf5c4deb8d9b0015a26e910e816fda2f3b1a8a10a331bd4478cdd3c03b456c34c4e73f5cc2edff954d8eebedc0135ddb61c451c445bf7291201ecc571f
6
+ metadata.gz: 98ab98ee673362e2d352a1dd2f6b6ac3913611204a1e156b17915541d2873e0a8d1cdc310879f39e50aaa026d3fc9b4f1b629e0c0606fae555738cbfaac0b2e8
7
+ data.tar.gz: bffbf314f6a14f2bb398ed4a52c5d716dc849d5229c69e580133dd25581d661bbf5765859ec7f54b3e0edb3e38ff6d5d3695fb7dd0403e65a578860dd99c8982
@@ -0,0 +1,19 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(bundle exec rake:*)",
5
+ "Bash(bundle exec rspec:*)",
6
+ "Bash(bundle exec irb:*)",
7
+ "Bash(bundle exec cucumber:*)",
8
+ "Bash(bundle exec ruby:*)",
9
+ "Bash(mkdir:*)",
10
+ "Bash(cp:*)",
11
+ "Bash(touch:*)",
12
+ "Bash(BOTRYTIS_LIVE_API=true bundle exec cucumber --name \"Authentication variations\")",
13
+ "Bash(bundle exec rails generate model:*)",
14
+ "Bash(bundle exec rails generate:*)",
15
+ "Bash(ls:*)"
16
+ ],
17
+ "deny": []
18
+ }
19
+ }
data/CLAUDE.md ADDED
@@ -0,0 +1,85 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Project Overview
6
+
7
+ Botrytis is a Ruby gem that provides LLM-powered semantic matching for Cucumber step definitions. It enables fuzzy matching of Cucumber steps using large language models, making BDD tests more flexible by matching similar but not exact step text.
8
+
9
+ ## Architecture
10
+
11
+ ### Core Components
12
+
13
+ - **SemanticMatcher** (`lib/botrytis/semantic_matcher.rb`): Main matching engine that finds semantic matches between step text and available step definitions using LLMs
14
+ - **SemanticMatchGenerator** (`lib/botrytis/semantic_match_generator.rb`): LLM interaction layer built on Sublayer for generating semantic matches
15
+ - **Configuration** (`lib/botrytis/configuration.rb`): Configurable settings for LLM provider, model, confidence thresholds, and caching
16
+ - **Formatter** (`lib/botrytis/formatter.rb`): Custom Cucumber formatter integration
17
+
18
+ ### Key Dependencies
19
+
20
+ - **cucumber**: Core Cucumber framework (>= 9)
21
+ - **sublayer**: LLM abstraction layer (>= 0.2.8) for AI provider interactions
22
+ - **rspec**: Testing framework
23
+
24
+ ### Configuration System
25
+
26
+ The gem supports configuration of:
27
+ - LLM provider (default: :openai)
28
+ - Model name (default: "gpt-4o")
29
+ - Confidence threshold (default: 0.7)
30
+ - Caching enabled/disabled (default: true)
31
+ - Cache directory (default: ".botrytis_cache")
32
+
33
+ ## Development Commands
34
+
35
+ ### Testing
36
+ ```bash
37
+ # Run all tests
38
+ bundle exec rake spec
39
+ # or
40
+ bundle exec rspec
41
+
42
+ # Run specific test files
43
+ bundle exec rspec spec/botrytis_spec.rb
44
+
45
+ # Run with specific options
46
+ bundle exec rspec --fail-fast
47
+ ```
48
+
49
+ ### Building and Installation
50
+ ```bash
51
+ # Build the gem
52
+ bundle exec rake build
53
+
54
+ # Install locally
55
+ bundle exec rake install:local
56
+
57
+ # Clean build artifacts
58
+ bundle exec rake clean
59
+ ```
60
+
61
+ ### Development Setup
62
+ ```bash
63
+ # Install dependencies
64
+ bundle install
65
+
66
+ # Run interactive console with gem loaded
67
+ bundle exec irb -r botrytis
68
+ ```
69
+
70
+ ## Semantic Matching Flow
71
+
72
+ 1. Step text is compared against available step definition patterns
73
+ 2. If caching is enabled, check for cached results first
74
+ 3. Query LLM through Sublayer with step text and available patterns
75
+ 4. LLM returns confidence score and best match pattern
76
+ 5. If confidence meets threshold, return Cucumber::Glue::StepMatch
77
+ 6. Cache result if caching enabled
78
+
79
+ ## File Structure Notes
80
+
81
+ - Main entry point: `lib/botrytis.rb`
82
+ - Core logic in `lib/botrytis/` directory
83
+ - RSpec tests in `spec/` directory
84
+ - Gem configuration in `botrytis.gemspec`
85
+ - Rake tasks defined in `Rakefile` (default task is `:spec`)
data/README.md CHANGED
@@ -1 +1,258 @@
1
- # Botrytis BDD
1
+ # Botrytis
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/botrytis.svg)](https://badge.fury.io/rb/botrytis)
4
+
5
+ **LLM-powered semantic matching for your Cucumber steps**
6
+
7
+ Botrytis makes your BDD tests more flexible by using Large Language Models to match semantically similar Cucumber steps, even when they don't match exactly.
8
+
9
+ ## What it does
10
+
11
+ Instead of your Cucumber tests failing when step text doesn't match exactly:
12
+
13
+ ```gherkin
14
+ # ❌ This fails without Botrytis
15
+ Given the user has authenticated successfully # No matching step definition
16
+
17
+ # ✅ But this step definition exists:
18
+ Given(/^the user has logged in to their account$/) do
19
+ # implementation
20
+ end
21
+ ```
22
+
23
+ With Botrytis, the LLM understands that "authenticated successfully" and "logged in to their account" are semantically equivalent, so your test passes!
24
+
25
+ ## Features
26
+
27
+ - 🧠 **Semantic step matching** using OpenAI, Claude, or Gemini
28
+ - 🎯 **Confidence-based matching** with configurable thresholds
29
+ - ⚡ **Intelligent caching** to avoid repeated LLM calls
30
+ - 🔄 **Parameter extraction** from semantically matched steps
31
+ - 📊 **Match reporting** shows how many fuzzy matches were found
32
+ - 🧪 **Live API testing** mode for development
33
+
34
+ ## Installation
35
+
36
+ Add this line to your application's Gemfile:
37
+
38
+ ```ruby
39
+ gem 'botrytis'
40
+ ```
41
+
42
+ And then execute:
43
+
44
+ ```bash
45
+ $ bundle install
46
+ ```
47
+
48
+ Or install it yourself as:
49
+
50
+ ```bash
51
+ $ gem install botrytis
52
+ ```
53
+
54
+ ## Quick Start
55
+
56
+ 1. **Add to your Cucumber support files**:
57
+
58
+ ```ruby
59
+ # features/support/env.rb
60
+ require 'botrytis/cucumber'
61
+ ```
62
+
63
+ 2. **Configure your LLM provider** (create `features/support/botrytis.rb`):
64
+
65
+ ```ruby
66
+ require 'botrytis'
67
+
68
+ Botrytis.configure do |config|
69
+ config.llm_provider = :openai # or :claude, :gemini
70
+ config.model_name = "gpt-4o"
71
+ config.confidence_threshold = 0.7
72
+ config.cache_enabled = true
73
+ end
74
+ ```
75
+
76
+ 3. **Set your API key** (e.g., in `.env` or environment):
77
+
78
+ ```bash
79
+ export OPENAI_API_KEY=your_api_key_here
80
+ ```
81
+
82
+ 4. **Run your tests** and see semantic matching in action!
83
+
84
+ ```bash
85
+ $ bundle exec cucumber
86
+
87
+ # Output shows:
88
+ # 6 scenarios (6 passed)
89
+ # 24 steps (24 passed)
90
+ # 🎯 Botrytis Semantic Matching Summary: 10 fuzzy matches found
91
+ ```
92
+
93
+ ## Examples
94
+
95
+ ### Authentication Variations
96
+
97
+ ```gherkin
98
+ # All of these match the same step definition:
99
+ Given the user has logged in to their account # Exact match
100
+ Given the user has authenticated successfully # Semantic match ✨
101
+ Given the user has signed in to their account # Semantic match ✨
102
+ ```
103
+
104
+ ### Action Variations
105
+
106
+ ```gherkin
107
+ # Step definition:
108
+ When(/^they click the "([^"]*)" button$/) do |button_name|
109
+ # implementation
110
+ end
111
+
112
+ # These all work:
113
+ When they click the "Buy Now" button # Exact match
114
+ When they press the "Buy Now" button # Semantic match ✨
115
+ When they tap the "Buy Now" button # Semantic match ✨
116
+ When they hit the purchase button # Semantic match ✨
117
+ When they mash the buy button # Semantic match ✨
118
+ When they gently caresses the "Buy Now" button # Semantic match ✨
119
+ ```
120
+
121
+ ### Assertion Variations
122
+
123
+ ```gherkin
124
+ # Step definition:
125
+ Then(/^they should see a confirmation message$/) do
126
+ # implementation
127
+ end
128
+
129
+ # These all work:
130
+ Then they should see a confirmation message # Exact match
131
+ Then they should view a confirmation message # Semantic match ✨
132
+ Then they receive a success notification # Semantic match ✨
133
+ Then they get a notification # Semantic match ✨
134
+ ```
135
+
136
+ ## Configuration
137
+
138
+ ```ruby
139
+ Botrytis.configure do |config|
140
+ # LLM Provider (required)
141
+ config.llm_provider = :openai # :openai, :claude, or :gemini
142
+
143
+ # Model name (required)
144
+ config.model_name = "gpt-4o" # or "claude-3-sonnet", "gemini-pro", etc.
145
+
146
+ # Confidence threshold (0.0 - 1.0)
147
+ config.confidence_threshold = 0.7 # Only matches above this confidence
148
+
149
+ # Caching
150
+ config.cache_enabled = true # Cache LLM responses
151
+ config.cache_directory = ".botrytis_cache" # Cache location
152
+ end
153
+ ```
154
+
155
+ ### LLM Provider Setup
156
+
157
+ **OpenAI**:
158
+ ```ruby
159
+ config.llm_provider = :openai
160
+ config.model_name = "gpt-4o" # or "gpt-4", "gpt-3.5-turbo"
161
+ # Set OPENAI_API_KEY environment variable
162
+ ```
163
+
164
+ **Claude**:
165
+ ```ruby
166
+ config.llm_provider = :claude
167
+ config.model_name = "claude-3-sonnet-20240229"
168
+ # Set ANTHROPIC_API_KEY environment variable
169
+ ```
170
+
171
+ **Gemini**:
172
+ ```ruby
173
+ config.llm_provider = :gemini
174
+ config.model_name = "gemini-pro"
175
+ # Set GOOGLE_API_KEY environment variable
176
+ ```
177
+
178
+ ## Development & Testing
179
+
180
+ ### Running Tests
181
+
182
+ ```bash
183
+ # Run all tests with mocked responses (fast)
184
+ bundle exec cucumber
185
+
186
+ # Run with live API calls (requires API key)
187
+ BOTRYTIS_LIVE_API=true bundle exec cucumber
188
+
189
+ # Run RSpec unit tests
190
+ bundle exec rspec
191
+ ```
192
+
193
+ ### Understanding the Output
194
+
195
+ When semantic matching occurs, you'll see a summary at the end:
196
+
197
+ ```bash
198
+ 🎯 Botrytis Semantic Matching Summary: 10 fuzzy matches found
199
+ ```
200
+
201
+ This tells you how many steps were matched semantically vs. exactly.
202
+
203
+ ### Cache Management
204
+
205
+ Botrytis caches LLM responses to improve performance:
206
+
207
+ ```bash
208
+ # Clear cache
209
+ rm -rf .botrytis_cache
210
+
211
+ # Disable caching for development
212
+ Botrytis.configure do |config|
213
+ config.cache_enabled = false
214
+ end
215
+ ```
216
+
217
+ ## How It Works
218
+
219
+ 1. **Step Execution**: When Cucumber can't find an exact step match, Botrytis intervenes
220
+ 2. **LLM Query**: The step text and available step patterns are sent to your configured LLM
221
+ 3. **Semantic Analysis**: The LLM analyzes semantic similarity and extracts parameters
222
+ 4. **Confidence Check**: Only matches above the confidence threshold are used
223
+ 5. **Execution**: The matched step definition runs with extracted parameters
224
+ 6. **Caching**: Results are cached to avoid repeated API calls
225
+
226
+ ## Requirements
227
+
228
+ - Ruby 3.1.0 or higher
229
+ - Cucumber 9.0 or higher
230
+ - Sublayer 0.2.8 or higher
231
+ - API key for your chosen LLM provider
232
+
233
+ ## Contributing
234
+
235
+ Bug reports and pull requests are welcome on GitHub at https://github.com/sublayerapp/botrytis.
236
+
237
+ ### Development Setup
238
+
239
+ ```bash
240
+ git clone https://github.com/sublayerapp/botrytis.git
241
+ cd botrytis
242
+ bundle install
243
+
244
+ # Run tests
245
+ bundle exec rake spec
246
+ bundle exec cucumber
247
+
248
+ # Build gem
249
+ bundle exec rake build
250
+ ```
251
+
252
+ ## License
253
+
254
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
255
+
256
+ ## Why "Botrytis"?
257
+
258
+ Botrytis is a genus of fungi known for being both beneficial and parasitic - much like how this gem helps your tests pass by being a little "fuzzy" with step matching! 🍄
@@ -0,0 +1,6 @@
1
+ require 'cucumber'
2
+
3
+ # Create a simple test to see what step_arguments should look like
4
+ puts "Testing step argument format..."
5
+
6
+ # Let's run a very simple cucumber test and see what gets created
data/future_tests.md ADDED
@@ -0,0 +1,141 @@
1
+ # Future Test Ideas for Botrytis
2
+
3
+ ## Parameter Translation & Extraction Testing
4
+
5
+ These are advanced test scenarios for future development that go beyond the basic semantic matching demonstrated in the blog post.
6
+
7
+ ### Complex Parameter Extraction
8
+ Test the ability to extract parameters from semantically similar but structurally different steps.
9
+
10
+ **Example:**
11
+ - **Defined step**: `When I (buy|sell) (\d+) (.*) for \$(\d+\.\d+)`
12
+ - **Test cases**:
13
+ - "When I purchase 3 bananas for $2.50" → should match with params `["purchase", "3", "bananas", "2.50"]`
14
+ - "When I acquire 5 oranges for $10.00" → should match with params `["acquire", "5", "oranges", "10.00"]`
15
+ - "When I sell 2 cars for $15000.00" → should match with params `["sell", "2", "cars", "15000.00"]`
16
+
17
+ ### Text-to-Number Parameter Conversion
18
+ Test semantic understanding of different number representations.
19
+
20
+ **Example:**
21
+ - **Defined step**: `Given I have (\d+) apples`
22
+ - **Test cases**:
23
+ - "Given I have five apples" → should extract `"5"`
24
+ - "Given I possess a dozen apples" → should extract `"12"`
25
+ - "Given I own several apples" → should handle ambiguous quantities
26
+
27
+ ### Date/Time Parameter Semantic Matching
28
+ Test interpretation of different time expressions.
29
+
30
+ **Example:**
31
+ - **Defined step**: `When I schedule a meeting for (\d{4}-\d{2}-\d{2})`
32
+ - **Test cases**:
33
+ - "When I schedule a meeting for tomorrow" → should convert to actual date
34
+ - "When I schedule a meeting for next Friday" → should convert to appropriate date
35
+ - "When I schedule a meeting for Christmas" → should handle holiday conversion
36
+
37
+ ### Multiple Parameter Reordering
38
+ Test ability to match steps where parameters appear in different orders.
39
+
40
+ **Example:**
41
+ - **Defined step**: `Given (\w+) has (\d+) (.*) in their (\w+)`
42
+ - **Test cases**:
43
+ - "Given Alice has 5 books in their backpack" → `["Alice", "5", "books", "backpack"]`
44
+ - "Given there are 3 pencils in Bob's drawer" → should reorder to `["Bob", "3", "pencils", "drawer"]`
45
+
46
+ ## Advanced Confidence Testing
47
+
48
+ ### Confidence Threshold Edge Cases
49
+ Test behavior at various confidence levels to validate threshold settings.
50
+
51
+ - Steps that should match at 0.9+ confidence
52
+ - Steps that should match at 0.7-0.8 confidence
53
+ - Steps that should be rejected below 0.7 confidence
54
+ - Borderline cases that test threshold boundaries
55
+
56
+ ### Ambiguous Step Resolution
57
+ Test handling when multiple step definitions could match with similar confidence.
58
+
59
+ **Example:**
60
+ - **Defined steps**:
61
+ - `When I click the save button`
62
+ - `When I click the submit button`
63
+ - **Ambiguous input**: "When I click the confirm button"
64
+ - **Expected behavior**: Either pick highest confidence match or request clarification
65
+
66
+ ## Performance & Scale Testing
67
+
68
+ ### Large Step Definition Sets
69
+ Test performance with hundreds of step definitions to ensure semantic matching scales.
70
+
71
+ ### Caching Effectiveness
72
+ Validate that caching improves performance for repeated semantic matches.
73
+
74
+ ### LLM Provider Comparison
75
+ Test semantic matching quality across different LLM providers (OpenAI, Anthropic, local models).
76
+
77
+ ## Error Handling & Resilience
78
+
79
+ ### LLM Service Failures
80
+ Test graceful degradation when LLM service is unavailable:
81
+ - Should fall back to exact matching
82
+ - Should provide helpful error messages
83
+ - Should not crash the test suite
84
+
85
+ ### Malformed LLM Responses
86
+ Test handling of unexpected LLM response formats:
87
+ - Invalid JSON responses
88
+ - Missing required fields
89
+ - Confidence scores outside 0.0-1.0 range
90
+
91
+ ### Network Timeout Scenarios
92
+ Test behavior under poor network conditions:
93
+ - Slow LLM responses
94
+ - Connection timeouts
95
+ - Retry logic validation
96
+
97
+ ## Integration with BDD Tools
98
+
99
+ ### Multiple Cucumber Versions
100
+ Test compatibility across different versions of Cucumber gem.
101
+
102
+ ### Other BDD Frameworks
103
+ Explore integration with:
104
+ - RSpec feature specs
105
+ - Turnip
106
+ - Spinach
107
+
108
+ ### IDE Integration
109
+ Test semantic matching in development environments:
110
+ - Step definition discovery in IDEs
111
+ - Autocomplete with semantic suggestions
112
+ - Real-time matching feedback
113
+
114
+ ## Real-World Scenario Testing
115
+
116
+ ### Business Domain Vocabularies
117
+ Test semantic matching within specific business contexts:
118
+ - E-commerce scenarios (buy/purchase/order)
119
+ - Financial scenarios (pay/transfer/deposit)
120
+ - Healthcare scenarios (diagnose/treat/prescribe)
121
+
122
+ ### Multi-language Step Definitions
123
+ Test semantic matching across different natural languages:
124
+ - English variations
125
+ - Formal vs informal language
126
+ - Technical vs business terminology
127
+
128
+ ## Security & Privacy Considerations
129
+
130
+ ### Sensitive Data in Steps
131
+ Ensure no sensitive information is sent to LLM providers:
132
+ - Test with steps containing mock credentials
133
+ - Validate data sanitization
134
+ - Test privacy-preserving modes
135
+
136
+ ### LLM Provider Data Retention
137
+ Understand and test implications of different LLM providers' data policies.
138
+
139
+ ## Conclusion
140
+
141
+ These advanced test scenarios will help ensure Botrytis becomes a robust, production-ready tool for semantic step matching. They build upon the basic functionality demonstrated in the blog post examples.
@@ -0,0 +1,151 @@
1
+ require 'cucumber'
2
+ require 'botrytis'
3
+
4
+ module Botrytis
5
+ module CucumberIntegration
6
+ @@step_definitions = []
7
+ @@semantic_matcher = nil
8
+ @@semantic_matches_count = 0
9
+ @@total_step_attempts = 0
10
+
11
+ def self.install!
12
+ # Hook into step matching process instead of undefined creation
13
+ Cucumber::Glue::RegistryAndMore.prepend(SemanticStepMatcher)
14
+
15
+ # Hook into step definition registration to collect them
16
+ Cucumber::Glue::StepDefinition.prepend(StepDefinitionCollector)
17
+
18
+ # Initialize semantic matcher
19
+ @@semantic_matcher = Botrytis::SemanticMatcher.new
20
+ end
21
+
22
+ def self.step_definitions
23
+ @@step_definitions
24
+ end
25
+
26
+ def self.semantic_matcher
27
+ @@semantic_matcher
28
+ end
29
+
30
+ def self.record_semantic_match!
31
+ @@semantic_matches_count += 1
32
+ end
33
+
34
+ def self.record_step_attempt!
35
+ @@total_step_attempts += 1
36
+ end
37
+
38
+ def self.semantic_matches_count
39
+ @@semantic_matches_count
40
+ end
41
+
42
+ def self.total_step_attempts
43
+ @@total_step_attempts
44
+ end
45
+
46
+ def self.print_summary
47
+ if @@semantic_matches_count > 0
48
+ puts "\n🎯 Botrytis Semantic Matching Summary: #{@@semantic_matches_count} fuzzy matches found"
49
+ end
50
+ end
51
+
52
+ module StepDefinitionCollector
53
+ def initialize(*args)
54
+ super
55
+ # Add this step definition to our collection
56
+ Botrytis::CucumberIntegration.add_step_definition(self)
57
+ end
58
+ end
59
+
60
+ def self.add_step_definition(step_def)
61
+ # Create an adapter to make the step definition compatible with semantic matcher
62
+ adapter = StepDefinitionAdapter.new(step_def)
63
+ @@step_definitions << adapter
64
+ end
65
+
66
+ # Adapter to make modern Cucumber StepDefinition compatible with semantic matcher
67
+ class StepDefinitionAdapter
68
+ def initialize(step_def)
69
+ @step_def = step_def
70
+ end
71
+
72
+ def regexp_source
73
+ @step_def.expression.to_s
74
+ end
75
+
76
+ def proc
77
+ # Create a proc that delegates to the step definition
78
+ lambda { |*args| @step_def.invoke(nil, *args) }
79
+ end
80
+
81
+ def method_missing(method, *args, &block)
82
+ @step_def.send(method, *args, &block)
83
+ end
84
+
85
+ def respond_to_missing?(method, include_private = false)
86
+ @step_def.respond_to?(method, include_private)
87
+ end
88
+ end
89
+
90
+ module SemanticStepMatcher
91
+ def step_matches(name_to_match)
92
+ # First try the normal step matching
93
+ matches = super(name_to_match)
94
+
95
+ if matches.any?
96
+ return matches
97
+ end
98
+
99
+ # If no exact matches, try semantic matching
100
+ # Track semantic match attempts
101
+ Botrytis::CucumberIntegration.record_step_attempt!
102
+ semantic_match = attempt_semantic_match(name_to_match)
103
+
104
+ if semantic_match
105
+ # Semantic match found
106
+ Botrytis::CucumberIntegration.record_semantic_match!
107
+ return [semantic_match]
108
+ else
109
+ # No semantic match found
110
+ return matches # Return empty array, which will result in undefined step
111
+ end
112
+ end
113
+
114
+ private
115
+
116
+ def attempt_semantic_match(step_name)
117
+ step_definitions = Botrytis::CucumberIntegration.step_definitions
118
+ semantic_matcher = Botrytis::CucumberIntegration.semantic_matcher
119
+
120
+ return nil if step_definitions.empty? || semantic_matcher.nil?
121
+
122
+ # Use the semantic matcher to find a match
123
+ match = semantic_matcher.find_match(step_name, step_definitions)
124
+
125
+ return match if match
126
+ nil
127
+ end
128
+ end
129
+ end
130
+ end
131
+
132
+ # Install the semantic matching when this file is loaded
133
+ Botrytis::CucumberIntegration.install!
134
+
135
+ # Custom StepMatch class for semantic matches that avoids display corruption
136
+ # The core issue is that Cucumber tries to highlight parameters in step text
137
+ # based on parameter positions from the constructed text, but the positions
138
+ # don't align with the original step text, causing garbled display.
139
+ class SemanticStepMatch < Cucumber::StepMatch
140
+ def replace_arguments(step_name, format, colour)
141
+ # For semantic matches, don't try to replace/highlight arguments
142
+ # Just return the original step name to avoid garbled text from
143
+ # parameter position mismatches
144
+ step_name
145
+ end
146
+ end
147
+
148
+ # Print summary at exit
149
+ at_exit do
150
+ Botrytis::CucumberIntegration.print_summary
151
+ end
@@ -0,0 +1,75 @@
1
+ require 'cucumber/formatter/console'
2
+ require_relative '../botrytis'
3
+
4
+ module Botrytis
5
+ class Formatter
6
+ include Cucumber::Formatter::Console
7
+
8
+ def initialize(config)
9
+ @config = config
10
+
11
+ # Initialize Botrytis configuration if not already done
12
+ unless defined?(Botrytis.configuration)
13
+ Botrytis.configure do |botrytis_config|
14
+ botrytis_config.confidence_threshold = 0.7
15
+ botrytis_config.cache_enabled = false
16
+ botrytis_config.llm_provider = :openai
17
+ botrytis_config.model_name = "gpt-4o"
18
+ end
19
+ end
20
+
21
+ @semantic_matcher = SemanticMatcher.new
22
+ @step_definitions = []
23
+
24
+ # Use the modern event system to collect step definitions
25
+ config.on_event(:step_definition_registered) do |event|
26
+ @step_definitions << event.step_definition
27
+ end
28
+
29
+ # Register semantic matcher once all setup is done
30
+ config.on_event(:test_run_started) do |event|
31
+ register_semantic_matcher
32
+ end
33
+ end
34
+
35
+ def register_semantic_matcher
36
+ # Find the correct StepDefinition class to monkey patch
37
+ step_def_class = if defined?(Cucumber::Glue::StepDefinition)
38
+ Cucumber::Glue::StepDefinition
39
+ elsif defined?(Cucumber::StepDefinition)
40
+ Cucumber::StepDefinition
41
+ else
42
+ # Try to find any step definition class
43
+ Cucumber.constants.select do |c|
44
+ Cucumber.const_get(c).is_a?(Class) && c.to_s.include?('Step')
45
+ end.first&.then { |c| Cucumber.const_get(c) }
46
+ end
47
+
48
+ return unless step_def_class
49
+
50
+ @original_match_method = step_def_class.instance_method(:match)
51
+
52
+ semantic_matcher = @semantic_matcher
53
+ step_definitions = @step_definitions
54
+
55
+ step_def_class.define_method(:match) do |step_name|
56
+ result = @original_match_method.bind(self).call(step_name)
57
+
58
+ if result.nil?
59
+ puts "\n🥒 Botrytis is looking for a fuzzy match for: \"#{step_name}\""
60
+
61
+ match = semantic_matcher.find_match(step_name, step_definitions)
62
+
63
+ if match
64
+ puts "✅ Found a semantic match!"
65
+ return match
66
+ else
67
+ puts "❌ No semantic match found"
68
+ end
69
+ end
70
+
71
+ result
72
+ end
73
+ end
74
+ end
75
+ end
@@ -6,7 +6,45 @@ module Botrytis
6
6
  name: "step_match_result",
7
7
  description: "Results of semantic matching for a cucumber step",
8
8
  attributes: [
9
- { name: "match_found", description: "
9
+ { name: "step_text_analysis", description: "Analysis of the step text, including semantic meaning and intent" },
10
+ { name: "match_found", description: "Indicates if a match was found, either the string yes or no" },
11
+ { name: "best_match_pattern", description: "The pattern that best matches semantically" },
12
+ { name: "confidence", description: "Confidence score of the match (0.0 - 1.0)" },
13
+ { name: "parameter_values", description: "A comma separated list of parameter values extracted from the match" }
10
14
  ]
15
+
16
+ def initialize(step_text:, available_patterns:)
17
+ @step_text = step_text
18
+ @available_patterns = available_patterns
19
+ end
20
+
21
+ def generate
22
+ super
23
+ end
24
+
25
+ def prompt
26
+ <<-PROMPT
27
+ You are a semantic matcher for Cucumber step definitions. Your task is to determine if a step text semantically matches one of the available regex patterns, even if it doesn't match exactly.
28
+
29
+ Step Text: "#{@step_text}"
30
+
31
+ Available Step Definition Patterns:
32
+ #{@available_patterns.join("\n")}
33
+
34
+ For each pattern, consider:
35
+ 1. The semantic meaning/intent of the step
36
+ 2. The structure of the pattern
37
+ 3. Any parameters that would need to be extracted
38
+
39
+ Choose the pattern that best matches the step text semantically.
40
+ If no pattern is a good semantic match, indicate that no match was found.
41
+
42
+ If you find a match, extract any parameters that would be captured by the pattern. And return them as a comma separated list.
43
+ For example, if the pattern is "I have (\\d+) cucumbers" and the step is "I have 5 cucumbers",
44
+ the parameter value would be "5".
45
+
46
+ Provide your confidence in the match as a value between 0.0 (no confidence) and 1.0 (absolute certainty).
47
+ PROMPT
48
+ end
11
49
  end
12
50
  end
@@ -0,0 +1,261 @@
1
+ require 'digest'
2
+ require 'fileutils'
3
+ require 'json'
4
+ require 'botrytis/semantic_match_generator'
5
+
6
+ module Botrytis
7
+ class SemanticMatcher
8
+ def initialize
9
+ ensure_cache_directory if Botrytis.configuration.cache_enabled
10
+ end
11
+
12
+ def find_match(step_text, available_step_definitions)
13
+ patterns = available_step_definitions.map do |step_def|
14
+ {
15
+ pattern: step_def.regexp_source,
16
+ proc: step_def.proc,
17
+ step_def: step_def
18
+ }
19
+ end
20
+
21
+ # Filter out test verification steps for semantic matching
22
+ # These are steps that end with "should have executed" or similar test patterns
23
+ business_patterns = patterns.reject do |p|
24
+ pattern_text = p[:pattern].to_s
25
+ pattern_text.include?("should have executed") ||
26
+ pattern_text.include?("configured for testing") ||
27
+ pattern_text.include?("test") ||
28
+ pattern_text.include?("verification")
29
+ end
30
+
31
+ # Use business patterns for LLM matching, but keep all patterns for final matching
32
+ query_patterns = business_patterns.empty? ? patterns : business_patterns
33
+
34
+ if Botrytis.configuration.cache_enabled
35
+ cache_result = check_cache(step_text, patterns.map { |p| p[:pattern] })
36
+ return cache_result if cache_result
37
+ end
38
+
39
+ match_result = query_llm(step_text, query_patterns.map { |p| p[:pattern] })
40
+
41
+ save_to_cache(step_text, patterns.map { |p| p[:pattern] }, match_result) if Botrytis.configuration.cache_enabled
42
+
43
+ if match_result.match_found == "yes" && match_result.confidence.to_f >= Botrytis.configuration.confidence_threshold
44
+
45
+ # Handle different pattern formats from LLM response
46
+ # The LLM might return escaped quotes or slightly different formats
47
+ matching_pattern = patterns.find do |p|
48
+ original_pattern = p[:pattern]
49
+ llm_pattern = match_result.best_match_pattern
50
+
51
+ # Try exact match first
52
+ original_pattern == llm_pattern ||
53
+ # Try with/without surrounding slashes
54
+ original_pattern == "/#{llm_pattern}/" ||
55
+ original_pattern.gsub(/^\/|\/$/,'') == llm_pattern.gsub(/^\/|\/$/,'') ||
56
+ # Try with unescaped quotes (LLM might escape them)
57
+ original_pattern == llm_pattern.gsub(/\\\"/, '"') ||
58
+ original_pattern.gsub(/^\/|\/$/,'') == llm_pattern.gsub(/^\/|\/$/,'').gsub(/\\\"/, '"')
59
+ end
60
+
61
+ if matching_pattern
62
+ # Convert comma-separated parameter_values string to array
63
+ parameter_values = if match_result.parameter_values.nil? || match_result.parameter_values.empty?
64
+ []
65
+ else
66
+ match_result.parameter_values.split(',').map(&:strip)
67
+ end
68
+ return create_match_result(matching_pattern[:step_def], step_text, parameter_values)
69
+ end
70
+ end
71
+
72
+ nil
73
+ end
74
+
75
+ private
76
+
77
+ def query_llm(step_text, patterns)
78
+ generator = SemanticMatchGenerator.new(
79
+ step_text: step_text,
80
+ available_patterns: patterns
81
+ )
82
+
83
+ case Botrytis.configuration.llm_provider
84
+ when :openai
85
+ Sublayer.configuration.ai_provider = Sublayer::Providers::OpenAI
86
+ when :claude
87
+ Sublayer.configuration.ai_provider = Sublayer::Providers::Claude
88
+ when :gemini
89
+ Sublayer.configuration.ai_provider = Sublayer::Providers::Gemini
90
+ end
91
+
92
+ Sublayer.configuration.ai_model = Botrytis.configuration.model_name
93
+
94
+ begin
95
+ result = generator.generate
96
+ result
97
+ rescue => e
98
+ # LLM API Error occurred, falling back to no match
99
+ # Return a "no match" response if API fails
100
+ OpenStruct.new(
101
+ match_found: "no",
102
+ best_match_pattern: "",
103
+ confidence: "0.0",
104
+ parameter_values: ""
105
+ )
106
+ end
107
+ end
108
+
109
+ def ensure_cache_directory
110
+ FileUtils.mkdir_p(Botrytis.configuration.cache_directory) unless Dir.exist?(Botrytis.configuration.cache_directory)
111
+ end
112
+
113
+ def cache_key(step_text, patterns)
114
+ Digest::MD5.hexdigest("#{step_text}-#{patterns.sort.join('-')}")
115
+ end
116
+
117
+ def check_cache(step_text, patterns)
118
+ key = cache_key(step_text, patterns)
119
+ cache_file = File.join(Botrytis.configuration.cache_directory, "#{key}.json")
120
+
121
+ if File.exist?(cache_file)
122
+ data = JSON.parse(File.read(cache_file))
123
+ end
124
+
125
+ nil
126
+ end
127
+
128
+ def create_match_result(step_definition, step_text, parameter_values)
129
+ # Instead of trying to manually create step arguments, let's use Cucumber's
130
+ # normal matching mechanism by having the step definition actually match
131
+ # a constructed step text that would produce the right parameters
132
+
133
+ begin
134
+ # Try to construct a step text that the step definition would actually match
135
+ constructed_step_text = construct_matching_step_text_for_step_def(step_definition, parameter_values)
136
+
137
+ # Use the step definition's normal matching mechanism
138
+ if step_definition.respond_to?(:arguments_from)
139
+ # This is the normal way Cucumber creates step matches
140
+ step_arguments = step_definition.arguments_from(constructed_step_text)
141
+ return SemanticStepMatch.new(step_definition, step_text, step_arguments)
142
+ else
143
+ # Fallback to manual creation
144
+ step_arguments = create_original_step_arguments(step_definition, parameter_values)
145
+ return SemanticStepMatch.new(step_definition, step_text, step_arguments)
146
+ end
147
+ rescue => e
148
+ # Error in create_match_result, falling back
149
+ # Fallback to simple creation with empty arguments
150
+ return SemanticStepMatch.new(step_definition, step_text, [])
151
+ end
152
+ end
153
+
154
+ def create_proper_step_arguments(step_definition, step_text, parameter_values)
155
+ # Create step arguments that don't interfere with display formatting
156
+ # For semantic matching, we just need the parameter values to be passed to the step
157
+ # We don't need complex MatchData objects since Cucumber will handle display
158
+ return parameter_values || []
159
+ end
160
+
161
+ def construct_matching_step_text_for_step_def(step_definition, parameter_values)
162
+ # This creates a step text that would actually match the step definition's regex
163
+ # and produce the desired parameter values
164
+
165
+ if parameter_values.nil? || parameter_values.empty?
166
+ # For steps without parameters, just use the regexp source without anchors
167
+ if step_definition.respond_to?(:regexp_source)
168
+ source = step_definition.regexp_source.to_s
169
+ return source.gsub(/^\/\^/, '').gsub(/\$\/$/, '').gsub(/[\^$\/]/, '')
170
+ end
171
+ end
172
+
173
+ # For the button example: /^they click the "([^"]*)" button$/
174
+ # We want to produce: they click the "Buy Now" button
175
+ # So that when matched, it captures "Buy Now"
176
+
177
+ if step_definition.respond_to?(:expression) && step_definition.expression.is_a?(Regexp)
178
+ regex = step_definition.expression
179
+ elsif step_definition.respond_to?(:regexp_source)
180
+ source = step_definition.regexp_source.to_s.gsub(/^\/\^/, '').gsub(/\$\/$/, '')
181
+ regex = Regexp.new("^#{source}$")
182
+ else
183
+ # Fallback - return something simple
184
+ return parameter_values.join(' ')
185
+ end
186
+
187
+ # Better approach: build step text by understanding the regex structure
188
+ pattern = regex.source
189
+
190
+ # For simple cases like they click the "([^"]*)" button
191
+ # We want to replace ([^"]*) with the actual parameter value
192
+ if pattern.include?('"([^"]*)"') && parameter_values.length == 1
193
+ # Handle quoted parameter patterns specifically
194
+ result = pattern.gsub(/\([^)]+\)/, parameter_values[0])
195
+ else
196
+ # Fallback to general replacement
197
+ parameter_values.each do |value|
198
+ pattern = pattern.sub(/\([^)]+\)/, value)
199
+ end
200
+ result = pattern
201
+ end
202
+
203
+ # Clean up anchors and regex chars
204
+ result.gsub(/[\^$]/, '').gsub(/^\//, '').gsub(/\/$/, '')
205
+ end
206
+
207
+ def create_original_step_arguments(step_definition, parameter_values)
208
+ # For semantic matching, create simple argument objects that work with Cucumber
209
+ # This avoids the complex text construction that causes display issues
210
+ return [] if parameter_values.nil? || parameter_values.empty?
211
+
212
+ # Create simple argument objects that just hold the parameter values
213
+ # without trying to map them to text positions
214
+ parameter_values.map do |value|
215
+ # Create a minimal object that responds to the methods Cucumber expects
216
+ StepArgument.new(value)
217
+ end
218
+ end
219
+
220
+ # Minimal step argument class for semantic matching
221
+ class StepArgument
222
+ def initialize(value)
223
+ @value = value
224
+ end
225
+
226
+ def group(index = 0)
227
+ index == 0 ? @value : nil
228
+ end
229
+
230
+ def to_s
231
+ @value.to_s
232
+ end
233
+
234
+ def value
235
+ @value
236
+ end
237
+
238
+ def captures
239
+ [@value]
240
+ end
241
+ end
242
+
243
+ def construct_matching_step_text(regex, parameter_values)
244
+ # This is a simple approach: take the regex pattern and substitute
245
+ # capture groups with our parameter values
246
+ pattern = regex.source
247
+
248
+ # Replace capture groups like ([^"]*) with actual values
249
+ parameter_values.each_with_index do |value, index|
250
+ # Replace the first capture group with the parameter value
251
+ pattern = pattern.sub(/\([^)]+\)/, value)
252
+ end
253
+
254
+ # Clean up the pattern to make it a valid step text
255
+ pattern = pattern.gsub(/[\^$]/, '') # Remove anchors
256
+ pattern
257
+ end
258
+
259
+
260
+ end
261
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Botrytis
4
- VERSION = "0.1.0"
4
+ VERSION = "1.0.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: botrytis
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Scott Werner
@@ -59,13 +59,20 @@ executables: []
59
59
  extensions: []
60
60
  extra_rdoc_files: []
61
61
  files:
62
+ - ".claude/settings.local.json"
62
63
  - ".rspec"
64
+ - CLAUDE.md
63
65
  - LICENSE
64
66
  - README.md
65
67
  - Rakefile
68
+ - debug_step_args.rb
69
+ - future_tests.md
66
70
  - lib/botrytis.rb
67
71
  - lib/botrytis/configuration.rb
72
+ - lib/botrytis/cucumber.rb
73
+ - lib/botrytis/formatter.rb
68
74
  - lib/botrytis/semantic_match_generator.rb
75
+ - lib/botrytis/semantic_matcher.rb
69
76
  - lib/botrytis/version.rb
70
77
  - sig/botrytis.rbs
71
78
  homepage: https://github.com/sublayerapp/botrytis