htm 0.0.18 → 0.0.30

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 (216) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +119 -1
  3. data/README.md +12 -0
  4. data/Rakefile +104 -18
  5. data/db/migrate/00001_enable_extensions.rb +9 -5
  6. data/db/migrate/00002_create_robots.rb +18 -6
  7. data/db/migrate/00003_create_file_sources.rb +30 -17
  8. data/db/migrate/00004_create_nodes.rb +60 -48
  9. data/db/migrate/00005_create_tags.rb +24 -12
  10. data/db/migrate/00006_create_node_tags.rb +28 -13
  11. data/db/migrate/00007_create_robot_nodes.rb +40 -26
  12. data/db/schema.sql +17 -1
  13. data/db/seeds.rb +34 -34
  14. data/docs/api/embedding-service.md +140 -110
  15. data/docs/api/yard/HTM/ActiveRecordConfig.md +6 -0
  16. data/docs/api/yard/HTM/Config.md +173 -0
  17. data/docs/api/yard/HTM/ConfigSection.md +28 -0
  18. data/docs/api/yard/HTM/Database.md +1 -1
  19. data/docs/api/yard/HTM/Railtie.md +2 -2
  20. data/docs/api/yard/HTM.md +0 -57
  21. data/docs/api/yard/index.csv +76 -61
  22. data/docs/api/yard-reference.md +2 -1
  23. data/docs/architecture/adrs/003-ollama-embeddings.md +45 -36
  24. data/docs/architecture/adrs/004-hive-mind.md +1 -1
  25. data/docs/architecture/adrs/008-robot-identification.md +1 -1
  26. data/docs/architecture/index.md +11 -9
  27. data/docs/architecture/overview.md +11 -7
  28. data/docs/assets/images/balanced-strategy-decay.svg +41 -0
  29. data/docs/assets/images/class-hierarchy.svg +1 -1
  30. data/docs/assets/images/eviction-priority.svg +43 -0
  31. data/docs/assets/images/exception-hierarchy.svg +2 -2
  32. data/docs/assets/images/hive-mind-shared-memory.svg +52 -0
  33. data/docs/assets/images/htm-architecture-overview.svg +3 -3
  34. data/docs/assets/images/htm-core-components.svg +4 -4
  35. data/docs/assets/images/htm-layered-architecture.svg +1 -1
  36. data/docs/assets/images/htm-memory-addition-flow.svg +2 -2
  37. data/docs/assets/images/htm-memory-recall-flow.svg +2 -2
  38. data/docs/assets/images/memory-topology.svg +53 -0
  39. data/docs/assets/images/two-tier-memory-architecture.svg +55 -0
  40. data/docs/database/naming-convention.md +244 -0
  41. data/docs/database_rake_tasks.md +31 -0
  42. data/docs/development/rake-tasks.md +80 -35
  43. data/docs/development/setup.md +76 -44
  44. data/docs/examples/basic-usage.md +133 -0
  45. data/docs/examples/config-files.md +170 -0
  46. data/docs/examples/file-loading.md +208 -0
  47. data/docs/examples/index.md +116 -0
  48. data/docs/examples/llm-configuration.md +168 -0
  49. data/docs/examples/mcp-client.md +172 -0
  50. data/docs/examples/rails-integration.md +173 -0
  51. data/docs/examples/robot-groups.md +210 -0
  52. data/docs/examples/sinatra-integration.md +218 -0
  53. data/docs/examples/standalone-app.md +216 -0
  54. data/docs/examples/telemetry.md +224 -0
  55. data/docs/examples/timeframes.md +143 -0
  56. data/docs/getting-started/installation.md +97 -40
  57. data/docs/getting-started/quick-start.md +28 -11
  58. data/docs/guides/configuration.md +515 -0
  59. data/docs/guides/file-loading.md +322 -0
  60. data/docs/guides/getting-started.md +40 -9
  61. data/docs/guides/index.md +3 -3
  62. data/docs/guides/mcp-server.md +100 -13
  63. data/docs/guides/propositions.md +264 -0
  64. data/docs/guides/recalling-memories.md +4 -4
  65. data/docs/guides/search-strategies.md +3 -3
  66. data/docs/guides/tags.md +318 -0
  67. data/docs/guides/telemetry.md +229 -0
  68. data/docs/index.md +8 -16
  69. data/docs/{architecture → robots}/hive-mind.md +8 -111
  70. data/docs/robots/index.md +73 -0
  71. data/docs/{guides → robots}/multi-robot.md +3 -3
  72. data/docs/{guides → robots}/robot-groups.md +8 -7
  73. data/docs/{architecture → robots}/two-tier-memory.md +13 -149
  74. data/docs/robots/why-robots.md +85 -0
  75. data/examples/.envrc +6 -0
  76. data/examples/.gitignore +2 -0
  77. data/examples/00_create_examples_db.rb +94 -0
  78. data/examples/{basic_usage.rb → 01_basic_usage.rb} +12 -16
  79. data/examples/{custom_llm_configuration.rb → 03_custom_llm_configuration.rb} +13 -3
  80. data/examples/{file_loader_usage.rb → 04_file_loader_usage.rb} +11 -14
  81. data/examples/{timeframe_demo.rb → 05_timeframe_demo.rb} +10 -3
  82. data/examples/{example_app → 06_example_app}/app.rb +15 -15
  83. data/examples/{cli_app → 07_cli_app}/htm_cli.rb +15 -22
  84. data/examples/08_sinatra_app/Gemfile.lock +241 -0
  85. data/examples/{sinatra_app → 08_sinatra_app}/app.rb +19 -18
  86. data/examples/{mcp_client.rb → 09_mcp_client.rb} +5 -8
  87. data/examples/{telemetry → 10_telemetry}/SETUP_README.md +1 -1
  88. data/examples/{telemetry → 10_telemetry}/demo.rb +14 -10
  89. data/examples/11_robot_groups/README.md +335 -0
  90. data/examples/{robot_groups → 11_robot_groups/lib}/robot_worker.rb +17 -3
  91. data/examples/{robot_groups → 11_robot_groups}/multi_process.rb +9 -9
  92. data/examples/{robot_groups → 11_robot_groups}/same_process.rb +9 -12
  93. data/examples/{rails_app → 12_rails_app}/Gemfile +3 -0
  94. data/examples/{rails_app → 12_rails_app}/Gemfile.lock +87 -58
  95. data/examples/{rails_app → 12_rails_app}/app/controllers/dashboard_controller.rb +10 -6
  96. data/examples/{rails_app → 12_rails_app}/app/controllers/files_controller.rb +5 -5
  97. data/examples/{rails_app → 12_rails_app}/app/controllers/memories_controller.rb +11 -7
  98. data/examples/{rails_app → 12_rails_app}/app/controllers/robots_controller.rb +8 -8
  99. data/examples/12_rails_app/app/controllers/tags_controller.rb +36 -0
  100. data/examples/{rails_app → 12_rails_app}/app/views/dashboard/index.html.erb +2 -2
  101. data/examples/{rails_app → 12_rails_app}/app/views/files/new.html.erb +5 -2
  102. data/examples/{rails_app → 12_rails_app}/app/views/memories/_memory_card.html.erb +3 -3
  103. data/examples/{rails_app → 12_rails_app}/app/views/memories/deleted.html.erb +3 -3
  104. data/examples/{rails_app → 12_rails_app}/app/views/memories/edit.html.erb +3 -3
  105. data/examples/{rails_app → 12_rails_app}/app/views/memories/show.html.erb +4 -4
  106. data/examples/{rails_app → 12_rails_app}/app/views/robots/index.html.erb +2 -2
  107. data/examples/{rails_app → 12_rails_app}/app/views/robots/show.html.erb +4 -4
  108. data/examples/{rails_app → 12_rails_app}/app/views/search/index.html.erb +1 -1
  109. data/examples/{rails_app → 12_rails_app}/app/views/tags/index.html.erb +2 -2
  110. data/examples/{rails_app → 12_rails_app}/app/views/tags/show.html.erb +1 -1
  111. data/examples/12_rails_app/config/initializers/htm.rb +7 -0
  112. data/examples/12_rails_app/config/initializers/rack.rb +5 -0
  113. data/examples/README.md +230 -211
  114. data/examples/examples_helper.rb +138 -0
  115. data/lib/htm/config/builder.rb +167 -0
  116. data/lib/htm/config/database.rb +317 -0
  117. data/lib/htm/config/defaults.yml +41 -13
  118. data/lib/htm/config/section.rb +74 -0
  119. data/lib/htm/config/validator.rb +83 -0
  120. data/lib/htm/config.rb +65 -361
  121. data/lib/htm/database.rb +85 -127
  122. data/lib/htm/errors.rb +14 -0
  123. data/lib/htm/integrations/sinatra.rb +13 -44
  124. data/lib/htm/job_adapter.rb +75 -1
  125. data/lib/htm/jobs/generate_embedding_job.rb +3 -4
  126. data/lib/htm/jobs/generate_propositions_job.rb +4 -5
  127. data/lib/htm/jobs/generate_tags_job.rb +16 -15
  128. data/lib/htm/loaders/defaults_loader.rb +23 -0
  129. data/lib/htm/loaders/markdown_loader.rb +17 -15
  130. data/lib/htm/loaders/xdg_config_loader.rb +9 -9
  131. data/lib/htm/long_term_memory/fulltext_search.rb +14 -14
  132. data/lib/htm/long_term_memory/hybrid_search.rb +396 -229
  133. data/lib/htm/long_term_memory/node_operations.rb +24 -23
  134. data/lib/htm/long_term_memory/relevance_scorer.rb +23 -20
  135. data/lib/htm/long_term_memory/robot_operations.rb +4 -4
  136. data/lib/htm/long_term_memory/tag_operations.rb +91 -77
  137. data/lib/htm/long_term_memory/vector_search.rb +4 -5
  138. data/lib/htm/long_term_memory.rb +13 -13
  139. data/lib/htm/mcp/cli.rb +115 -8
  140. data/lib/htm/mcp/resources.rb +4 -3
  141. data/lib/htm/mcp/server.rb +5 -4
  142. data/lib/htm/mcp/tools.rb +37 -28
  143. data/lib/htm/migration.rb +72 -0
  144. data/lib/htm/models/file_source.rb +52 -31
  145. data/lib/htm/models/node.rb +224 -108
  146. data/lib/htm/models/node_tag.rb +49 -28
  147. data/lib/htm/models/robot.rb +38 -27
  148. data/lib/htm/models/robot_node.rb +63 -35
  149. data/lib/htm/models/tag.rb +126 -123
  150. data/lib/htm/observability.rb +45 -41
  151. data/lib/htm/proposition_service.rb +76 -7
  152. data/lib/htm/railtie.rb +2 -2
  153. data/lib/htm/robot_group.rb +30 -18
  154. data/lib/htm/sequel_config.rb +215 -0
  155. data/lib/htm/sql_builder.rb +14 -16
  156. data/lib/htm/tag_service.rb +78 -0
  157. data/lib/htm/tasks.rb +3 -0
  158. data/lib/htm/version.rb +1 -1
  159. data/lib/htm/workflows/remember_workflow.rb +213 -0
  160. data/lib/htm.rb +27 -22
  161. data/lib/tasks/db.rake +0 -2
  162. data/lib/tasks/doc.rake +2 -2
  163. data/lib/tasks/files.rake +11 -18
  164. data/lib/tasks/htm.rake +190 -62
  165. data/lib/tasks/jobs.rake +179 -54
  166. data/lib/tasks/tags.rake +8 -13
  167. data/mkdocs.yml +33 -8
  168. data/scripts/backfill_parent_tags.rb +376 -0
  169. data/scripts/normalize_plural_tags.rb +335 -0
  170. metadata +168 -86
  171. data/docs/api/yard/HTM/Configuration.md +0 -240
  172. data/docs/telemetry.md +0 -391
  173. data/examples/rails_app/app/controllers/tags_controller.rb +0 -30
  174. data/examples/sinatra_app/Gemfile.lock +0 -166
  175. data/lib/htm/active_record_config.rb +0 -104
  176. /data/examples/{config_file_example → 02_config_file_example}/README.md +0 -0
  177. /data/examples/{config_file_example → 02_config_file_example}/config/htm.local.yml +0 -0
  178. /data/examples/{config_file_example → 02_config_file_example}/custom_config.yml +0 -0
  179. /data/examples/{config_file_example → 02_config_file_example}/show_config.rb +0 -0
  180. /data/examples/{example_app → 06_example_app}/Rakefile +0 -0
  181. /data/examples/{cli_app → 07_cli_app}/README.md +0 -0
  182. /data/examples/{sinatra_app → 08_sinatra_app}/Gemfile +0 -0
  183. /data/examples/{telemetry → 10_telemetry}/README.md +0 -0
  184. /data/examples/{telemetry → 10_telemetry}/grafana/dashboards/htm-metrics.json +0 -0
  185. /data/examples/{rails_app → 12_rails_app}/.gitignore +0 -0
  186. /data/examples/{rails_app → 12_rails_app}/Procfile.dev +0 -0
  187. /data/examples/{rails_app → 12_rails_app}/README.md +0 -0
  188. /data/examples/{rails_app → 12_rails_app}/Rakefile +0 -0
  189. /data/examples/{rails_app → 12_rails_app}/app/assets/stylesheets/application.css +0 -0
  190. /data/examples/{rails_app → 12_rails_app}/app/assets/stylesheets/inter-font.css +0 -0
  191. /data/examples/{rails_app → 12_rails_app}/app/controllers/application_controller.rb +0 -0
  192. /data/examples/{rails_app → 12_rails_app}/app/controllers/search_controller.rb +0 -0
  193. /data/examples/{rails_app → 12_rails_app}/app/javascript/application.js +0 -0
  194. /data/examples/{rails_app → 12_rails_app}/app/javascript/controllers/application.js +0 -0
  195. /data/examples/{rails_app → 12_rails_app}/app/javascript/controllers/index.js +0 -0
  196. /data/examples/{rails_app → 12_rails_app}/app/views/files/index.html.erb +0 -0
  197. /data/examples/{rails_app → 12_rails_app}/app/views/files/show.html.erb +0 -0
  198. /data/examples/{rails_app → 12_rails_app}/app/views/layouts/application.html.erb +0 -0
  199. /data/examples/{rails_app → 12_rails_app}/app/views/memories/index.html.erb +0 -0
  200. /data/examples/{rails_app → 12_rails_app}/app/views/memories/new.html.erb +0 -0
  201. /data/examples/{rails_app → 12_rails_app}/app/views/robots/new.html.erb +0 -0
  202. /data/examples/{rails_app → 12_rails_app}/app/views/shared/_navbar.html.erb +0 -0
  203. /data/examples/{rails_app → 12_rails_app}/app/views/shared/_stat_card.html.erb +0 -0
  204. /data/examples/{rails_app → 12_rails_app}/bin/dev +0 -0
  205. /data/examples/{rails_app → 12_rails_app}/bin/rails +0 -0
  206. /data/examples/{rails_app → 12_rails_app}/bin/rake +0 -0
  207. /data/examples/{rails_app → 12_rails_app}/config/application.rb +0 -0
  208. /data/examples/{rails_app → 12_rails_app}/config/boot.rb +0 -0
  209. /data/examples/{rails_app → 12_rails_app}/config/database.yml +0 -0
  210. /data/examples/{rails_app → 12_rails_app}/config/environment.rb +0 -0
  211. /data/examples/{rails_app → 12_rails_app}/config/importmap.rb +0 -0
  212. /data/examples/{rails_app → 12_rails_app}/config/routes.rb +0 -0
  213. /data/examples/{rails_app → 12_rails_app}/config/tailwind.config.js +0 -0
  214. /data/examples/{rails_app → 12_rails_app}/config.ru +0 -0
  215. /data/examples/{rails_app → 12_rails_app}/log/.keep +0 -0
  216. /data/examples/{rails_app → 12_rails_app}/tmp/local_secret.txt +0 -0
