htm 0.0.31 → 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 (157) 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/validator.rb +12 -18
  111. data/lib/htm/config.rb +76 -65
  112. data/lib/htm/database.rb +193 -199
  113. data/lib/htm/embedding_service.rb +4 -9
  114. data/lib/htm/integrations/sinatra.rb +7 -7
  115. data/lib/htm/job_adapter.rb +14 -21
  116. data/lib/htm/jobs/generate_embedding_job.rb +28 -44
  117. data/lib/htm/jobs/generate_propositions_job.rb +29 -55
  118. data/lib/htm/jobs/generate_relationships_job.rb +137 -0
  119. data/lib/htm/jobs/generate_tags_job.rb +45 -67
  120. data/lib/htm/loaders/markdown_loader.rb +65 -112
  121. data/lib/htm/long_term_memory/fulltext_search.rb +1 -1
  122. data/lib/htm/long_term_memory/hybrid_search.rb +300 -128
  123. data/lib/htm/long_term_memory/node_operations.rb +2 -2
  124. data/lib/htm/long_term_memory/relevance_scorer.rb +100 -68
  125. data/lib/htm/long_term_memory/tag_operations.rb +87 -120
  126. data/lib/htm/long_term_memory/vector_search.rb +1 -1
  127. data/lib/htm/long_term_memory.rb +2 -1
  128. data/lib/htm/mcp/cli.rb +59 -58
  129. data/lib/htm/mcp/server.rb +5 -6
  130. data/lib/htm/mcp/tools.rb +30 -36
  131. data/lib/htm/migration.rb +10 -10
  132. data/lib/htm/models/node.rb +2 -3
  133. data/lib/htm/models/node_relationship.rb +72 -0
  134. data/lib/htm/models/node_tag.rb +2 -2
  135. data/lib/htm/models/robot_node.rb +2 -2
  136. data/lib/htm/models/tag.rb +41 -28
  137. data/lib/htm/observability.rb +45 -51
  138. data/lib/htm/proposition_service.rb +3 -7
  139. data/lib/htm/query_cache.rb +13 -15
  140. data/lib/htm/railtie.rb +1 -2
  141. data/lib/htm/robot_group.rb +9 -9
  142. data/lib/htm/sequel_config.rb +1 -0
  143. data/lib/htm/sql_builder.rb +1 -1
  144. data/lib/htm/tag_service.rb +2 -6
  145. data/lib/htm/timeframe.rb +4 -5
  146. data/lib/htm/timeframe_extractor.rb +42 -83
  147. data/lib/htm/version.rb +1 -1
  148. data/lib/htm/workflows/remember_workflow.rb +112 -115
  149. data/lib/htm/working_memory.rb +21 -26
  150. data/lib/htm.rb +103 -116
  151. data/lib/tasks/db.rake +0 -2
  152. data/lib/tasks/doc.rake +14 -13
  153. data/lib/tasks/files.rake +5 -12
  154. data/lib/tasks/htm.rake +70 -71
  155. data/lib/tasks/jobs.rake +41 -47
  156. data/lib/tasks/tags.rake +3 -8
  157. metadata +25 -100
data/lib/htm/database.rb CHANGED
@@ -2,7 +2,6 @@
2
2
 
3
3
  require 'pg'
4
4
  require 'uri'
5
- require 'set'
6
5
 
7
6
  class HTM
8
7
  # Database setup and configuration for HTM
@@ -35,10 +34,9 @@ class HTM
35
34
  puts "HTM database schema created successfully"
36
35
 
37
36
  # Optionally dump schema
38
- if dump_schema
39
- puts ""
40
- self.dump_schema(db_url)
41
- end
37
+ return unless dump_schema
38
+ puts ""
39
+ self.dump_schema(db_url)
42
40
  end
43
41
 
44
42
  # Run pending database migrations
@@ -70,45 +68,12 @@ class HTM
70
68
  require 'sequel'
71
69
  require_relative 'sequel_config'
72
70
 
73
- # Establish Sequel connection (don't load models - we just need the DB)
74
71
  HTM::SequelConfig.establish_connection!(load_models: false)
