htm 0.0.30 → 0.0.32

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 (161) hide show
  1. checksums.yaml +4 -4
  2. data/.irbrc +2 -3
  3. data/.rubocop.yml +184 -0
  4. data/CHANGELOG.md +46 -0
  5. data/README.md +2 -0
  6. data/Rakefile +93 -12
  7. data/db/migrate/00008_create_node_relationships.rb +54 -0
  8. data/db/migrate/00009_fix_node_relationships_column_types.rb +17 -0
  9. data/db/schema.sql +124 -1
  10. data/docs/api/database.md +35 -57
  11. data/docs/api/embedding-service.md +1 -1
  12. data/docs/api/index.md +26 -15
  13. data/docs/api/working-memory.md +8 -8
  14. data/docs/architecture/index.md +5 -7
  15. data/docs/architecture/overview.md +5 -8
  16. data/docs/assets/images/htm-architecture-overview.svg +1 -1
  17. data/docs/assets/images/htm-context-assembly-flow.svg +2 -2
  18. data/docs/assets/images/htm-layered-architecture.svg +3 -3
  19. data/docs/assets/images/two-tier-memory-architecture.svg +1 -1
  20. data/docs/database/README.md +1 -0
  21. data/docs/database_rake_tasks.md +20 -28
  22. data/docs/development/contributing.md +5 -5
  23. data/docs/development/index.md +4 -7
  24. data/docs/development/schema.md +71 -1
  25. data/docs/development/setup.md +40 -82
  26. data/docs/development/testing.md +1 -1
  27. data/docs/examples/file-loading.md +4 -4
  28. data/docs/examples/mcp-client.md +1 -1
  29. data/docs/getting-started/quick-start.md +4 -4
  30. data/docs/guides/adding-memories.md +14 -1
  31. data/docs/guides/configuration.md +5 -5
  32. data/docs/guides/context-assembly.md +4 -4
  33. data/docs/guides/file-loading.md +12 -12
  34. data/docs/guides/getting-started.md +2 -2
  35. data/docs/guides/long-term-memory.md +7 -27
  36. data/docs/guides/propositions.md +20 -19
  37. data/docs/guides/recalling-memories.md +5 -5
  38. data/docs/guides/tags.md +18 -13
  39. data/docs/multi_framework_support.md +1 -1
  40. data/docs/robots/hive-mind.md +1 -1
  41. data/docs/robots/multi-robot.md +2 -2
  42. data/docs/robots/robot-groups.md +1 -1
  43. data/docs/robots/two-tier-memory.md +72 -94
  44. data/docs/setup_local_database.md +8 -54
  45. data/docs/using_rake_tasks_in_your_app.md +6 -6
  46. data/examples/01_basic_usage.rb +1 -0
  47. data/examples/03_custom_llm_configuration.rb +1 -0
  48. data/examples/04_file_loader_usage.rb +1 -0
  49. data/examples/05_timeframe_demo.rb +1 -0
  50. data/examples/06_example_app/app.rb +1 -0
  51. data/examples/07_cli_app/htm_cli.rb +1 -0
  52. data/examples/09_mcp_client.rb +1 -0
  53. data/examples/10_telemetry/demo.rb +1 -0
  54. data/examples/11_robot_groups/multi_process.rb +1 -0
  55. data/examples/11_robot_groups/same_process.rb +1 -0
  56. data/examples/12_rails_app/.envrc +12 -0
  57. data/examples/12_rails_app/Gemfile +8 -3
  58. data/examples/12_rails_app/Gemfile.lock +94 -89
  59. data/examples/12_rails_app/README.md +70 -19
  60. data/examples/12_rails_app/app/controllers/application_controller.rb +6 -0
  61. data/examples/12_rails_app/app/controllers/chats_controller.rb +305 -0
  62. data/examples/12_rails_app/app/controllers/dashboard_controller.rb +3 -0
  63. data/examples/12_rails_app/app/controllers/files_controller.rb +17 -2
  64. data/examples/12_rails_app/app/controllers/home_controller.rb +8 -0
  65. data/examples/12_rails_app/app/controllers/memories_controller.rb +9 -4
  66. data/examples/12_rails_app/app/controllers/messages_controller.rb +214 -0
  67. data/examples/12_rails_app/app/controllers/robots_controller.rb +11 -1
  68. data/examples/12_rails_app/app/controllers/tags_controller.rb +14 -1
  69. data/examples/12_rails_app/app/javascript/application.js +1 -1
  70. data/examples/12_rails_app/app/models/application_record.rb +5 -0
  71. data/examples/12_rails_app/app/models/chat.rb +36 -0
  72. data/examples/12_rails_app/app/models/message.rb +5 -0
  73. data/examples/12_rails_app/app/models/model.rb +5 -0
  74. data/examples/12_rails_app/app/models/tool_call.rb +5 -0
  75. data/examples/12_rails_app/app/views/chats/index.html.erb +61 -0
  76. data/examples/12_rails_app/app/views/chats/show.html.erb +213 -0
  77. data/examples/12_rails_app/app/views/dashboard/index.html.erb +3 -0
  78. data/examples/12_rails_app/app/views/files/index.html.erb +10 -5
  79. data/examples/12_rails_app/app/views/files/new.html.erb +4 -2
  80. data/examples/12_rails_app/app/views/files/show.html.erb +19 -3
  81. data/examples/12_rails_app/app/views/home/index.html.erb +45 -0
  82. data/examples/12_rails_app/app/views/layouts/application.html.erb +20 -18
  83. data/examples/12_rails_app/app/views/memories/_memory_card.html.erb +1 -1
  84. data/examples/12_rails_app/app/views/memories/deleted.html.erb +3 -1
  85. data/examples/12_rails_app/app/views/memories/edit.html.erb +2 -0
  86. data/examples/12_rails_app/app/views/memories/index.html.erb +2 -0
  87. data/examples/12_rails_app/app/views/memories/new.html.erb +2 -0
  88. data/examples/12_rails_app/app/views/memories/show.html.erb +4 -2
  89. data/examples/12_rails_app/app/views/messages/_message.html.erb +20 -0
  90. data/examples/12_rails_app/app/views/robots/index.html.erb +2 -0
  91. data/examples/12_rails_app/app/views/robots/new.html.erb +2 -0
  92. data/examples/12_rails_app/app/views/robots/show.html.erb +2 -0
  93. data/examples/12_rails_app/app/views/search/index.html.erb +59 -8
  94. data/examples/12_rails_app/app/views/shared/_navbar.html.erb +75 -29
  95. data/examples/12_rails_app/app/views/tags/index.html.erb +2 -0
  96. data/examples/12_rails_app/app/views/tags/show.html.erb +3 -1
  97. data/examples/12_rails_app/config/application.rb +1 -1
  98. data/examples/12_rails_app/config/database.yml +9 -5
  99. data/examples/12_rails_app/config/importmap.rb +1 -1
  100. data/examples/12_rails_app/config/initializers/htm.rb +9 -2
  101. data/examples/12_rails_app/config/initializers/ruby_llm.rb +33 -0
  102. data/examples/12_rails_app/config/routes.rb +39 -23
  103. data/examples/12_rails_app/db/migrate/20250124000001_create_ruby_llm_tables.rb +34 -0
  104. data/examples/12_rails_app/db/migrate/20250124000002_create_models_table.rb +28 -0
  105. data/examples/12_rails_app/db/schema.rb +67 -0
  106. data/examples/examples_helper.rb +25 -0
  107. data/lib/htm/circuit_breaker.rb +5 -6
  108. data/lib/htm/config/builder.rb +12 -12
  109. data/lib/htm/config/database.rb +21 -27
  110. data/lib/htm/config/defaults.yml +25 -13
  111. data/lib/htm/config/validator.rb +12 -18
  112. data/lib/htm/config.rb +93 -173
  113. data/lib/htm/database.rb +193 -199
  114. data/lib/htm/embedding_service.rb +4 -9
  115. data/lib/htm/integrations/sinatra.rb +7 -7
  116. data/lib/htm/job_adapter.rb +14 -21
  117. data/lib/htm/jobs/generate_embedding_job.rb +28 -44
  118. data/lib/htm/jobs/generate_propositions_job.rb +29 -55
  119. data/lib/htm/jobs/generate_relationships_job.rb +137 -0
  120. data/lib/htm/jobs/generate_tags_job.rb +45 -67
  121. data/lib/htm/loaders/markdown_loader.rb +65 -112
  122. data/lib/htm/long_term_memory/fulltext_search.rb +1 -1
  123. data/lib/htm/long_term_memory/hybrid_search.rb +300 -128
  124. data/lib/htm/long_term_memory/node_operations.rb +2 -2
  125. data/lib/htm/long_term_memory/relevance_scorer.rb +100 -68
  126. data/lib/htm/long_term_memory/tag_operations.rb +87 -120
  127. data/lib/htm/long_term_memory/vector_search.rb +1 -1
  128. data/lib/htm/long_term_memory.rb +2 -1
  129. data/lib/htm/mcp/cli.rb +59 -58
  130. data/lib/htm/mcp/server.rb +5 -6
  131. data/lib/htm/mcp/tools.rb +30 -36
  132. data/lib/htm/migration.rb +10 -10
  133. data/lib/htm/models/node.rb +2 -3
  134. data/lib/htm/models/node_relationship.rb +72 -0
  135. data/lib/htm/models/node_tag.rb +2 -2
  136. data/lib/htm/models/robot_node.rb +2 -2
  137. data/lib/htm/models/tag.rb +41 -28
  138. data/lib/htm/observability.rb +45 -51
  139. data/lib/htm/proposition_service.rb +3 -7
  140. data/lib/htm/query_cache.rb +13 -15
  141. data/lib/htm/railtie.rb +1 -2
  142. data/lib/htm/robot_group.rb +9 -9
  143. data/lib/htm/sequel_config.rb +1 -0
  144. data/lib/htm/sql_builder.rb +1 -1
  145. data/lib/htm/tag_service.rb +2 -6
  146. data/lib/htm/timeframe.rb +4 -5
  147. data/lib/htm/timeframe_extractor.rb +42 -83
  148. data/lib/htm/version.rb +1 -1
  149. data/lib/htm/workflows/remember_workflow.rb +112 -115
  150. data/lib/htm/working_memory.rb +21 -26
  151. data/lib/htm.rb +103 -116
  152. data/lib/tasks/db.rake +0 -2
  153. data/lib/tasks/doc.rake +14 -13
  154. data/lib/tasks/files.rake +5 -12
  155. data/lib/tasks/htm.rake +70 -71
  156. data/lib/tasks/jobs.rake +41 -47
  157. data/lib/tasks/tags.rake +3 -8
  158. metadata +28 -106
  159. data/lib/htm/config/section.rb +0 -74
  160. data/lib/htm/loaders/defaults_loader.rb +0 -166
  161. data/lib/htm/loaders/xdg_config_loader.rb +0 -116