data/lib/htm/database.rb CHANGED
@@ -6,7 +6,7 @@ require 'set'
6
6
 
7
7
  class HTM
8
8
  # Database setup and configuration for HTM
9
- # Handles schema creation and database initialization
9
+ # Handles schema creation and database initialization using Sequel
10
10
  class Database
11
11
  class << self
12
12
  # Set up the HTM database schema
@@ -17,19 +17,22 @@ class HTM
17
17
  # @return [void]
18
18
  #
19
19
  def setup(db_url = nil, run_migrations: true, dump_schema: false)
20
- require 'active_record'
21
- require_relative 'active_record_config'
20
+ require 'sequel'
21
+ require_relative 'sequel_config'
22
22
 
23
- # Establish ActiveRecord connection
24
- HTM::ActiveRecordConfig.establish_connection!
23
+ # Establish Sequel connection (don't load models yet - tables may not exist)
24
+ HTM::SequelConfig.establish_connection!(load_models: false)
25
25
 
26
- # Run migrations using ActiveRecord
26
+ # Run migrations using Sequel
27
27
  if run_migrations
28
- puts "Running ActiveRecord migrations..."
29
- run_activerecord_migrations
28
+ puts "Running Sequel migrations..."
29
+ run_sequel_migrations
30
30
  end