75
72
 
76
- migrations_path = File.expand_path('../../db/migrate', __dir__)
77
-
78
- # Get available migrations from files
79
- available_migrations = Dir.glob(File.join(migrations_path, '*.rb')).map do |file|
80
- {
81
- version: File.basename(file).split('_').first.to_i,
82
- name: File.basename(file, '.rb')
83
- }
84
- end.sort_by { |m| m[:version] }
85
-
86
- # Get applied migrations from database
87
- applied_versions = begin
88
- HTM.db[:schema_migrations].select_map(:version).map(&:to_i)
89
- rescue Sequel::DatabaseError
90
- []
91
- end
92
-
93
- puts "\nMigration Status"
94
- puts "=" * 100
95
-
96
- if available_migrations.empty?
97
- puts "No migration files found in db/migrate/"
98
- else
99
- available_migrations.each do |migration|
100
- status = applied_versions.include?(migration[:version])
101
- status_mark = status ? "[x]" : "[ ]"
102
-
103
- puts "#{status_mark} #{migration[:name]}"
104
- end
105
- end
73
+ available = load_available_migrations
74
+ applied = load_applied_versions
106
75
 
107
- applied_count = applied_versions.length
108
- pending_count = available_migrations.length - applied_count
109
-
110
- puts "\nSummary: #{applied_count} applied, #{pending_count} pending"
111
- puts "=" * 100
76
+ print_migration_status_table(available, applied)
112
77
  end
113
78
 
114
79
  # Drop all HTM tables (respects RAILS_ENV)
@@ -125,16 +90,14 @@ class HTM
125
90
 
126
91
  conn = PG.connect(config)
127
92
 
128
- tables = ['nodes', 'node_tags', 'tags', 'robots', 'robot_nodes', 'file_sources', 'schema_migrations']
93
+ tables = %w[nodes node_tags tags robots robot_nodes file_sources schema_migrations]
129
94
 
130
95
  puts "Dropping HTM tables..."
131
96
  tables.each do |table|
132
- begin
133
- conn.exec("DROP TABLE IF EXISTS #{table} CASCADE")
134
- puts " Dropped #{table}"
135
- rescue PG::Error => e
136
- puts " Error dropping #{table}: #{e.message}"
137
- end
97
+ conn.exec("DROP TABLE IF EXISTS #{table} CASCADE")
98
+ puts " Dropped #{table}"
99
+ rescue PG::Error => e
100
+ puts " Error dropping #{table}: #{e.message}"
138
101
  end
139
102
 
140
103
  # Drop functions and triggers
@@ -264,7 +227,7 @@ class HTM
264
227
  ]
265
228
 
266
229
  require 'open3'
267
- stdout, stderr, status = Open3.capture3(env, *cmd)
230
+ _, stderr, status = Open3.capture3(env, *cmd)
268
231
 
269
232
  unless status.success?
270
233
  puts "Error loading schema:"
@@ -281,60 +244,11 @@ class HTM
281
244
  # @return [void]
282
245
  #
283
246
  def generate_docs(db_url = nil)