@@ -72,6 +72,31 @@ end
72
72
 
73
73
  # Module with helper methods for examples
74
74
  module ExamplesHelper
75
+ # Reset examples database if --reset flag is present in ARGV
76
+ #
77
+ # Drops and recreates htm_examples, then runs all migrations.
78
+ # Removes --reset from ARGV so downstream option parsers are unaffected.
79
+ #
80
+ # @return [void]
81
+ def self.reset_if_requested!
82
+ return unless ARGV.delete('--reset')
83
+
84
+ db_name = "#{ENV['HTM_SERVICE__NAME'] || 'htm'}_examples"
85
+ puts "Resetting examples database '#{db_name}'..."
86
+
87
+ HTM::SequelConfig.disconnect! if HTM::SequelConfig.db
88
+
89
+ abort "ERROR: Failed to drop '#{db_name}'" unless system("dropdb --if-exists #{db_name}")
90
+ abort "ERROR: Failed to create '#{db_name}'" unless system("createdb #{db_name}")
91
+
92
+ HTM::SequelConfig.establish_connection!(load_models: false)
93
+ HTM::Database.setup
94
+ HTM::SequelConfig.ensure_models_loaded!
95
+
96
+ puts "Database reset complete."
97
+ puts
98
+ end
99
+
75
100
  # Check if database is available and configured
