prospector_engine 0.1.1 → 0.2.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: 46798902589ee83ac3a63b7cf36fcb67b61d4a84fbf2ac07819b04a778439099
4
- data.tar.gz: 87cb71c8d3c832156306184d31f9a7d3e8f45343e33bceb266ae3cc095a61130
3
+ metadata.gz: f6ff4fed78e9e8c81ec50623a7e2cbe0076b0f890f3c3fec3561551585de0513
4
+ data.tar.gz: 584620ecbee56ffdf94520638a5b7ab409e13c9fc264dd934a44d8e798bf4275
5
5
  SHA512:
6
- metadata.gz: 68982b0d84db8931d2e691f365d0032a5bfbff4f0f9ea40fcafcabeab8e0783699bb8c6e202d2529c083fa639e22ac77aae961978ddb7b0663d4414f9f255868
7
- data.tar.gz: 1876729cef3a17d3a09a717de8ef392c4e67a611b6d164d36b0b985e37dda846f16d3814273dabbfe551a01adae5d9b6999f577b88ce82a389bc102ba57d46de
6
+ metadata.gz: 7bbc93fdaca17a89d0ba229fbd23bba95eef79cd723b288ec872162eab5801bc9bc4fa0f93506245b1e7a796ded93523c0d237d61d5463c60c603644cb7450e6
7
+ data.tar.gz: ac7828b1928c11db81a56079fa6b65442a8b85404a214d147283e02a69c4f9109b3fed4568da47548e1e9b758dc415206339ef3e19abeecde10d4e9588d7ec09
data/README.md CHANGED
@@ -1,20 +1,21 @@
1
1
  # Prospector
2
2
 
3
- A Rails engine for discovering businesses from multiple sources with AI-powered keyword generation and classification.
3
+ A Rails engine for discovering businesses from multiple sources with AI-powered keyword generation, contact enrichment, and classification.
4
4
 
5
- Prospector handles the full discovery pipeline: generate search keywords with AI, fetch business listings from external APIs, classify results for domain relevance, and present an admin review interface -- all inside your existing Rails app.
5
+ Prospector handles the full discovery pipeline: generate search keywords with AI, fetch business listings from external APIs, scrape websites for contact info, classify results for domain relevance, and present an admin review interface -- all inside your existing Rails app.
6
6
 
7
7
  ## Features
8
8
 
9
- - **Multi-source adapters** -- Ships with Google Places. Pluggable interface for adding Yelp, Bing, OpenStreetMap, or custom sources.
9
+ - **Multi-source adapters** -- Ships with Google Places. Pluggable interface for adding Yelp, Bing, or custom sources.
10
+ - **Contact enrichment** -- Automatically scrapes candidate websites for email and social links (Facebook, Instagram, LinkedIn, TikTok, YouTube). SSRF-protected with IP validation on every redirect hop.
10
11
  - **AI keyword generation** -- On-demand LLM-powered keyword generation for any business domain. Keywords are stored and reused across runs.
11
12
  - **AI classification** -- Automatically classifies discovered businesses for domain relevance. Non-relevant results are auto-rejected.
12
- - **Flexible geography** -- Search by metro area, city, coordinates + radius, ZIP code, or bounding box.
13
- - **Admin UI** -- Mountable admin interface with self-contained CSS. Review candidates, approve/reject, bulk approve, trigger reclassification.
13
+ - **Flexible geography** -- Search by metro area, city, coordinates + radius, ZIP code, or bounding box. 42 preloaded US metro areas available as presets.
14
+ - **Admin UI** -- Mountable admin interface with self-contained dark theme. Review candidates, approve/reject, bulk approve, trigger reclassification.
14
15
  - **Keyword management** -- Admin UI for viewing, adding, toggling, and AI-generating search keywords per category.
15
16
  - **Background jobs** -- Fetch, classify, and bulk approve jobs integrate with your existing queue (Solid Queue, Sidekiq, etc.).
16
- - **Turbo Streams** -- Real-time updates when Turbo is present. Gracefully degrades without it.
17
- - **Domain-agnostic** -- Works for motorcycle services, aviation operators, restaurants, or any business vertical.
17
+ - **Turbo Streams** -- Real-time progress updates when Turbo is present. Gracefully degrades without it.
18
+ - **Instrumentation** -- ActiveSupport::Notifications events for run status changes and candidate approvals.
18
19
 