284
- unless system('which tbls > /dev/null 2>&1')
285
- puts "Error: 'tbls' is not installed"
286
- puts ""
287
- puts "Install tbls:"
288
- puts " brew install k1LoW/tap/tbls"
289
- puts " # or"
290
- puts " go install github.com/k1LoW/tbls@latest"
291
- puts ""
292
- puts "See: https://github.com/k1LoW/tbls"
293
- exit 1
294
- end
295
-
296
- project_root = File.expand_path('../..', __dir__)
297
- tbls_config = File.join(project_root, '.tbls.yml')
298
-
299
- unless File.exist?(tbls_config)
300
- puts "Error: .tbls.yml not found at #{tbls_config}"
301
- exit 1
302
- end
303
-
304
- dsn = db_url || ENV['HTM_DATABASE__URL']
305
- raise "Database configuration not found. Set HTM_DATABASE__URL environment variable." unless dsn
306
-
307
- unless dsn.include?('sslmode=')
308
- separator = dsn.include?('?') ? '&' : '?'
309
- dsn = "#{dsn}#{separator}sslmode=disable"
310
- end
311
-
312
- puts "Generating database documentation using #{tbls_config}..."
313
-
314
- require 'open3'
315
- cmd = ['tbls', 'doc', '--config', tbls_config, '--dsn', dsn, '--force']
316
-
317
- stdout, stderr, status = Open3.capture3(*cmd)
318
-
319
- unless status.success?
320
- puts "Error generating documentation:"
321
- puts stderr
322
- puts stdout
323
- exit 1
324
- end
325
-
326
- puts stdout if stdout && !stdout.empty?
327
-
328
- doc_path = 'docs/database'
329
- puts "Database documentation generated successfully"
330
- puts ""
331
- puts "Documentation files:"
332
- puts " #{doc_path}/README.md - Main documentation"
333
- puts " #{doc_path}/schema.svg - ER diagram"
334
- puts " #{doc_path}/*.md - Individual table documentation"
335
- puts ""
336
- puts "View documentation:"
337
- puts " open #{doc_path}/README.md"
247
+ check_tbls_installed!
248
+ tbls_config = locate_tbls_config!
249
+ dsn = build_tbls_dsn(db_url)
250
+ run_tbls_doc(tbls_config, dsn)
251
+ print_tbls_doc_success
338
252
  end
339
253
 
340
254
  # Show database info (respects RAILS_ENV)
@@ -347,44 +261,13 @@ class HTM
347
261
  raise "Database configuration not found" unless config
348
262
 
349
263
  conn = PG.connect(config)
350
-
351
264
  puts "\nHTM Database Information (#{HTM.env})"
352
265
  puts "=" * 80
353
-
354
- puts "\nConnection:"
355
- puts " Environment: #{HTM.env}"
356
- puts " Host: #{config[:host]}"
357
- puts " Port: #{config[:port]}"
358
- puts " Database: #{config[:dbname]}"
359
- puts " User: #{config[:user]}"
360
-
361
- version = conn.exec("SELECT version()").first['version']
362
- puts "\nPostgreSQL Version:"
363
- puts " #{version.split(',').first}"
364
-
365
- puts "\nExtensions:"
366
- extensions = conn.exec("SELECT extname, extversion FROM pg_extension ORDER BY extname").to_a
367
- extensions.each do |ext|
368
- puts " #{ext['extname']} (#{ext['extversion']})"
369
- end
370
-
371
- puts "\nHTM Tables:"
372
- tables = ['nodes', 'node_tags', 'tags', 'robots', 'robot_nodes', 'file_sources', 'schema_migrations']
373
- tables.each do |table|
374
- begin
375
- count = conn.exec("SELECT COUNT(*) FROM #{table}").first['count']
376
- puts " #{table}: #{count} rows"
377
- rescue PG::UndefinedTable
378
- puts " #{table}: not created"
379
- end
380
- end
381
-
382
- db_size = conn.exec(
383
- "SELECT pg_size_pretty(pg_database_size($1)) AS size",
384
- [config[:dbname]]
385
- ).first['size']
386
- puts "\nDatabase Size: #{db_size}"
387
-
266
+ print_connection_info(config)
267
+ print_pg_version(conn)
268
+ print_extensions_list(conn)
269
+ print_table_counts(conn)
270
+ print_db_size(conn, config[:dbname])
388
271
  conn.close
389
272
  puts "=" * 80
390
273
  end
@@ -399,20 +282,9 @@ class HTM
399
282
  return nil unless url
400
283
 
401
284
  uri = URI.parse(url)
402
-
403
- unless uri.scheme&.match?(/\Apostgres(?:ql)?\z/i)
404
- raise ArgumentError, "Invalid database URL scheme: #{uri.scheme}. Expected 'postgresql' or 'postgres'."
405
- end
406
-
407
- unless uri.host && !uri.host.empty?
408
- raise ArgumentError, "Database URL must include a host"
409
- end
285
+ validate_pg_uri!(uri)
410
286
 
411
287
  dbname = uri.path&.slice(1..-1)