76
101
  #
77
102
  # @return [Boolean] true if database is ready
@@ -158,7 +158,7 @@ class HTM
158
158
  end
159
159
  when :closed
160
160
  # Reset failure count on success in closed state
161
- @failure_count = 0 if @failure_count > 0
161
+ @failure_count = 0 if @failure_count.positive?
162
162
  end
163
163
  end
164
164
  end
@@ -190,11 +190,10 @@ class HTM
190
190
  return unless @state == :open && @last_failure_time
191
191
 
192
192
  elapsed = Time.now - @last_failure_time
193
- if elapsed >= @reset_timeout
194
- @state = :half_open
195
- @success_count = 0
196
- HTM.logger.info "CircuitBreaker[#{@name}]: Reset timeout elapsed (#{@reset_timeout}s), circuit HALF-OPEN"
197
- end
193
+ return unless elapsed >= @reset_timeout
194
+ @state = :half_open
195
+ @success_count = 0
196
+ HTM.logger.info "CircuitBreaker[#{@name}]: Reset timeout elapsed (#{@reset_timeout}s), circuit HALF-OPEN"
198
197
  end
199
198
  end
200
199
  end
@@ -31,7 +31,7 @@ class HTM
31
31
  response = RubyLLM.embed(text, model: model)
32
32
  embedding = extract_embedding_from_response(response)