19
20
  ## Requirements
20
21
 
@@ -51,11 +52,20 @@ Edit `config/initializers/prospector.rb`:
51
52
 
52
53
  ```ruby
53
54
  Prospector.configure do |config|
54
- # Required: domain slug for keyword generation and classification context
55
+ # Required: domain slug for keyword generation and classification context.
55
56
  config.domain = "motorcycle_services"
56
57
 
57
- # Required: called when an admin approves a candidate.
58
- # Receives a Prospector::Candidate instance.
58
+ # Required: admin authentication.
59
+ config.authenticate_admin_with do |controller|
60
+ controller.current_user&.admin?
61
+ end
62
+
63
+ # Classifier class (must be set before classification runs).
64
+ # Can be a class constant or a string for lazy resolution.
65
+ config.classifier = MotorcycleClassifier
66
+ # config.classifier = "MotorcycleClassifier"
67
+
68
+ # Called when an admin approves a candidate (optional).
59
69
  # Runs AFTER the candidate status is committed (safe to enqueue jobs).
60
70
  config.on_approve do |candidate|
61
71
  data = candidate.normalized_data
@@ -67,26 +77,16 @@ Prospector.configure do |config|
67
77
  zip_code: data["zip_code"],
68
78
  phone_number: data["phone_number"],
69
79
  website: data["website"],
80
+ email: candidate.email,
70
81
  latitude: data["latitude"],
71
82
  longitude: data["longitude"],
72
83
  service_types: candidate.llm_categories,
73
- import_source: "prospector"
84
+ facebook_url: candidate.facebook_url,
85
+ instagram_url: candidate.instagram_url
74
86
  )
75
87
  end
76
88
 
77
- # Required: admin authentication.
78
- # Receives the controller instance.
79
- config.authenticate_admin_with do |controller|
80
- controller.current_user&.admin?
81
- end
82
-
83
- # Required: classifier class (inherits LlmClassifier::Classifier).
84
- # Defines categories, model, and classification rules for your domain.
85
- # See "Defining a Classifier" below.
86
- config.classifier = MotorcycleClassifier
87
-
88
89
  # Optional: check for duplicates before creating candidates.
89
- # Return true to skip the candidate.
90
90
  config.duplicate_check do |source_uid:, name:, **|
91
91
  Business.exists?(["import_metadata->>'place_id' = ?", source_uid])
92
92
  end
@@ -94,14 +94,17 @@ Prospector.configure do |config|
94
94
  # Optional: default source adapter (default: :google_places)
95
95
  config.default_source = :google_places
96
96
 
97
- # Optional: default AI model (passed to classifier when not overridden)
98
- config.default_classifier_model = "anthropic:claude-sonnet-4-20250514"
97
+ # Optional: model override for classification and keyword generation (a bare ruby_llm model id).
98
+ # Defaults to nil: classification uses the classifier's `model`, keyword generation uses RubyLLM's default.
99
+ # config.default_classifier_model = "claude-opus-4-6"
99
100
 
100
101
  # Optional: job queue name (default: :default)
101
102
  config.queue_name = :prospector
102
103
  end
103
104
  ```
104
105
 
106
+ **Note:** `validate!` only runs in production. In development, missing `domain` or `authenticate_admin_with` will cause runtime errors rather than startup failures.
107
+
105
108
  Set the required environment variables:
106
109
 
107
110
  ```bash
@@ -116,20 +119,21 @@ ANTHROPIC_API_KEY=your_anthropic_api_key # or whatever ruby_llm needs
116
119
  Visit `/prospector` in your browser. The admin interface provides:
117
120
 
118
121
  - **Runs** -- Create, monitor, retry, restart, and cancel discovery runs
119
- - **Candidates** -- Review discovered businesses, approve or reject individually or in bulk
122
+ - **Candidates** -- Review discovered businesses with contact info, approve or reject individually or in bulk
120
123
  - **Keywords** -- View, add, toggle, and AI-generate search keywords per category
121
124
 
122
125
  ### Creating a Run
123
126
 
124
127
  1. Click "New Run" in the admin UI
125
128
  2. Select a geography type (metro area, city, coordinates, ZIP code, or bounding box)
126
- 3. Fill in the geography details
127
- 4. Click "Start Run"
129
+ 3. For metro areas, optionally select from 42 preloaded US metros or enter a custom one
130
+ 4. Fill in the geography details
131
+ 5. Click "Start Run"
128
132
 
129
133
  The run progresses through these stages automatically:
130
134
 
131
135
  ```