31
31
 
32
- puts "✓ HTM database schema created successfully"
32
+ # Now that tables exist, load models
33
+ HTM::SequelConfig.ensure_models_loaded!
34
+
35
+ puts "HTM database schema created successfully"
33
36
 
34
37
  # Optionally dump schema
35
38
  if dump_schema
@@ -44,15 +47,18 @@ class HTM
44
47
  # @return [void]
45
48
  #
46
49
  def migrate(db_url = nil)
47
- require 'active_record'
48
- require_relative 'active_record_config'
50
+ require 'sequel'
51
+ require_relative 'sequel_config'
52
+
53
+ # Establish Sequel connection (don't load models - tables may not exist)
54
+ HTM::SequelConfig.establish_connection!(load_models: false)
49
55
 
50
- # Establish ActiveRecord connection
51
- HTM::ActiveRecordConfig.establish_connection!
56
+ run_sequel_migrations
52
57
 
53
- run_activerecord_migrations
58
+ # Load models now that tables exist
59
+ HTM::SequelConfig.ensure_models_loaded!
54
60
 
55
- puts "Database migrations completed"
61
+ puts "Database migrations completed"
56
62
  end
57
63
 
58
64
  # Show migration status
@@ -61,26 +67,26 @@ class HTM
61
67
  # @return [void]