33
33
 
34
- unless embedding.is_a?(Array) && embedding.all? { |v| v.is_a?(Numeric) }
34
+ unless embedding.is_a?(Array) && embedding.all?(Numeric)
35
35
  raise HTM::EmbeddingError, "Invalid embedding response format from #{embedding_provider}"
36
36
  end
37
37
 
@@ -119,17 +119,17 @@ class HTM
119
119
 
120
120
  def parse_tag_response(text)
121
121
  tags = text.to_s.split("\n").map(&:strip).reject(&:empty?)
122
- valid_tags = tags.select { |tag| tag =~ /^[a-z0-9\-]+(:[a-z0-9\-]+)*$/ }
122
+ valid_tags = tags.grep(/^[a-z0-9-]+(:[a-z0-9-]+)*$/)
123
123
  valid_tags.select { |tag| tag.count(':') < max_tag_depth }
124
124
  end
125
125
 
126
126
  def parse_proposition_response(text)
127
127
  text.to_s
128
- .split("\n")
129
- .map(&:strip)
130
- .map { |line| line.sub(/^[-*]\s*/, '') }
131
- .map(&:strip)
132
- .reject(&:empty?)
128
+ .split("\n")
129
+ .map(&:strip)
130
+ .map { |line| line.sub(/^[-*]\s*/, '') }
131
+ .map(&:strip)
132
+ .reject(&:empty?)
133
133
  end
134
134
 
135
135
  # ==========================================================================
@@ -138,11 +138,11 @@ class HTM
138
138
 
139
139
  def build_tag_extraction_prompt(text, existing_ontology)
140
140
  taxonomy_context = if existing_ontology.any?
141
- sample_tags = existing_ontology.sample([existing_ontology.size, 20].min)
142
- tag.taxonomy_context_existing % { sample_tags: sample_tags.join(', ') }
143
- else
144
- tag.taxonomy_context_empty
145
- end
141
+ sample_tags = existing_ontology.sample([existing_ontology.size, 20].min)
142
+ tag.taxonomy_context_existing % { sample_tags: sample_tags.join(', ') }
143
+ else
144
+ tag.taxonomy_context_empty
145
+ end
146
146
 