412
- if dbname.nil? || dbname.empty?
413
- raise ArgumentError, "Database URL must include a database name (path segment)"
414
- end
415
-
416
288
  params = URI.decode_www_form(uri.query || '').to_h
417
289
 
418
290
  {
@@ -437,9 +309,9 @@ class HTM
437
309
  {
438
310
  host: ENV['HTM_DATABASE__HOST'] || 'localhost',
439
311
  port: (ENV['HTM_DATABASE__PORT'] || 5432).to_i,
440
- dbname: ENV['HTM_DATABASE__NAME'],
441
- user: ENV['HTM_DATABASE__USER'],
442
- password: ENV['HTM_DATABASE__PASSWORD'],
312
+ dbname: ENV.fetch('HTM_DATABASE__NAME', nil),
313
+ user: ENV.fetch('HTM_DATABASE__USER', nil),
314
+ password: ENV.fetch('HTM_DATABASE__PASSWORD', nil),
443
315
  sslmode: ENV['HTM_DATABASE__SSLMODE'] || 'prefer'
444
316
  }
445
317
  end
@@ -474,6 +346,65 @@ class HTM
474
346
 
475
347
  private
476
348
 
349
+ def print_connection_info(config)
350
+ puts "\nConnection:"
351
+ puts " Environment: #{HTM.env}"
352
+ puts " Host: #{config[:host]}"
353
+ puts " Port: #{config[:port]}"
354
+ puts " Database: #{config[:dbname]}"
355
+ puts " User: #{config[:user]}"
356
+ end
357
+
358
+ def print_pg_version(conn)
359
+ version = conn.exec("SELECT version()").first['version']
360
+ puts "\nPostgreSQL Version:"
361
+ puts " #{version.split(',').first}"
362
+ end
363
+
364
+ def print_extensions_list(conn)
365
+ puts "\nExtensions:"
366
+ conn.exec("SELECT extname, extversion FROM pg_extension ORDER BY extname").each do |ext|
367
+ puts " #{ext['extname']} (#{ext['extversion']})"
368
+ end
369
+ end
370
+
371
+ def print_table_counts(conn)
372
+ puts "\nHTM Tables:"
373
+ %w[nodes node_tags tags robots robot_nodes file_sources schema_migrations].each do |table|
374
+ count = conn.exec("SELECT COUNT(*) FROM #{table}").first['count']
375
+ puts " #{table}: #{count} rows"
376
+ rescue PG::UndefinedTable
377
+ puts " #{table}: not created"
378
+ end
379
+ end
380
+
381
+ def print_db_size(conn, dbname)
382
+ size = conn.exec("SELECT pg_size_pretty(pg_database_size($1)) AS size", [dbname]).first['size']
383
+ puts "\nDatabase Size: #{size}"
384
+ end
385
+
386
+ def ensure_schema_migrations_table(db)
387
+ return if db.table_exists?(:schema_migrations)
388
+ db.create_table(:schema_migrations) { String :version, primary_key: true, null: false }
389
+ end
390
+
391
+ def run_migration_file(file, db)
392
+ version = File.basename(file).split('_').first
393
+ name = File.basename(file, '.rb')
394
+
395
+ if db[:schema_migrations].where(version: version).any?
396
+ puts " [x] #{name} (already migrated)"
397
+ else
398
+ puts " --> Running #{name}..."
399
+ require file
400
+ class_name = name.split('_')[1..].map(&:capitalize).join
401
+ migration_class = Object.const_get(class_name)
402
+ migration_class.new(db).up
403
+ db[:schema_migrations].insert(version: version)
404
+ puts " Completed"
405
+ end
406
+ end
407
+
477
408
  def verify_extensions(conn)
478
409
  # Check pgvector
479
410
  pgvector = conn.exec("SELECT extversion FROM pg_extension WHERE extname='vector'").first
@@ -506,54 +437,17 @@ class HTM
506
437
  #
507
438
  def run_sequel_migrations
508
439
  migrations_path = File.expand_path('../../db/migrate', __dir__)
509
-
510
440
  unless Dir.exist?(migrations_path)
511
441
  puts "No migrations directory found at #{migrations_path}"
512
442
  return
513
443
  end
514
444
 
515
445
  db = HTM.db
446
+ ensure_schema_migrations_table(db)
516
447
 
517
- # Create schema_migrations table if it doesn't exist
518
- unless db.table_exists?(:schema_migrations)
519
- db.create_table(:schema_migrations) do
520
- String :version, primary_key: true, null: false
521
- end
522
- end
523
-
524
- # Get list of migration files
525
- migration_files = Dir.glob("#{migrations_path}/*.rb").sort
448
+ migration_files = Dir.glob("#{migrations_path}/*.rb")
526
449
  puts "Found #{migration_files.length} migration files"
527
-
528
- # Run each migration
529
- migration_files.each do |file|
530
- version = File.basename(file).split('_').first
531
- name = File.basename(file, '.rb')
532
-
533
- # Check if already run
534
- already_run = db[:schema_migrations].where(version: version).count > 0
535
-
536
- if already_run
537
- puts " [x] #{name} (already migrated)"
538
- else
539
- puts " --> Running #{name}..."
540
- require file
541
-
542
- # Get the migration class
543
- class_name = name.split('_')[1..].map(&:capitalize).join
544
- migration_class = Object.const_get(class_name)
545
-
546
- # Run the migration
547
- migration = migration_class.new(db)
548
- migration.up
549
-
550
- # Record in schema_migrations
551
- db[:schema_migrations].insert(version: version)
552
-
553
- puts " Completed"
554
- end
555
- end
556
-
450
+ migration_files.each { |file| run_migration_file(file, db) }
557
451
  puts "All migrations completed"
558
452
  end
559
453
 
@@ -576,11 +470,9 @@ class HTM
576
470
 
577
471
  lines.each do |line|
578
472
  if skip_until_content
579
- if line =~ /^(SET|CREATE|ALTER|--\s*Name:|COMMENT)/
580
- skip_until_content = false
581
- else
582
- next
583
- end
473
+ next unless line =~ /^(SET|CREATE|ALTER|--\s*Name:|COMMENT)/
474
+ skip_until_content = false
475
+
584
476
  end
585
477
 
586
478
  next if line =~ /^SET /
@@ -595,6 +487,108 @@ class HTM
595
487
 
596
488
  result
597
489
  end
490
+
491
+ def load_available_migrations
492
+ migrations_path = File.expand_path('../../db/migrate', __dir__)
493
+ migrations = Dir.glob(File.join(migrations_path, '*.rb')).map do |file|
494
+ { version: File.basename(file).split('_').first.to_i, name: File.basename(file, '.rb') }
495
+ end
496
+ migrations.sort_by { |m| m[:version] }
497
+ end
498
+
499
+ def load_applied_versions
500
+ HTM.db[:schema_migrations].select_map(:version).map(&:to_i)
501
+ rescue Sequel::DatabaseError
502
+ []
503
+ end
504
+
505
+ def print_migration_status_table(available, applied)
506
+ puts "\nMigration Status"
507
+ puts "=" * 100
508
+ if available.empty?
509
+ puts "No migration files found in db/migrate/"
510
+ else
511
+ available.each do |m|
512
+ mark = applied.include?(m[:version]) ? "[x]" : "[ ]"
513
+ puts "#{mark} #{m[:name]}"
514
+ end
515
+ end
516
+ pending = available.length - applied.length
517
+ puts "\nSummary: #{applied.length} applied, #{pending} pending"
518
+ puts "=" * 100
519
+ end
520
+
521
+ def check_tbls_installed!
522
+ return if system('which tbls > /dev/null 2>&1')
523
+
524
+ puts <<~MSG
525
+ Error: 'tbls' is not installed
526
+
527
+ Install tbls:
528
+ brew install k1LoW/tap/tbls
529
+ # or
530
+ go install github.com/k1LoW/tbls@latest
531
+
532
+ See: https://github.com/k1LoW/tbls
533
+ MSG
534
+ exit 1
535
+ end
536
+
537
+ def locate_tbls_config!
538
+ project_root = File.expand_path('../..', __dir__)
539
+ config_path = File.join(project_root, '.tbls.yml')
540
+ return config_path if File.exist?(config_path)
541
+
542
+ puts "Error: .tbls.yml not found at #{config_path}"
543
+ exit 1
544
+ end
545
+
546
+ def build_tbls_dsn(db_url)
547
+ dsn = db_url || ENV.fetch('HTM_DATABASE__URL', nil)
548
+ raise "Database configuration not found. Set HTM_DATABASE__URL environment variable." unless dsn
549
+
550
+ return dsn if dsn.include?('sslmode=')
551
+
552
+ separator = dsn.include?('?') ? '&' : '?'
553
+ "#{dsn}#{separator}sslmode=disable"
554
+ end
555
+
556
+ def run_tbls_doc(tbls_config, dsn)
557
+ require 'open3'
558
+ puts "Generating database documentation using #{tbls_config}..."
559
+ stdout, stderr, status = Open3.capture3('tbls', 'doc', '--config', tbls_config, '--dsn', dsn, '--force')
560
+ return puts(stdout) if status.success? && stdout && !stdout.empty?
561
+
562
+ puts "Error generating documentation:"
563
+ puts stderr
564
+ puts stdout
565
+ exit 1
566
+ end
567
+
568
+ def print_tbls_doc_success
569
+ doc_path = 'docs/database'
570
+ puts <<~MSG
571
+ Database documentation generated successfully
572
+
573
+ Documentation files:
574
+ #{doc_path}/README.md - Main documentation
575
+ #{doc_path}/schema.svg - ER diagram
576
+ #{doc_path}/*.md - Individual table documentation
577
+
578
+ View documentation:
579
+ open #{doc_path}/README.md
580
+ MSG
581
+ end
582
+
583
+ def validate_pg_uri!(uri)
584
+ unless uri.scheme&.match?(/\Apostgres(?:ql)?\z/i)
585
+ raise ArgumentError, "Invalid database URL scheme: #{uri.scheme}. Expected 'postgresql' or 'postgres'."
586
+ end
587
+ raise ArgumentError, "Database URL must include a host" if uri.host.nil? || uri.host.empty?
588
+
589
+ dbname = uri.path&.slice(1..-1)
590
+ raise ArgumentError, "Database URL must include a database name (path segment)" if dbname.nil? || dbname.empty?
591
+ end
598
592
  end
599
593
  end
600
594
  end
@@ -99,11 +99,7 @@ class HTM
99
99
  storage_embedding: storage_string,
100
100
  storage_dimension: max_dim
101
101
  }