62
68
  #
63
69
  def migration_status(db_url = nil)
64
- require 'active_record'
65
- require_relative 'active_record_config'
70
+ require 'sequel'
71
+ require_relative 'sequel_config'
66
72
 
67
- # Establish ActiveRecord connection
68
- HTM::ActiveRecordConfig.establish_connection!
73
+ # Establish Sequel connection (don't load models - we just need the DB)
74
+ HTM::SequelConfig.establish_connection!(load_models: false)
69
75
 
70
76
  migrations_path = File.expand_path('../../db/migrate', __dir__)
71
77
 
72
78
  # Get available migrations from files
73
79
  available_migrations = Dir.glob(File.join(migrations_path, '*.rb')).map do |file|
74
80
  {
75
- version: File.basename(file).split('_').first,
81
+ version: File.basename(file).split('_').first.to_i,
76
82
  name: File.basename(file, '.rb')
77
83
  }
78
84
  end.sort_by { |m| m[:version] }
79
85
 
80
86
  # Get applied migrations from database
81
87
  applied_versions = begin
82
- ActiveRecord::Base.connection.select_values('SELECT version FROM schema_migrations ORDER BY version')
83
- rescue ActiveRecord::StatementInvalid
88
+ HTM.db[:schema_migrations].select_map(:version).map(&:to_i)
89
+ rescue Sequel::DatabaseError
84
90
  []
85
91
  end
86
92
 
@@ -92,7 +98,7 @@ class HTM
92
98
  else
93
99
  available_migrations.each do |migration|
94
100
  status = applied_versions.include?(migration[:version])
95
- status_mark = status ? "" : ""
101
+ status_mark = status ? "[x]" : "[ ]"
96
102
 
97
103
  puts "#{status_mark} #{migration[:name]}"
98
104
  end
@@ -125,38 +131,36 @@ class HTM
125
131
  tables.each do |table|
126
132
  begin
127
133
  conn.exec("DROP TABLE IF EXISTS #{table} CASCADE")
128
- puts " Dropped #{table}"
134
+ puts " Dropped #{table}"
129
135
  rescue PG::Error => e
130
- puts " Error dropping #{table}: #{e.message}"
136
+ puts " Error dropping #{table}: #{e.message}"
131
137
  end
132
138
  end
133
139
 
134
140
  # Drop functions and triggers
135
141
  begin
136
142
  conn.exec("DROP FUNCTION IF EXISTS extract_ontology_topics() CASCADE")
137
- puts " Dropped ontology functions and triggers"
143
+ puts " Dropped ontology functions and triggers"
138
144
  rescue PG::Error => e
139
- puts " Error dropping functions: #{e.message}"
145
+ puts " Error dropping functions: #{e.message}"
140
146
  end
141
147
 
142
148
  # Drop views
143
149
  begin
144
150
  conn.exec("DROP VIEW IF EXISTS ontology_structure CASCADE")