132
- pending -> running (fetching from source) -> classifying (AI classification) -> completed
136
+ pending -> running (fetch + enrich) -> classifying (AI) -> completed
133
137
  ```
134
138
 
135
139
  ### Defining a Classifier
@@ -143,9 +147,8 @@ class MotorcycleClassifier < LlmClassifier::Classifier
143
147
  :detailing, :training, :towing, :insurance, :touring,
144
148
  :clubs, :parts
145
149
 
146
- model "anthropic:claude-sonnet-4-20250514"
150
+ model "claude-opus-4-6"
147
151
  multi_label true
148
- require_categories true # fail when no categories match (auto-reject)
149
152
 
150
153
  system_prompt <<~PROMPT
151
154
  You are classifying businesses for the motorcycle services domain.
@@ -164,13 +167,11 @@ class MotorcycleClassifier < LlmClassifier::Classifier
164
167
  end
165
168
  ```
166
169
 
167
- Then set it in your Prospector config:
170
+ The classifier receives a hash with `name`, `address`, `website`, `description`, and `source_types` (comma-joined string) keys.
168
171
 
169
- ```ruby
170
- config.classifier = MotorcycleClassifier
171
- ```
172
+ Use a bare model id that ruby_llm recognizes (`claude-opus-4-6`, not `anthropic:claude-opus-4-6`) and that supports structured outputs, since llm_classifier 0.3 sends every request with a JSON schema.
172
173
 
173
- The classifier receives a hash with `name`, `address`, `website`, `description`, and `source_types` keys. It returns an `LlmClassifier::Result` with `.categories`, `.confidence`, and `.reasoning`.
174
+ Candidates the classifier assigns no categories are auto-rejected with reason `no_relevant_categories`. A failed classification (API error, unknown model, unparseable response) is recorded in `metadata["llm_error"]`, and the candidate stays pending so the next reclassification retries it. Leave `require_categories` off: with it on, llm_classifier reports "no categories" as a failure, so irrelevant businesses are counted as errors instead of being rejected.
174
175
 
175
176
  ### Programmatic Usage
176
177
 