102
-
103
- rescue HTM::CircuitBreakerOpenError
104
- # Re-raise circuit breaker errors without wrapping
105
- raise
106
- rescue HTM::EmbeddingError
102
+ rescue HTM::CircuitBreakerOpenError, HTM::EmbeddingError
107
103
  raise
108
104
  rescue StandardError => e
109
105
  HTM.logger.error "EmbeddingService: Failed to generate embedding: #{e.message}"
@@ -124,14 +120,13 @@ class HTM
124
120
  raise HTM::EmbeddingError, "Embedding array is empty"
125
121
  end
126
122
 
127
- unless embedding.all? { |v| v.is_a?(Numeric) }
123
+ unless embedding.all?(Numeric)
128
124
  raise HTM::EmbeddingError, "Embedding must contain only numeric values"
129
125
  end
130
126
 
131
127
  # Check for NaN or Infinity
132
- if embedding.any? { |v| v.respond_to?(:nan?) && v.nan? || v.respond_to?(:infinite?) && v.infinite? }
133
- raise HTM::EmbeddingError, "Embedding contains NaN or Infinity values"
134
- end
128
+ return unless embedding.any? { |v| (v.respond_to?(:nan?) && v.nan?) || (v.respond_to?(:infinite?) && v.infinite?) }
129
+ raise HTM::EmbeddingError, "Embedding contains NaN or Infinity values"
135
130
  end