145
151
  conn.exec("DROP VIEW IF EXISTS topic_relationships CASCADE")
146
- puts " Dropped ontology views"
152
+ puts " Dropped ontology views"
147
153
  rescue PG::Error => e
148
- puts " Error dropping views: #{e.message}"
154
+ puts " Error dropping views: #{e.message}"
149
155
  end
150
156
 
151
157
  conn.close
152
- puts "All HTM tables dropped"
158
+ puts "All HTM tables dropped"
153
159
  end
154
160
 
155
161
  # Seed database with sample data
156
162
  #
157
163
  # Loads and executes db/seeds.rb file following Rails conventions.
158
- # All seeding logic is contained in db/seeds.rb and reads data
159
- # from markdown files in db/seed_data/ directory.
160
164
  #
161
165
  # @param db_url [String] Database connection URL (uses ENV['HTM_DATABASE__URL'] if not provided)
162
166
  # @return [void]
@@ -165,7 +169,7 @@ class HTM
165
169
  seeds_file = File.expand_path('../../db/seeds.rb', __dir__)
166
170
 
167
171
  unless File.exist?(seeds_file)
168
- puts "Error: Seeds file not found at #{seeds_file}"
172
+ puts "Error: Seeds file not found at #{seeds_file}"
169
173
  puts " Please create db/seeds.rb with your seeding logic"
170
174
  exit 1
171
175
  end
@@ -189,12 +193,6 @@ class HTM
189
193
 
190
194
  puts "Dumping schema to #{schema_file}..."
191
195
 
192
- # Build pg_dump command
193
- # --schema-only: only dump schema, not data
194
- # --no-owner: don't set ownership
195
- # --no-privileges: don't dump access privileges
196
- # --no-tablespaces: don't dump tablespace assignments
197
- # --exclude-schema=_timescaledb_*: exclude TimescaleDB internal schemas
198
196
  env = {
199
197
  'PGPASSWORD' => config[:password]
200
198
  }
@@ -214,23 +212,19 @@ class HTM
214
212
  '-d', config[:dbname]
215
213
  ]
216
214
 
217
- # Execute pg_dump and capture output
218
215
  require 'open3'
219
216
  stdout, stderr, status = Open3.capture3(env, *cmd)
220
217
 
221
218
  unless status.success?
222
- puts "Error dumping schema:"
219
+ puts "Error dumping schema:"
223
220
  puts stderr
224
221
  exit 1
225
222
  end
226
223
 
227
- # Clean up the output
228
224
  cleaned_schema = clean_schema_dump(stdout)
229
-
230
- # Write to file
231
225
  File.write(schema_file, cleaned_schema)
232
226
 
233
- puts "Schema dumped successfully to #{schema_file}"
227
+ puts "Schema dumped successfully to #{schema_file}"
234
228
  puts " Size: #{File.size(schema_file)} bytes"
235
229
  end
236
230
 
@@ -248,14 +242,13 @@ class HTM
248
242
  schema_file = File.expand_path('../../db/schema.sql', __dir__)
249
243
 
250
244
  unless File.exist?(schema_file)
251
- puts "Schema file not found: #{schema_file}"
245
+ puts "Schema file not found: #{schema_file}"
252
246
  puts " Run 'rake htm:db:schema:dump' first to create it"
253
247
  exit 1
254
248
  end
255
249
 
256
250
  puts "Loading schema from #{schema_file}..."
257
251
 
258
- # Build psql command
259
252
  env = {
260
253
  'PGPASSWORD' => config[:password]
261
254
  }
@@ -270,35 +263,26 @@ class HTM
270
263
  '--quiet'
271
264
  ]
272
265
 
273
- # Execute psql
274
266
  require 'open3'
275
267
  stdout, stderr, status = Open3.capture3(env, *cmd)
276
268
 
277
269
  unless status.success?
278
- puts "Error loading schema:"
270
+ puts "Error loading schema:"
279
271
  puts stderr
280
272
  exit 1
281
273
  end
282
274
 
283
- puts "Schema loaded successfully"
275
+ puts "Schema loaded successfully"
284
276
  end
285
277
 
286
278
  # Generate database documentation using tbls
287
279
  #
288
- # Uses .tbls.yml configuration file for output directory and settings.
289
- # Creates comprehensive database documentation including:
290
- # - Entity-relationship diagrams
291
- # - Table schemas with comments
292
- # - Index information
293
- # - Relationship diagrams
294
- #
295
280
  # @param db_url [String] Database connection URL (uses ENV['HTM_DATABASE__URL'] if not provided)
296
281
  # @return [void]
297
282
  #
298
283
  def generate_docs(db_url = nil)
299
- # Check if tbls is installed
300
284
  unless system('which tbls > /dev/null 2>&1')
301
- puts "Error: 'tbls' is not installed"
285
+ puts "Error: 'tbls' is not installed"
302
286
  puts ""
303
287
  puts "Install tbls:"
304
288
  puts " brew install k1LoW/tap/tbls"
@@ -309,20 +293,17 @@ class HTM
309
293
  exit 1
310
294
  end
311
295
 
312
- # Find the project root (where .tbls.yml should be)
313
296
  project_root = File.expand_path('../..', __dir__)
314
297
  tbls_config = File.join(project_root, '.tbls.yml')
315
298
 
316
299
  unless File.exist?(tbls_config)
317
- puts "Error: .tbls.yml not found at #{tbls_config}"
300
+ puts "Error: .tbls.yml not found at #{tbls_config}"
318
301
  exit 1