@@ -195,13 +196,19 @@ Prospector::Keyword.create!(
195
196
  # Approve a candidate programmatically
196
197
  candidate = Prospector::Candidate.find(id)
197
198
  candidate.approve! # fires the on_approve callback
199
+
200
+ # Access enriched contact info
201
+ candidate.email # => "info@example.com"
202
+ candidate.facebook_url # => "https://facebook.com/example"
203
+ candidate.instagram_url # => "https://instagram.com/example"
198
204
  ```
199
205
 
200
206
  ### Geography Types
201
207
 
202
208
  ```ruby
203
- # Metro area (text search)
204
- Prospector::Geography::MetroArea.new(name: "San Francisco", primary_state: "CA")
209
+ # Metro area (text search) -- 42 US metros available as presets
210
+ Prospector::Geography::MetroArea.new(name: "San Francisco Bay Area", primary_state: "CA")
211
+ Prospector::Geography::MetroArea::PRELOADED # => [{name: "Atlanta", primary_state: "GA"}, ...]
205
212
 
206
213
  # City (text search)
207
214
  Prospector::Geography::City.new(city: "Austin", state: "TX")
@@ -226,8 +233,6 @@ class MyApp::YelpAdapter < Prospector::Sources::Base
226
233
  def self.adapter_key = "yelp"
227
234
 
228
235
  def fetch(geography:, keywords:)
229
- # Call the Yelp API for each keyword + geography combination.
230
- # Return an Array of Prospector::Sources::Result.
231
236
  keywords.flat_map do |keyword|
232
237
  search_yelp(keyword, geography).map do |biz|
233
238
  Prospector::Sources::Result.new(
@@ -260,8 +265,6 @@ Prospector.configure do |config|
260
265
  end
261
266
  ```
262
267
 
263
- Then select "Yelp" as the source when creating a run.
264
-
265
268
  ## Database Tables
266
269
 
267
270
  Prospector creates four tables, all prefixed with `prospector_`:
@@ -269,7 +272,7 @@ Prospector creates four tables, all prefixed with `prospector_`:
269
272
  | Table | Purpose |
270
273
  |-------|---------|
271
274
  | `prospector_runs` | Discovery runs with status, geography, and progress counters |
272
- | `prospector_candidates` | Discovered businesses pending review |
275
+ | `prospector_candidates` | Discovered businesses with contact info (email, social links) pending review |
273
276
  | `prospector_classification_runs` | Tracks AI reclassification operations |
274
277
  | `prospector_keywords` | Stored search keywords per domain and category |
275
278
 
@@ -284,33 +287,52 @@ Prospector::Run (state machine)
284
287
  | |-- Pipeline::Orchestrator
285
288
  | | |-- Pipeline::Normalizer (address parsing)
286
289
  | | |-- Duplicate checking (via config.duplicate_check)
287
- | | '-- Creates Prospector::Candidate records
290
+ | | |-- Creates Prospector::Candidate records
291
+ | | '-- Enrichment::ContactScraper (email + social links)
288
292
  | '-- Enqueues ClassifyJob
289
293
  |
290
294
  |-- ClassifyJob
291
295
  | '-- Classification::Runner
292
- | |-- Calls LLM via ruby_llm
296
+ | |-- Calls LLM via llm_classifier
293
297
  | |-- Stores categories, confidence, reasoning in metadata
294
- | '-- Auto-rejects candidates with no relevant categories
298
+ | |-- Auto-rejects candidates with no relevant categories
299
+ | '-- Records failed classifications as llm_error, leaves them pending
295
300
  |
296
301
  '-- Admin UI
297
- |-- Review candidates (approve / reject / restore)
298
- |-- Bulk approve
299
- '-- Trigger reclassification with different models
302
+ |-- Runs: create, monitor, retry, restart, cancel
303
+ |-- Candidates: review, approve, reject, restore, bulk approve
304
+ |-- Keywords: view, add, toggle active, AI-generate
305
+ '-- Reclassification with model override
300
306
  ```
301
307
 
302
308
  ## Run States
303
309
 
304
310
  ```
305
311
  pending --> running --> classifying --> completed
306
- | |
307
- v v
308
- failed cancelled
309
- ^ ^
310
- |__________________________|
311
- (retry / restart)
312
+ | | |
313
+ v v v
314
+ failed cancelled
315
+ ^ ^
316
+ |____________|
317
+ (retry/restart)
312
318
  ```
313
319
 
320
+ - `cancellable?` -- pending, running, or classifying
321
+ - `retryable?` -- failed only
322
+ - `restartable?` -- completed, failed, or cancelled
323
+ - `restart!` destroys pending/rejected candidates and resets all counters
324
+ - `reset_for_retry!` resets status and error fields only
325
+
326
+ ## Instrumentation
327
+
328
+ Prospector emits ActiveSupport::Notifications events:
329
+
330
+ | Event | Payload |
331
+ |-------|---------|
332
+ | `prospector.run.status_changed` | `run`, `status`, `previous_status` |
333
+ | `prospector.candidate.approved` | `candidate`, `record` |
334
+ | `prospector.candidate.rejected` | `candidate`, `reason` |
335
+
314
336
  ## Development
315
337
 
316
338
  The gem ships with a devcontainer for containerized development:
@@ -320,10 +342,10 @@ The gem ships with a devcontainer for containerized development:
320
342
  docker compose -f .devcontainer/compose.yaml up -d --build
321
343
 
322
344
  # Run tests (inside the container)
323
- docker exec -w /workspaces/prospector prospector-app-1 bundle exec rake test
345
+ docker exec -w /workspaces/prospector_engine prospector_engine-app-1 bundle exec rake test
324
346
 
325
347
  # Run a single test file
326
- docker exec -w /workspaces/prospector prospector-app-1 bundle exec ruby -Itest test/models/run_test.rb
348
+ docker exec -w /workspaces/prospector_engine prospector_engine-app-1 bundle exec ruby -Itest test/models/run_test.rb
327
349
  ```
328
350
 
329
351
  The test suite uses a dummy Rails app (`test/dummy/`) with PostgreSQL.
@@ -12,7 +12,7 @@ module Prospector
12
12
 
13
13
  def build_classification_run(run)
14
14
  run.classification_runs.new(
15
- ai_model: params[:ai_model] || Prospector.config.default_classifier_model,
15
+ ai_model: params[:ai_model].presence || Prospector.config.resolved_classifier_model,
16
16
  include_approved: params[:include_approved] == "1",
17
17
  actor_id: respond_to?(:current_user) ? current_user&.id : nil
18
18
  )
@@ -24,8 +24,9 @@ Prospector.configure do |config|
24
24
  # Optional: default source adapter (default: :google_places)
25
25
  # config.default_source = :google_places
26
26
 
27
- # Optional: default AI model for classification (passed to classifier)
28
- # config.default_classifier_model = "anthropic:claude-sonnet-4-20250514"
27
+ # Optional: model override for classification and keyword generation (a bare ruby_llm model id)
28
+ # Defaults to nil: classification uses the classifier's `model`, keyword generation uses RubyLLM's default
29
+ # config.default_classifier_model = "claude-opus-4-6"
29
30
 
30
31
  # Optional: duplicate check hook
31
32
  # config.duplicate_check do |source_uid:, name:, **|
@@ -39,7 +39,7 @@ Key methods: `to_query_string` (for text search), `to_coordinate_restriction` (f
39
39
 
40
40
  ## Classification (`classification/`)
41
41
 
42
- - `Runner` - Iterates unprocessed candidates, calls LLM via ruby_llm, stores results in candidate metadata. Auto-rejects candidates with empty categories.
42
+ - `Runner` - Iterates unprocessed candidates, calls the configured `LlmClassifier::Classifier`, stores results in candidate metadata. Auto-rejects candidates with empty categories. A failed `Result` (or raised error) is stored as `llm_error` and counted as an error, never a rejection; the candidate stays `ai_unprocessed` so it is retried.
43
43
 
44
44
  ## Keywords (`keywords/`)
45
45
 
@@ -47,6 +47,6 @@ Key methods: `to_query_string` (for text search), `to_coordinate_restriction` (f
47
47
 
48
48
  ## Other Files
49
49
 
50
- - `configuration.rb` - `Prospector.configure` DSL. Required keys: domain, on_approve, authenticate_admin_with.
50
+ - `configuration.rb` - `Prospector.configure` DSL. Required keys: domain, on_approve, authenticate_admin_with. `default_classifier_model` defaults to nil so the classifier's own `model` applies; `resolved_classifier_model` reports the model that will actually run.
51
51
  - `engine.rb` - Rails::Engine with isolate_namespace.
52
52
  - `error.rb` - Error hierarchy: Error, ConfigurationError, AdapterError, MissingApiKeyError, ClassificationError.
@@ -34,25 +34,24 @@ module Prospector
34
34
 
35
35
  def classify_candidate(candidate)
36
36
  result = invoke_classifier(candidate)
37
+ return record_error(candidate, result.error) if result.failure?
38
+
37
39
  store_result(candidate, result)
38
40
  auto_reject_if_empty(candidate, result)
39
41
  @processed += 1
40
42
  rescue => e
41
- candidate.update!(metadata: candidate.metadata.merge("llm_error" => e.message))
42
- @errors += 1
43
+ record_error(candidate, e.message)
43
44
  end
44
45
 
45
46
  def invoke_classifier(candidate)
46
- classifier = resolve_classifier
47
+ classifier = Prospector.config.classifier_class
47
48
  input = build_input(candidate)
48
49
  classifier.classify(input, model: @model)
49
50
  end
50
51
 
51
- def resolve_classifier
52
- raw = Prospector.config.classifier
53
- raise ConfigurationError, "Prospector.config.classifier must be set" unless raw
54
-
55
- raw.is_a?(String) ? raw.constantize : raw
52
+ def record_error(candidate, message)
53
+ candidate.update!(metadata: candidate.metadata.merge("llm_error" => message))
54
+ @errors += 1
56
55
  end
57
56
 
58
57
  def build_input(candidate)
@@ -66,7 +65,7 @@ module Prospector
66
65
  end
67
66
 
68
67
  def store_result(candidate, result)
69
- candidate.update!(metadata: candidate.metadata.merge(
68
+ candidate.update!(metadata: candidate.metadata.except("llm_error").merge(
70
69
  "llm_categories" => result.categories,
71
70
  "llm_confidence" => result.confidence,
72
71
  "llm_reasoning" => result.reasoning,
@@ -12,7 +12,7 @@ module Prospector
12
12
  def initialize
13
13
  @domain = nil
14
14
  @default_source = :google_places
15
- @default_classifier_model = "anthropic:claude-sonnet-4-20250514"
15
+ @default_classifier_model = nil
16
16
  @classifier = nil
17
17
  @turbo_enabled = defined?(Turbo::Broadcastable)
18
18
  @queue_name = :default
@@ -48,6 +48,18 @@ module Prospector
48
48
  adapter.is_a?(String) ? adapter.constantize : adapter
49
49
  end
50
50
 
51
+ def classifier_class
52
+ raise ConfigurationError, "Prospector.config.classifier must be set" unless classifier
53
+
54
+ classifier.is_a?(String) ? classifier.constantize : classifier
55
+ end
56
+
57
+ def resolved_classifier_model
58
+ require "ruby_llm"
59
+
60
+ default_classifier_model || classifier_class.model || RubyLLM.config.default_model
61
+ end
62
+
51
63
  def validate!
52
64
  raise ConfigurationError, "Prospector.config.domain must be set" if domain.blank?
53
65
  raise ConfigurationError, "Prospector.config.authenticate_admin_with must be set" unless @authenticate_admin_block
@@ -2,8 +2,16 @@ module Prospector
2
2
  class Engine < ::Rails::Engine
3
3
  isolate_namespace Prospector
4
4
 
5
- config.autoload_paths << File.expand_path("..", __dir__)
6
- config.eager_load_paths << File.expand_path("..", __dir__)
5
+ initializer "prospector.autoload", before: :set_autoload_paths do |app|
6
+ lib_path = root.join("lib")
7
+ app.config.autoload_paths << lib_path
8
+ app.config.eager_load_paths << lib_path
9
+
10
+ Rails.autoloaders.main.ignore(
11
+ root.join("lib/prospector/version.rb"),
12
+ root.join("lib/generators")
13
+ )
14
+ end
7
15
 
8
16
  initializer "prospector.configuration" do
9
17
  Prospector.config.validate! if Rails.env.production?
@@ -30,6 +30,7 @@ module Prospector
30
30
 
31
31
  chat = RubyLLM.chat(model: Prospector.config.default_classifier_model)
32
32
  response = chat.ask(prompt_for(category))
33
+ @generated_by = chat.model.id
33
34
  parse_keywords(response.content)
34
35
  end
35
36
 
@@ -63,7 +64,7 @@ module Prospector
63
64
  ) do |record|
64
65
  record.source = "llm"
65
66
  record.metadata = {
66
- "generated_by" => Prospector.config.default_classifier_model,
67
+ "generated_by" => @generated_by,
67
68
  "generated_at" => Time.current.iso8601
68
69
  }
69
70
  end
@@ -1,3 +1,3 @@
1
1
  module Prospector
2
- VERSION = "0.1.1"
2
+ VERSION = "0.2.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: prospector_engine
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - AxiumFoundry
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-04-06 00:00:00.000000000 Z
11
+ date: 2026-09-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -42,16 +42,16 @@ dependencies:
42
42
  name: llm_classifier
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
- - - ">="
45
+ - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: 0.2.0
47
+ version: 0.3.0
48
48
  type: :runtime
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
- - - ">="
52
+ - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: 0.2.0
54
+ version: 0.3.0
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: minitest
57
57
  requirement: !ruby/object:Gem::Requirement
@@ -178,7 +178,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
178
178
  - !ruby/object:Gem::Version
179
179
  version: '0'
180
180
  requirements: []
181
- rubygems_version: 3.4.20
181
+ rubygems_version: 3.5.22
182
182
  signing_key:
183
183
  specification_version: 4
184
184
  summary: Business discovery from multiple sources with AI classification