136
131
 
137
132
  # Pad embedding to max_dimension with zeros
@@ -82,8 +82,8 @@ class HTM
82
82
  # @param options [Hash] Recall options (timeframe, limit, strategy, etc.)
83
83
  # @return [Array<Hash>] Matching memories
84
84
  #
85
- def recall(topic, **options)
86
- htm.recall(topic, **options)
85
+ def recall(topic, **)
86
+ htm.recall(topic, **)
87
87
  end
88
88
 
89
89
  # JSON response helper
@@ -174,11 +174,11 @@ module ::Sinatra
174
174
  config.logger = logger if respond_to?(:logger)
175
175
 
176
176
  # Use Sidekiq if available, otherwise thread-based
177
- if defined?(::Sidekiq)
178
- config.job.backend = :sidekiq
179
- else
180
- config.job.backend = :thread
181
- end
177
+ config.job.backend = if defined?(::Sidekiq)
178
+ :sidekiq
179
+ else
180
+ :thread
181
+ end
182
182
  end
183
183
 
184
184
  # Establish initial connection (Sequel handles pooling automatically)
@@ -84,21 +84,17 @@ class HTM
84
84
 
85
85
  # Execute job inline (synchronously)
86
86
  def enqueue_inline(job_class, **params)
87
- begin
88
- job_class.perform(**params)
89
- rescue StandardError => e
90
- HTM.logger.error "Inline job #{job_class.name} failed: #{e.class.name} - #{e.message}"
91
- end
87
+ job_class.perform(**params)
88
+ rescue StandardError => e
89
+ HTM.logger.error "Inline job #{job_class.name} failed: #{e.class.name} - #{e.message}"
92
90
  end