147
147
  tag.user_prompt_template % {
148
148
  text: text,
@@ -87,9 +87,9 @@ class HTM
87
87
 
88
88
  unless database_configured?
89
89
  raise HTM::ConfigurationError,
90
- "No database configured for environment '#{environment}'. " \
91
- "Set HTM_DATABASE__URL or HTM_DATABASE__NAME, " \
92
- "or add database.name to the '#{environment}:' section in your config."
90
+ "No database configured for environment '#{environment}'. " \
91
+ "Set HTM_DATABASE__URL or HTM_DATABASE__NAME, " \
92
+ "or add database.name to the '#{environment}:' section in your config."
93
93
  end
94
94
 
95
95
  true
@@ -165,16 +165,16 @@ class HTM
165
165
  return true if actual == expected
166
166
 
167
167
  raise HTM::ConfigurationError,
168
- "Database name '#{actual}' does not match expected '#{expected}'.\n" \
169
- "Database names must follow the convention: {service_name}_{environment}\n" \
170
- " Service name: #{service_name}\n" \
171
- " Environment: #{environment}\n" \
172
- " Expected: #{expected}\n" \
173
- " Actual: #{actual}\n\n" \
174
- "Either:\n" \
175
- " - Set HTM_DATABASE__URL to point to '#{expected}'\n" \
176
- " - Set HTM_DATABASE__NAME=#{expected}\n" \
177
- " - Change HTM_ENV to match the database suffix"
168
+ "Database name '#{actual}' does not match expected '#{expected}'.\n" \
169
+ "Database names must follow the convention: {service_name}_{environment}\n " \
170
+ "Service name: #{service_name}\n " \
171
+ "Environment: #{environment}\n " \
172
+ "Expected: #{expected}\n " \
173
+ "Actual: #{actual}\n\n" \
174
+ "Either:\n " \
175
+ "- Set HTM_DATABASE__URL to point to '#{expected}'\n " \
176
+ "- Set HTM_DATABASE__NAME=#{expected}\n " \
177
+ "- Change HTM_ENV to match the database suffix"
178
178
  end
179
179
 
180
180
  # Check if the database name matches the expected convention
@@ -216,23 +216,17 @@ class HTM
216
216
 
217
217
  def build_database_url
218
218
  return nil unless database.name && !database.name.empty?
219
-
220
- auth = if database.user && !database.user.empty?
221
- database.password && !database.password.empty? ? "#{database.user}:#{database.password}@" : "#{database.user}@"
222
- else
223
- ''
224
- end
225
-
226
- url = "postgresql://#{auth}#{database.host}:#{database.port}/#{database.name}"
227
-
228
- # Add sslmode as query parameter if set
229
- if database.sslmode && !database.sslmode.empty?
230
- url += "?sslmode=#{database.sslmode}"
231
- end
232
-
219
+ url = "postgresql://#{database_auth_segment}#{database.host}:#{database.port}/#{database.name}"
220
+ url += "?sslmode=#{database.sslmode}" if database.sslmode && !database.sslmode.empty?
233
221
  url
234
222
  end
235
223
 
224
+ def database_auth_segment
225
+ return '' unless database.user && !database.user.empty?
226
+ return "#{database.user}@" unless database.password && !database.password.empty?
227
+ "#{database.user}:#{database.password}@"
228
+ end
229
+
236
230
  # ==========================================================================
237
231
  # Database Configuration Reconciliation
238
232
  # ==========================================================================
@@ -49,7 +49,7 @@ defaults:
49
49
  # Access: HTM.config.embedding.provider, HTM.config.embedding.model, etc.
50
50
  # ---------------------------------------------------------------------------
51
51
  embedding:
52
- provider: ollama
52
+ provider: :ollama
53
53
  model: nomic-embed-text:latest
54
54
  dimensions: 768
55
55
  timeout: 120
@@ -65,7 +65,7 @@ defaults:
65
65
  # %{taxonomy_context} - existing taxonomy info or new taxonomy message
66
66
  # ---------------------------------------------------------------------------
67
67
  tag:
68
- provider: ollama
68
+ provider: :ollama
69
69
  model: gemma3:latest
70
70
  timeout: 180
71
71
  max_depth: 4
@@ -110,7 +110,7 @@ defaults:
110
110
  # %{text} - the content to extract propositions from
111
111
  # ---------------------------------------------------------------------------
112
112
  proposition:
113
- provider: ollama
113
+ provider: :ollama
114
114
  model: gemma3:latest
115
115
  timeout: 180
116
116
  enabled: true
@@ -164,11 +164,12 @@ defaults:
164
164
 
165
165
  # ---------------------------------------------------------------------------
166
166
  # Chunking Configuration (for file loading)
167
- # Access: HTM.config.chunking.size, HTM.config.chunking.overlap
167
+ # Access: HTM.config.chunking.chunk_size, HTM.config.chunking.chunk_overlap
168
+ # Note: Using chunk_size/chunk_overlap to avoid collision with Enumerable#size
168
169
  # ---------------------------------------------------------------------------
169
170
  chunking:
170
- size: 1024
171
- overlap: 64
171
+ chunk_size: 1024
172
+ chunk_overlap: 64
172
173
 
173
174
  # ---------------------------------------------------------------------------
174
175
  # Circuit Breaker Configuration
@@ -195,16 +196,16 @@ defaults:
195
196
  # Access: HTM.config.job.backend
196
197
  # ---------------------------------------------------------------------------
197
198
  job:
198
- backend: fiber
199
+ backend: :fiber
199
200
 
200
201
  # ---------------------------------------------------------------------------
201
202
  # General Settings
202
203
  # Access: HTM.config.week_start, HTM.config.connection_timeout, etc.
203
204
  # ---------------------------------------------------------------------------
204
- week_start: sunday
205
+ week_start: :sunday
205
206
  connection_timeout: 60
206
207
  telemetry_enabled: false
207
- log_level: info
208
+ log_level: :info
208
209
 
209
210
  # ---------------------------------------------------------------------------
210
211
  # Provider Credentials
@@ -250,7 +251,7 @@ defaults:
250
251
  development:
251
252
  database:
252
253
  name: htm_development
253
- log_level: debug
254
+ log_level: :debug
254
255
 
255
256
  # =============================================================================
256
257
  # Test Environment Overrides
@@ -259,8 +260,8 @@ test:
259
260
  database:
260
261
  name: htm_test
261
262
  job:
262
- backend: inline
263
- log_level: warn
263
+ backend: :inline
264
+ log_level: :warn
264
265
  telemetry_enabled: false
265
266
 
266
267
  # =============================================================================
@@ -270,5 +271,16 @@ production:
270
271
  database:
271
272
  pool_size: 25
272
273
  sslmode: require
273
- log_level: warn
274
+ log_level: :warn
274
275
  telemetry_enabled: true
276
+
277
+ # =============================================================================
278
+ # Examples Environment Overrides (for running example scripts)
279
+ # =============================================================================
280
+ examples:
281
+ database:
282
+ name: htm_examples
283
+ job:
284
+ backend: :inline
285
+ log_level: :info
286
+ telemetry_enabled: false
@@ -27,32 +27,28 @@ class HTM
27
27
  def validate_provider(name, value)
28
28
  return if value.nil?
29
29
 
30
- unless SUPPORTED_PROVIDERS.include?(value)
31
- raise_validation_error("#{name} must be one of: #{SUPPORTED_PROVIDERS.join(', ')} (got #{value.inspect})")
32
- end
30
+ return if SUPPORTED_PROVIDERS.include?(value)
31
+ raise_validation_error("#{name} must be one of: #{SUPPORTED_PROVIDERS.join(', ')} (got #{value.inspect})")
33
32
  end
34
33
 
35
34
  def validate_job_backend
36
35
  return unless job_backend
37
36
 
38
- unless SUPPORTED_JOB_BACKENDS.include?(job_backend)
39
- raise_validation_error("job.backend must be one of: #{SUPPORTED_JOB_BACKENDS.join(', ')} (got #{job_backend.inspect})")
40
- end
37
+ return if SUPPORTED_JOB_BACKENDS.include?(job_backend)
38
+ raise_validation_error("job.backend must be one of: #{SUPPORTED_JOB_BACKENDS.join(', ')} (got #{job_backend.inspect})")
41
39
  end
42
40
 
43
41
  def validate_week_start
44
- unless SUPPORTED_WEEK_STARTS.include?(week_start)
45
- raise_validation_error("week_start must be one of: #{SUPPORTED_WEEK_STARTS.join(', ')} (got #{week_start.inspect})")
46
- end
42
+ return if SUPPORTED_WEEK_STARTS.include?(week_start)
43
+ raise_validation_error("week_start must be one of: #{SUPPORTED_WEEK_STARTS.join(', ')} (got #{week_start.inspect})")
47
44
  end
48
45
 
49
46
  def validate_relevance_weights
50
47
  total = relevance_semantic_weight + relevance_tag_weight +
51
48
  relevance_recency_weight + relevance_access_weight
52
49
 
53
- unless (0.99..1.01).cover?(total)
54
- raise_validation_error("relevance weights must sum to 1.0 (got #{total})")
55
- end
50
+ return if (0.99..1.01).cover?(total)
51
+ raise_validation_error("relevance weights must sum to 1.0 (got #{total})")
56
52
  end
57
53
 
58
54
  def validate_callables
@@ -68,15 +64,13 @@ class HTM
68
64
  raise HTM::ValidationError, "proposition_extractor must be callable"
69
65
  end
70
66
 
71
- unless @token_counter.respond_to?(:call)
72
- raise HTM::ValidationError, "token_counter must be callable"
73
- end
67
+ return if @token_counter.respond_to?(:call)
68
+ raise HTM::ValidationError, "token_counter must be callable"
74
69
  end
75
70
 
76
71
  def validate_logger
77
- unless @logger.respond_to?(:info) && @logger.respond_to?(:warn) && @logger.respond_to?(:error)
78
- raise HTM::ValidationError, "logger must respond to :info, :warn, and :error"
79
- end
72
+ return if @logger.respond_to?(:info) && @logger.respond_to?(:warn) && @logger.respond_to?(:error)
73
+ raise HTM::ValidationError, "logger must respond to :info, :warn, and :error"
80
74
  end
81
75
  end
82
76
  end