319
302
  end
320
303
 
321
- # Get database URL
322
304
  dsn = db_url || ENV['HTM_DATABASE__URL']
323
305
  raise "Database configuration not found. Set HTM_DATABASE__URL environment variable." unless dsn
324
306
 
325
- # Ensure sslmode is set for local development (tbls requires it)
326
307
  unless dsn.include?('sslmode=')
327
308
  separator = dsn.include?('?') ? '&' : '?'
328
309
  dsn = "#{dsn}#{separator}sslmode=disable"
@@ -330,15 +311,13 @@ class HTM
330
311
 
331
312
  puts "Generating database documentation using #{tbls_config}..."
332
313
 
333
- # Run tbls doc command with config file and DSN override
334
- # The --dsn flag overrides the dsn in .tbls.yml but other settings are preserved
335
314
  require 'open3'
336
315
  cmd = ['tbls', 'doc', '--config', tbls_config, '--dsn', dsn, '--force']
337
316
 
338
317
  stdout, stderr, status = Open3.capture3(*cmd)
339
318
 
340
319
  unless status.success?
341
- puts "Error generating documentation:"
320
+ puts "Error generating documentation:"
342
321
  puts stderr
343
322
  puts stdout
344
323
  exit 1
@@ -346,9 +325,8 @@ class HTM
346
325
 
347
326
  puts stdout if stdout && !stdout.empty?
348
327
 
349
- # Read docPath from config to show correct output location
350
- doc_path = 'docs/database' # default from .tbls.yml
351
- puts "✓ Database documentation generated successfully"
328
+ doc_path = 'docs/database'
329
+ puts "Database documentation generated successfully"
352
330
  puts ""
353
331
  puts "Documentation files:"
354
332
  puts " #{doc_path}/README.md - Main documentation"
@@ -373,7 +351,6 @@ class HTM
373
351
  puts "\nHTM Database Information (#{HTM.env})"
374
352
  puts "=" * 80
375
353
 
376
- # Connection info
377
354
  puts "\nConnection:"
378
355
  puts " Environment: #{HTM.env}"
379
356
  puts " Host: #{config[:host]}"
@@ -381,19 +358,16 @@ class HTM
381
358
  puts " Database: #{config[:dbname]}"
382
359
  puts " User: #{config[:user]}"
383
360
 
384
- # PostgreSQL version
385
361
  version = conn.exec("SELECT version()").first['version']
386
362
  puts "\nPostgreSQL Version:"
387
363
  puts " #{version.split(',').first}"
388
364
 
389
- # Extensions
390
365
  puts "\nExtensions:"
391
366
  extensions = conn.exec("SELECT extname, extversion FROM pg_extension ORDER BY extname").to_a
392
367
  extensions.each do |ext|
393
368
  puts " #{ext['extname']} (#{ext['extversion']})"
394
369
  end
395
370
 
396
- # Table info
397
371
  puts "\nHTM Tables:"
398
372
  tables = ['nodes', 'node_tags', 'tags', 'robots', 'robot_nodes', 'file_sources', 'schema_migrations']
399
373
  tables.each do |table|
@@ -405,7 +379,6 @@ class HTM
405
379
  end
406
380
  end
407
381
 