93
91
 
94
92
  # Execute job in background thread (legacy)
95
93
  def enqueue_thread(job_class, **params)
96
94
  Thread.new do
97
- begin
98
- job_class.perform(**params)
99
- rescue StandardError => e
100
- HTM.logger.error "Thread job #{job_class.name} failed: #{e.class.name} - #{e.message}"
101
- end
95
+ job_class.perform(**params)
96
+ rescue StandardError => e
97
+ HTM.logger.error "Thread job #{job_class.name} failed: #{e.class.name} - #{e.message}"
102
98
  end
103
99
  rescue StandardError => e
104
100
  HTM.logger.error "Failed to start thread for #{job_class.name}: #{e.message}"
@@ -108,11 +104,9 @@ class HTM
108
104
  # Non-blocking for I/O-bound operations like LLM API calls
109
105
  def enqueue_fiber(job_class, **params)
110
106
  Async do
111
- begin
112
- job_class.perform(**params)
113
- rescue StandardError => e
114
- HTM.logger.error "Fiber job #{job_class.name} failed: #{e.class.name} - #{e.message}"
115
- end
107
+ job_class.perform(**params)
108
+ rescue StandardError => e
109
+ HTM.logger.error "Fiber job #{job_class.name} failed: #{e.class.name} - #{e.message}"
116
110
  end
117
111
  rescue StandardError => e
118
112
  HTM.logger.error "Failed to start fiber for #{job_class.name}: #{e.message}"
@@ -153,16 +147,14 @@ class HTM
153
147
 
154
148
  # Execute multiple jobs in parallel using async fibers
155
149
  def enqueue_parallel_fiber(jobs)
156
- Async do |task|
150
+ Async do |_task|
157
151
  barrier = Async::Barrier.new
158
152
 
159
153
  jobs.each do |job_class, params|
160
154
  barrier.async do
161
- begin
162
- job_class.perform(**params)
163
- rescue StandardError => e
164
- HTM.logger.error "Parallel fiber job #{job_class.name} failed: #{e.class.name} - #{e.message}"
165
- end
155
+ job_class.perform(**params)
156
+ rescue StandardError => e
157
+ HTM.logger.error "Parallel fiber job #{job_class.name} failed: #{e.class.name} - #{e.message}"
166
158
  end
167
159
  end
168
160
 
@@ -202,6 +194,7 @@ class HTM
202
194
  # and convert string keys back to symbols for the underlying job
203
195
  Class.new do
204
196
  include Sidekiq::Worker
197
+
205
198
  sidekiq_options queue: :htm, retry: 3
206
199
 
207
200
  define_method(:perform) do |params|