408
- # Database size
409
382
  db_size = conn.exec(
410
383
  "SELECT pg_size_pretty(pg_database_size($1)) AS size",
411
384
  [config[:dbname]]
@@ -427,7 +400,6 @@ class HTM
427
400
 
428
401
  uri = URI.parse(url)
429
402
 
430
- # Validate URL format
431
403
  unless uri.scheme&.match?(/\Apostgres(?:ql)?\z/i)
432
404
  raise ArgumentError, "Invalid database URL scheme: #{uri.scheme}. Expected 'postgresql' or 'postgres'."
433
405
  end
@@ -436,7 +408,7 @@ class HTM
436
408
  raise ArgumentError, "Database URL must include a host"
437
409
  end
438
410
 
439
- dbname = uri.path&.slice(1..-1) # Remove leading /
411
+ dbname = uri.path&.slice(1..-1)
440
412
  if dbname.nil? || dbname.empty?
441
413
  raise ArgumentError, "Database URL must include a database name (path segment)"
442
414
  end
@@ -482,16 +454,16 @@ class HTM
482
454
  htm_config = HTM.config
483
455
 
484
456
  if htm_config.database_configured?
485
- ar_config = htm_config.database_config
457
+ db_config = htm_config.database_config
486
458
 
487
- # Convert ActiveRecord config keys to PG-style keys
459
+ # Convert to PG-style keys
488
460
  {
489
- host: ar_config[:host],
490
- port: ar_config[:port],
491
- dbname: ar_config[:database],
492
- user: ar_config[:username],
493
- password: ar_config[:password],
494
- sslmode: ar_config[:sslmode] || 'prefer'
461
+ host: db_config[:host],
462
+ port: db_config[:port],
463
+ dbname: db_config[:database],
464
+ user: db_config[:username],
465
+ password: db_config[:password],
466
+ sslmode: db_config[:sslmode] || 'prefer'
495
467
  }
496
468
  elsif ENV['HTM_DATABASE__URL']
497
469
  parse_connection_url(ENV['HTM_DATABASE__URL'])
@@ -506,38 +478,46 @@ class HTM
506
478
  # Check pgvector
507
479
  pgvector = conn.exec("SELECT extversion FROM pg_extension WHERE extname='vector'").first
508
480
  if pgvector
509
- puts "pgvector version: #{pgvector['extversion']}"
481
+ puts "pgvector version: #{pgvector['extversion']}"
510
482
  else
511
- puts "Warning: pgvector extension not found"
483
+ puts "Warning: pgvector extension not found"
512
484
  end
513
485
 
514
486
  # Check pg_trgm
515
487
  pg_trgm = conn.exec("SELECT extversion FROM pg_extension WHERE extname='pg_trgm'").first
516
488
  if pg_trgm
517
- puts "pg_trgm version: #{pg_trgm['extversion']}"
489
+ puts "pg_trgm version: #{pg_trgm['extversion']}"
490
+ else
491
+ puts "Warning: pg_trgm extension not found"
492
+ end
493
+
494
+ # Check pg_search (BM25 full-text search)
495
+ pg_search = conn.exec("SELECT extversion FROM pg_extension WHERE extname='pg_search'").first
496
+ if pg_search
497
+ puts "pg_search version: #{pg_search['extversion']}"
518
498
  else
519
- puts "Warning: pg_trgm extension not found"
499
+ puts "Warning: pg_search extension not found"
520
500
  end
521
501
  end
522
502
 
523
- # Run ActiveRecord migrations from db/migrate/
503
+ # Run Sequel migrations from db/migrate/
524
504
  #
525
505
  # @return [void]
526
506
  #
527
- def run_activerecord_migrations
507
+ def run_sequel_migrations
528
508
  migrations_path = File.expand_path('../../db/migrate', __dir__)
529
509
 
530
510
  unless Dir.exist?(migrations_path)
531
- puts "No migrations directory found at #{migrations_path}"
511
+ puts "No migrations directory found at #{migrations_path}"
532
512
  return
533
513
  end
534
514
 
535
- conn = ActiveRecord::Base.connection
515
+ db = HTM.db
536
516
 
537
517
  # Create schema_migrations table if it doesn't exist
538
- unless conn.table_exists?('schema_migrations')
539
- conn.create_table(:schema_migrations, id: false) do |t|
540
- t.string :version, null: false, primary_key: true
518
+ unless db.table_exists?(:schema_migrations)
519
+ db.create_table(:schema_migrations) do
520
+ String :version, primary_key: true, null: false
541
521
  end
542
522
  end
543
523
 
@@ -550,17 +530,13 @@ class HTM
550
530
  version = File.basename(file).split('_').first
551
531
  name = File.basename(file, '.rb')
552
532
 
553
- # Check if already run (use parameterized query to prevent SQL injection)
554
- already_run = conn.select_value(
555
- ActiveRecord::Base.sanitize_sql_array(
556
- ["SELECT COUNT(*) FROM schema_migrations WHERE version = ?", version]
557
- )
558
- ).to_i > 0
533
+ # Check if already run
534
+ already_run = db[:schema_migrations].where(version: version).count > 0
559
535
 
560
536
  if already_run
561
- puts " #{name} (already migrated)"
537
+ puts " [x] #{name} (already migrated)"
562
538
  else
563
- puts " Running #{name}..."
539
+ puts " --> Running #{name}..."
564
540
  require file
565
541
 
566
542
  # Get the migration class
@@ -568,21 +544,17 @@ class HTM
568
544
  migration_class = Object.const_get(class_name)
569
545
 
570
546
  # Run the migration
571
- migration = migration_class.new
572
- migration.migrate(:up)
547
+ migration = migration_class.new(db)
548
+ migration.up
573
549
 
574
- # Record in schema_migrations (use parameterized query to prevent SQL injection)
575
- conn.execute(
576
- ActiveRecord::Base.sanitize_sql_array(
577
- ["INSERT INTO schema_migrations (version) VALUES (?)", version]
578
- )
579
- )
550
+ # Record in schema_migrations
551
+ db[:schema_migrations].insert(version: version)
580
552
 
581
- puts "Completed"
553
+ puts " Completed"
582
554
  end
583
555
  end
584
556
 
585
- puts "All migrations completed"
557
+ puts "All migrations completed"
586
558
  end
587
559
 
588
560
  # Clean up pg_dump output to make it more readable
@@ -594,18 +566,15 @@ class HTM
594
566
  lines = schema_dump.split("\n")
595
567
  cleaned = []
596
568
 
597
- # Add header
598
569
  cleaned << "-- HTM Database Schema"
599
570
  cleaned << "-- Auto-generated from database using pg_dump"
600
571
  cleaned << "-- DO NOT EDIT THIS FILE MANUALLY"
601
572
  cleaned << "-- Run 'rake htm:db:schema:dump' to regenerate"
602
573
  cleaned << ""
603
574
 
604
- # Skip pg_dump header comments
605
575
  skip_until_content = true
606
576
 
607
577
  lines.each do |line|
608
- # Skip header comments
609
578
  if skip_until_content
610
579
  if line =~ /^(SET|CREATE|ALTER|--\s*Name:|COMMENT)/
611
580
  skip_until_content = false
@@ -614,29 +583,18 @@ class HTM
614
583
  end
615
584
  end
616
585
 
617
- # Skip SET commands (session-specific settings)
618
586
  next if line =~ /^SET /
619
-
620
- # Skip SELECT pg_catalog.set_config
621
587
  next if line =~ /^SELECT pg_catalog\.set_config/
622
-
623
- # Skip extension comments (we keep extension creation)
624
588
  next if line =~ /^COMMENT ON EXTENSION/
625
589
 
626
- # Keep everything else
627
590
  cleaned << line
628
591
  end
629
592
 
630
- # Remove multiple blank lines
631
593
  result = cleaned.join("\n")
632
594
  result.gsub!(/\n{3,}/, "\n\n")
633
595
 
634
596
  result
635
597
  end
636
-
637
- # Old methods removed - now using ActiveRecord migrations
638
- # def run_schema(conn) - REMOVED
639
- # def run_migrations_if_needed(conn) - REMOVED (see run_activerecord_migrations above)
640
598
  end
641
599
  end
642
600
  end
data/lib/htm/errors.rb CHANGED
@@ -44,6 +44,20 @@ class HTM
44
44
  #
45
45
  class ValidationError < Error; end
46
46
 
47
+ # Raised when configuration is invalid or incomplete
48
+ #
49
+ # Common causes:
50
+ # - Invalid HTM_ENV value (not defined in config)
51
+ # - Missing database configuration for environment
52
+ # - HTM_CONF pointing to non-existent file
53
+ # - Invalid YAML syntax in config file
54
+ #
55
+ # @example
56
+ # HTM_ENV=staginr rake htm:db:create # => raises ConfigurationError
57
+ # HTM_ENV=defaults rake htm:db:setup # => raises ConfigurationError
58
+ #
59
+ class ConfigurationError < Error; end
60
+
47
61
  # Raised when system resources are exhausted
48
62
  #
49
63
  # Common causes:
@@ -100,6 +100,7 @@ class HTM
100
100
  # Rack middleware for HTM connection management
101
101
  #
102
102
  # Ensures database connections are properly managed across requests.
103
+ # With Sequel's fiber-safe connection pooling, this is largely automatic.
103
104
  #
104
105
  # @example Use in Sinatra app
105
106
  # class MyApp < Sinatra::Base
@@ -107,70 +108,38 @@ class HTM
107
108
  # end
108
109
  #
109
110
  class Middleware
110
- # Class-level storage for connection configuration (shared across threads)
111
- @@db_config = nil
112
- @@config_mutex = Mutex.new
113
-
114
111
  def initialize(app, options = {})
115
112
  @app = app
116
113
  @options = options
117
114
  end
118
115
 
119
116
  def call(env)
120
- # Ensure connection is available in this thread
121
- ensure_thread_connection!
117
+ # Ensure connection is available
118
+ ensure_connection!
122
119
 
123
120
  # Process request
124
121
  status, headers, body = @app.call(env)
125
122
 
126
123
  # Return response
127
124
  [status, headers, body]
128
- ensure
129
- # Return connections to pool after request completes
130
- if defined?(ActiveRecord::Base) && ActiveRecord::Base.respond_to?(:connection_handler)
131
- ActiveRecord::Base.connection_handler.clear_active_connections!
132
- end
133
125
  end
134
126
 
135
127
  # Store the connection config at startup (called from register_htm)
136
128
  def self.store_config!
137
- @@config_mutex.synchronize do
138
- return if @@db_config
139
-
140
- @@db_config = HTM::ActiveRecordConfig.load_database_config
141
- end
129
+ # With Sequel, connection is established globally via HTM::SequelConfig
130
+ # No additional per-request config storage needed
142
131
  end
143
132
 
144
133
  private
145
134
 
146
- def ensure_thread_connection!
147
- # Check if connection pool exists and has an active connection
148
- pool_exists = begin
149
- ActiveRecord::Base.connection_pool
150
- true
151
- rescue ActiveRecord::ConnectionNotDefined
152
- false
153
- end
154
-
155
- if pool_exists
156
- return if ActiveRecord::Base.connection_pool.active_connection?
157
- end
158
-
159
- # Re-establish connection using stored config
160
- if @@db_config
161
- ActiveRecord::Base.establish_connection(@@db_config)
162
- else
163
- raise "HTM database config not stored - call register_htm at app startup"
164
- end
165
- rescue ActiveRecord::ConnectionNotDefined, ActiveRecord::ConnectionNotEstablished
166
- # Pool doesn't exist, establish connection
167
- if @@db_config
168
- ActiveRecord::Base.establish_connection(@@db_config)
169
- else
170
- raise "HTM database config not stored - call register_htm at app startup"
135
+ def ensure_connection!
136
+ # Sequel handles connection pooling automatically
137
+ # Just verify the connection is available
138
+ unless HTM.db
139
+ HTM::SequelConfig.establish_connection!
171
140
  end
172
141
  rescue StandardError => e
173
- HTM.logger.error "Failed to ensure thread connection: #{e.class} - #{e.message}"
142
+ HTM.logger.error "Failed to ensure connection: #{e.class} - #{e.message}"
174
143
  raise
175
144
  end
176
145
  end
@@ -212,10 +181,10 @@ module ::Sinatra
212
181
  end
213
182
  end
214
183
 
215
- # Store database config for thread-safe access and establish initial connection
184
+ # Establish initial connection (Sequel handles pooling automatically)
216
185
  begin
217
186
  HTM::Sinatra::Middleware.store_config!
218
- HTM::ActiveRecordConfig.establish_connection!
187
+ HTM::SequelConfig.establish_connection!
219
188
  HTM.logger.info "HTM database connection established"
220
189
  rescue StandardError => e
221
190
  HTM.logger.error "Failed to establish HTM database connection: #{e.message}"