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
data/lib/tasks/doc.rake CHANGED
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'English'
3
4
  namespace :htm do
4
5
  namespace :doc do
5
6
  desc "Build YARD API documentation (markdown format for MkDocs)"
@@ -13,7 +14,7 @@ namespace :htm do
13
14
  puts
14
15
 
15
16
  # Clean previous output
16
- FileUtils.rm_rf(output_dir) if Dir.exist?(output_dir)
17
+ FileUtils.rm_rf(output_dir)
17
18
  FileUtils.mkdir_p(output_dir)
18
19
 
19
20
  # Build YARD documentation in markdown format
@@ -35,7 +36,7 @@ namespace :htm do
35
36
 
36
37
  system("yard doc #{options.join(' ')}")
37
38
 
38
- if $?.success?
39
+ if $CHILD_STATUS.success?
39
40
  # Post-process markdown files for MkDocs compatibility
40
41
  fix_yard_anchors_for_mkdocs(output_dir)
41
42
 
@@ -71,7 +72,7 @@ namespace :htm do
71
72
 
72
73
  # Pattern 0: Fix malformed YARD output where code fence is joined with heading
73
74
  # "```## method_name() [](#anchor)" -> "```\n## method_name() {: #anchor }"
74
- content.gsub!(%r{^(```)(\#{1,6}\s+.+?)\s*\[\]\(\#([^)]+)\)\s*$}) do
75
+ content.gsub!(/^(```)(\#{1,6}\s+.+?)\s*\[\]\(\#([^)]+)\)\s*$/) do
75
76
  fence = Regexp.last_match(1)
76
77
  heading = Regexp.last_match(2)
77
78
  anchor_id = Regexp.last_match(3)
@@ -82,7 +83,7 @@ namespace :htm do
82
83
  # Pattern 1: Heading with trailing anchor link
83
84
  # "## method_name() [](#anchor-id)" -> "## method_name() {: #anchor-id }"
84
85
  # Use %r{} to avoid # interpolation issues in regex
85
- content.gsub!(%r{^(\#{1,6}\s+.+?)\s*\[\]\(\#([^)]+)\)\s*$}) do
86
+ content.gsub!(/^(\#{1,6}\s+.+?)\s*\[\]\(\#([^)]+)\)\s*$/) do
86
87
  heading = Regexp.last_match(1)
87
88
  anchor_id = Regexp.last_match(2)
88
89
  anchors_fixed += 1
@@ -91,7 +92,7 @@ namespace :htm do
91
92
 
92
93
  # Pattern 2: Attribute headings with [RW]/[R]/[W] markers
93
94
  # "## attr_name[RW] [](#attribute-i-attr_name)" -> "## attr_name [RW] {: #attribute-i-attr_name }"
94
- content.gsub!(%r{^(\#{1,6}\s+\w+)\[([RW]+)\]\s*\[\]\(\#([^)]+)\)\s*$}) do
95
+ content.gsub!(/^(\#{1,6}\s+\w+)\[([RW]+)\]\s*\[\]\(\#([^)]+)\)\s*$/) do
95
96
  heading = Regexp.last_match(1)
96
97
  rw_marker = Regexp.last_match(2)
97
98
  anchor_id = Regexp.last_match(3)
@@ -103,7 +104,8 @@ namespace :htm do
103
104
  # "**@param**" -> "**`@param`**" (inline code prevents magiclink processing)
104
105
  # Common YARD tags: @param, @return, @raise, @yield, @yieldparam, @yieldreturn,
105
106
  # @option, @overload, @example, @see, @note, @todo, @deprecated
106
- yard_tags = %w[param return raise yield yieldparam yieldreturn option overload example see note todo deprecated abstract api author since version private]
107
+ yard_tags = %w[param return raise yield yieldparam yieldreturn option overload example see note todo deprecated abstract api author
108
+ since version private]
107
109
  yard_tags.each do |tag|
108
110
  # Match **@tag** and replace with **`@tag`**
109
111
  if content.gsub!(/\*\*@#{tag}\*\*/i, "**`@#{tag}`**")
@@ -117,10 +119,9 @@ namespace :htm do
117
119
  end
118
120
  end
119
121
 
120
- if files_fixed > 0
121
- puts "Fixed #{anchors_fixed} anchors in #{files_fixed} files for MkDocs compatibility"
122
- puts "Escaped #{mentions_escaped} YARD annotations to prevent @mention linking" if mentions_escaped > 0
123
- end
122
+ return unless files_fixed.positive?
123
+ puts "Fixed #{anchors_fixed} anchors in #{files_fixed} files for MkDocs compatibility"
124
+ puts "Escaped #{mentions_escaped} YARD annotations to prevent @mention linking" if mentions_escaped.positive?
124
125
  end
125
126
 
126
127
  def create_yard_index_page(yard_output_dir)
@@ -192,12 +193,12 @@ namespace :htm do
192
193
  classes = []
193
194
 
194
195
  # Check for markdown files in output directory
195
- Dir.glob(File.join(yard_output_dir, "**/*.md")).sort.each do |file|
196
+ Dir.glob(File.join(yard_output_dir, "**/*.md")).each do |file|
196
197
  relative_path = file.sub("#{yard_output_dir}/", "")
197
198
  basename = File.basename(file, ".md")
198
199
 
199
200
  # Skip index files and non-class files
200
- next if basename == "index" || basename == "_index"
201
+ next if %w[index _index].include?(basename)
201
202
  next if basename.start_with?("_")
202
203
 
203
204
  # Determine class name from path
@@ -224,7 +225,7 @@ namespace :htm do
224
225
 
225
226
  htm_dir = File.join(yard_output_dir, "HTM")
226
227
  if Dir.exist?(htm_dir)
227
- Dir.glob(File.join(htm_dir, "*.html")).sort.each do |file|
228
+ Dir.glob(File.join(htm_dir, "*.html")).each do |file|
228
229
  basename = File.basename(file, ".html")
229
230
  next if basename.end_with?("Error")
230
231
  next if basename == "Railtie"
data/lib/tasks/files.rake CHANGED
@@ -12,7 +12,6 @@ namespace :htm do
12
12
  namespace :files do
13
13
  desc "Load a markdown file into long-term memory. Usage: rake htm:files:load[path/to/file.md]"
14
14
  task :load, [:path] do |_t, args|
15
-
16
15
  path = args[:path]
17
16
  unless path
18
17
  puts "Error: File path required."
@@ -31,7 +30,7 @@ namespace :htm do
31
30
  htm = HTM.new(robot_name: "FileLoader")
32
31
  force = ENV['FORCE'] == 'true'
33
32
 
34
- puts "Loading file: #{path}#{force ? ' (force)' : ''}"
33
+ puts "Loading file: #{path}#{' (force)' if force}"
35
34
  result = htm.load_file(path, force: force)
36
35
 
37
36
  if result[:skipped]
@@ -48,7 +47,6 @@ namespace :htm do
48
47
 
49
48
  desc "Load all markdown files from a directory. Usage: rake htm:files:load_dir[path/to/dir]"
50
49
  task :load_dir, [:path, :pattern] do |_t, args|
51
-
52
50
  path = args[:path]
53
51
  unless path
54
52
  puts "Error: Directory path required."
@@ -70,7 +68,7 @@ namespace :htm do
70
68
  force = ENV['FORCE'] == 'true'
71
69
 
72
70
  puts "Loading files from: #{path}"
73
- puts "Pattern: #{pattern}#{force ? ' (force)' : ''}"
71
+ puts "Pattern: #{pattern}#{' (force)' if force}"
74
72
  puts
75
73
 
76
74
  results = htm.load_directory(path, pattern: pattern, force: force)
@@ -102,7 +100,6 @@ namespace :htm do
102
100
 
103
101
  desc "List all loaded file sources"
104
102
  task :list do
105
-
106
103
  # Ensure database connection
107
104
  HTM::SequelConfig.establish_connection!
108
105
 
@@ -133,7 +130,6 @@ namespace :htm do
133
130
 
134
131
  desc "Show details for a loaded file. Usage: rake htm:files:info[path/to/file.md]"
135
132
  task :info, [:path] do |_t, args|
136
-
137
133
  path = args[:path]
138
134
  unless path
139
135
  puts "Error: File path required."
@@ -188,7 +184,6 @@ namespace :htm do
188
184
 
189
185
  desc "Unload a file from memory. Usage: rake htm:files:unload[path/to/file.md]"
190
186
  task :unload, [:path] do |_t, args|
191
-
192
187
  path = args[:path]
193
188
  unless path
194
189
  puts "Error: File path required."
@@ -211,14 +206,13 @@ namespace :htm do
211
206
 
212
207
  desc "Sync all loaded files (reload changed files)"
213
208
  task :sync do
214
-
215
209
  # Ensure database connection
216
210
  HTM::SequelConfig.establish_connection!
217
211
 
218
212
  htm = HTM.new(robot_name: "FileLoader")
219
213
  sources = HTM::Models::FileSource.all
220
214
 
221
- if sources.count.zero?
215
+ if sources.none?
222
216
  puts "No files loaded."
223
217
  next
224
218
  end
@@ -257,7 +251,6 @@ namespace :htm do
257
251
 
258
252
  desc "Show file loading statistics"
259
253
  task :stats do
260
-
261
254
  # Ensure database connection
262
255
  HTM::SequelConfig.establish_connection!
263
256
 
@@ -281,9 +274,9 @@ namespace :htm do
281
274
  puts " Total files loaded: #{total_sources}"
282
275
  puts " Total chunks: #{total_chunks}"
283
276
  puts " Files needing sync: #{needs_sync}"
284
- puts " Missing files: #{missing}" if missing > 0
277
+ puts " Missing files: #{missing}" if missing.positive?
285
278
 
286
- if total_sources > 0
279
+ if total_sources.positive?
287
280
  avg_chunks = (total_chunks.to_f / total_sources).round(1)
288
281
  puts " Average chunks per file: #{avg_chunks}"
289
282
  end
data/lib/tasks/htm.rake CHANGED
@@ -10,7 +10,7 @@
10
10
 
11
11
  namespace :htm do
12
12
  namespace :db do
13
- # Note: Database configuration validation (environment, URL/component reconciliation,
13
+ # NOTE: Database configuration validation (environment, URL/component reconciliation,
14
14
  # naming convention) happens automatically when HTM is required above.
15
15
 
16
16
  desc "Set up HTM database schema and run migrations (set DUMP_SCHEMA=true to auto-dump schema after)"
@@ -35,7 +35,7 @@ namespace :htm do
35
35
  HTM::Database.drop
36
36
  else
37
37
  print "Are you sure you want to drop all tables? This cannot be undone! (yes/no): "
38
- response = STDIN.gets&.chomp
38
+ response = $stdin.gets&.chomp
39
39
  if response&.downcase == 'yes'
40
40
  HTM::Database.drop
41
41
  else
@@ -51,7 +51,7 @@ namespace :htm do
51
51
  HTM::Database.setup(dump_schema: true)
52
52
  else
53
53
  print "Are you sure you want to drop all tables? This cannot be undone! (yes/no): "
54
- response = STDIN.gets&.chomp
54
+ response = $stdin.gets&.chomp
55
55
  if response&.downcase == 'yes'
56
56
  HTM::Database.drop
57
57
  HTM::Database.setup(dump_schema: true)
@@ -103,9 +103,9 @@ namespace :htm do
103
103
 
104
104
  puts "Connecting to #{config[:database]} (#{HTM.env})..."
105
105
  exec "psql", "-h", config[:host],
106
- "-p", config[:port].to_s,
107
- "-U", config[:username],
108
- "-d", config[:database]
106
+ "-p", config[:port].to_s,
107
+ "-U", config[:username],
108
+ "-d", config[:database]
109
109
  end
110
110
 
111
111
  desc "Seed database with sample data"
@@ -129,13 +129,13 @@ namespace :htm do
129
129
  # Define tables with their models and optional extra info
130
130
  tables = [
131
131
  { name: 'robots', model: HTM::Models::Robot },
132
- { name: 'nodes', model: HTM::Models::Node, extras: ->(m) {
132
+ { name: 'nodes', model: HTM::Models::Node, extras: lambda { |m|
133
133
  # Node uses default_scope for active nodes, so m.count is active count
134
134
  active = m.count
135
135
  deleted = m.deleted.count
136
136
  with_embedding = m.exclude(embedding: nil).count
137
137
  " (active: #{active}, deleted: #{deleted}, with embeddings: #{with_embedding})"
138
- }},
138
+ } },
139
139
  { name: 'tags', model: HTM::Models::Tag },
140
140
  { name: 'nodes_tags', model: HTM::Models::NodeTag },
141
141
  { name: 'file_sources', model: HTM::Models::FileSource }
@@ -160,14 +160,14 @@ namespace :htm do
160
160
 
161
161
  # Nodes per robot (via robot_nodes join table)
162
162
  robot_counts = HTM::Models::RobotNode
163
- .join(:nodes, id: :node_id)
164
- .where(Sequel[:nodes][:deleted_at] => nil)
165
- .group_and_count(:robot_id)
166
- .all
167
- .to_h { |row| [row[:robot_id], row[:count]] }
168
- .transform_keys { |id| HTM::Models::Robot[id]&.name || "Unknown (#{id})" }
169
- .sort_by { |_, count| -count }
170
- .first(5)
163
+ .join(:nodes, id: :node_id)
164
+ .where(Sequel[:nodes][:deleted_at] => nil)
165
+ .group_and_count(:robot_id)
166
+ .all
167
+ .to_h { |row| [row[:robot_id], row[:count]] }
168
+ .transform_keys { |id| HTM::Models::Robot[id]&.name || "Unknown (#{id})" }
169
+ .sort_by { |_, count| -count }
170
+ .first(5)
171
171
 
172
172
  if robot_counts.any?
173
173
  puts " Top robots by node count:"
@@ -178,12 +178,12 @@ namespace :htm do
178
178
 
179
179
  # Tag distribution
180
180
  top_root_tags = HTM.db[:tags]
181
- .select(Sequel.lit("split_part(name, ':', 1) as root"), Sequel.function(:count, Sequel.lit('*')).as(:cnt))
182
- .group(Sequel.lit("split_part(name, ':', 1)"))
183
- .order(Sequel.desc(:cnt))
184
- .limit(5)
185
- .all
186
- .map { |t| [t[:root], t[:cnt]] }
181
+ .select(Sequel.lit("split_part(name, ':', 1) as root"), Sequel.function(:count, Sequel.lit('*')).as(:cnt))
182
+ .group(Sequel.lit("split_part(name, ':', 1)"))
183
+ .order(Sequel.desc(:cnt))
184
+ .limit(5)
185
+ .all
186
+ .map { |t| [t[:root], t[:cnt]] }
187
187
 
188
188
  if top_root_tags.any?
189
189
  puts " Top root tag categories:"
@@ -302,7 +302,7 @@ namespace :htm do
302
302
  end
303
303
 
304
304
  # Delete existing proposition nodes
305
- if existing_propositions > 0
305
+ if existing_propositions.positive?
306
306
  puts "\nDeleting #{existing_propositions} existing proposition nodes..."
307
307
  deleted = HTM::Models::Node.propositions.delete
308
308
  puts " Deleted #{deleted} proposition nodes"
@@ -422,7 +422,7 @@ namespace :htm do
422
422
  [db_name]
423
423
  )
424
424
 
425
- if result.ntuples == 0
425
+ if result.ntuples.zero?
426
426
  admin_conn.exec("CREATE DATABASE #{PG::Connection.quote_ident(db_name)}")
427
427
  puts "✓ Database created: #{db_name}"
428
428
 
@@ -461,8 +461,8 @@ namespace :htm do
461
461
 
462
462
  # Step 1: Find active node_tags pointing to soft-deleted or missing nodes
463
463
  stale_node_tags = HTM::Models::NodeTag
464
- .left_join(:nodes, id: :node_id)
465
- .where(Sequel.lit("nodes.id IS NULL OR nodes.deleted_at IS NOT NULL"))
464
+ .left_join(:nodes, id: :node_id)
465
+ .where(Sequel.lit("nodes.id IS NULL OR nodes.deleted_at IS NOT NULL"))
466
466
 
467
467
  stale_count = stale_node_tags.count
468
468
 
@@ -470,7 +470,7 @@ namespace :htm do
470
470
  orphaned_tags = HTM::Models::Tag.orphaned
471
471
  orphan_count = orphaned_tags.count
472
472
 
473
- if stale_count == 0 && orphan_count == 0
473
+ if stale_count.zero? && orphan_count.zero?
474
474
  puts "No cleanup needed."
475
475
  puts " Stale node_tags entries: 0"
476
476
  puts " Orphaned tags: 0"
@@ -481,7 +481,7 @@ namespace :htm do
481
481
  puts " Stale node_tags entries: #{stale_count} (pointing to deleted/missing nodes)"
482
482
  puts " Orphaned tags: #{orphan_count} (no active nodes)"
483
483
 
484
- if orphan_count > 0
484
+ if orphan_count.positive?
485
485
  puts "\nOrphaned tags:"
486
486
  orphaned_tags.limit(20).select_map(:name).each do |name|
487
487
  puts " - #{name}"
@@ -500,13 +500,13 @@ namespace :htm do
500
500
  now = Time.now
501
501
 
502
502
  # Soft delete stale node_tags first
503
- if stale_count > 0
503
+ if stale_count.positive?
504
504
  soft_deleted_node_tags = stale_node_tags.update(deleted_at: now)
505
505
  puts "\nSoft deleted #{soft_deleted_node_tags} stale node_tags entries."
506
506
  end
507
507
 
508
508
  # Then soft delete orphaned tags
509
- if orphan_count > 0
509
+ if orphan_count.positive?
510
510
  soft_deleted_tags = orphaned_tags.update(deleted_at: now)
511
511
  puts "Soft deleted #{soft_deleted_tags} orphaned tags."
512
512
  end
@@ -531,44 +531,44 @@ namespace :htm do
531
531
  # Find orphaned propositions (source_node_id no longer exists)
532
532
  # Get all source_node_ids from propositions
533
533
  proposition_source_ids = HTM::Models::Node
534
- .where(Sequel.lit("metadata->>'is_proposition' = ?", 'true'))
535
- .exclude(Sequel.lit("metadata->>'source_node_id' IS NULL"))
536
- .select_map(Sequel.lit("(metadata->>'source_node_id')::integer"))
537
- .uniq
534
+ .where(Sequel.lit("metadata->>'is_proposition' = ?", 'true'))
535
+ .exclude(Sequel.lit("metadata->>'source_node_id' IS NULL"))
536
+ .select_map(Sequel.lit("(metadata->>'source_node_id')::integer"))
537
+ .uniq
538
538
 
539
539
  # Find which source nodes no longer exist (not even soft-deleted)
540
540
  existing_node_ids = HTM::Models::Node.with_deleted
541
- .where(id: proposition_source_ids)
542
- .select_map(:id)
541
+ .where(id: proposition_source_ids)
542
+ .select_map(:id)
543
543
 
544
544
  missing_source_ids = proposition_source_ids - existing_node_ids
545
545
 
546
546
  orphaned_propositions = if missing_source_ids.any?
547
- HTM::Models::Node
548
- .where(Sequel.lit("metadata->>'is_proposition' = ?", 'true'))
549
- .where(Sequel.lit("(metadata->>'source_node_id')::integer") => missing_source_ids)
550
- .count
551
- else
552
- 0
553
- end
547
+ HTM::Models::Node
548
+ .where(Sequel.lit("metadata->>'is_proposition' = ?", 'true'))
549
+ .where(Sequel.lit("(metadata->>'source_node_id')::integer") => missing_source_ids)
550
+ .count
551
+ else
552
+ 0
553
+ end
554
554
 
555
555
  # Find orphaned join table entries (pointing to non-existent nodes)
556
556
  orphaned_node_tags = HTM::Models::NodeTag.with_deleted
557
- .left_join(:nodes, id: :node_id)
558
- .where(Sequel[:nodes][:id] => nil)
559
- .count
557
+ .left_join(:nodes, id: :node_id)
558
+ .where(Sequel[:nodes][:id] => nil)
559
+ .count
560
560
 
561
561
  orphaned_robot_nodes = HTM::Models::RobotNode.with_deleted
562
- .left_join(:nodes, id: :node_id)
563
- .where(Sequel[:nodes][:id] => nil)
564
- .count
562
+ .left_join(:nodes, id: :node_id)
563
+ .where(Sequel[:nodes][:id] => nil)
564
+ .count
565
565
 
566
566
  # Find orphaned robots (no active memory nodes)
567
567
  orphaned_robots = HTM::Models::Robot
568
- .where(Sequel.~(Sequel.exists(
569
- HTM::Models::RobotNode.where(Sequel[:robot_nodes][:robot_id] => Sequel[:robots][:id]).select(1)
570
- )))
571
- .count
568
+ .where(Sequel.~(Sequel.exists(
569
+ HTM::Models::RobotNode.where(Sequel[:robot_nodes][:robot_id] => Sequel[:robots][:id]).select(1)
570
+ )))
571
+ .count
572
572
 
573
573
  # Display record counts by table
574
574
  puts "\nSoft-deleted records by table:"
@@ -585,10 +585,10 @@ namespace :htm do
585
585
  total_to_delete = deleted_nodes + deleted_node_tags + deleted_robot_nodes +
586
586
  orphaned_propositions + orphaned_node_tags + orphaned_robot_nodes + orphaned_robots
587
587
 
588
- puts " " + "-" * 40
588
+ puts " " + ("-" * 40)
589
589
  puts " %-20s %8d" % ['Total', total_to_delete]
590
590
 
591
- if total_to_delete == 0
591
+ if total_to_delete.zero?
592
592
  puts "\nNo records to purge."
593
593
  next
594
594
  end
@@ -614,9 +614,9 @@ namespace :htm do
614
614
  # Step 1: Delete orphaned propositions (source_node_id no longer exists)
615
615
  if missing_source_ids.any?
616
616
  purged[:orphaned_propositions] = HTM::Models::Node
617
- .where(Sequel.lit("metadata->>'is_proposition' = ?", 'true'))
618
- .where(Sequel.lit("(metadata->>'source_node_id')::integer") => missing_source_ids)
619
- .delete
617
+ .where(Sequel.lit("metadata->>'is_proposition' = ?", 'true'))
618
+ .where(Sequel.lit("(metadata->>'source_node_id')::integer") => missing_source_ids)
619
+ .delete
620
620
  else
621
621
  purged[:orphaned_propositions] = 0
622
622
  end
@@ -624,9 +624,9 @@ namespace :htm do
624
624
  # Step 2: Delete orphaned node_tags (pointing to non-existent nodes)
625
625
  # This now includes entries from deleted propositions
626
626
  orphaned_nt_ids = HTM::Models::NodeTag.with_deleted
627
- .left_join(:nodes, id: :node_id)
628
- .where(Sequel[:nodes][:id] => nil)
629
- .select_map(Sequel[:node_tags][:id])
627
+ .left_join(:nodes, id: :node_id)
628
+ .where(Sequel[:nodes][:id] => nil)
629
+ .select_map(Sequel[:node_tags][:id])
630
630
  purged[:orphaned_node_tags] = HTM::Models::NodeTag.with_deleted.where(id: orphaned_nt_ids).delete
631
631
 
632
632
  # Step 3: Delete soft-deleted node_tags
@@ -635,9 +635,9 @@ namespace :htm do
635
635
  # Step 4: Delete orphaned robot_nodes (pointing to non-existent nodes)
636
636
  # This now includes entries from deleted propositions
637
637
  orphaned_rn_ids = HTM::Models::RobotNode.with_deleted
638
- .left_join(:nodes, id: :node_id)
639
- .where(Sequel[:nodes][:id] => nil)
640
- .select_map(Sequel[:robot_nodes][:id])
638
+ .left_join(:nodes, id: :node_id)
639
+ .where(Sequel[:nodes][:id] => nil)
640
+ .select_map(Sequel[:robot_nodes][:id])
641
641
  purged[:orphaned_robot_nodes] = HTM::Models::RobotNode.with_deleted.where(id: orphaned_rn_ids).delete
642
642
 
643
643
  # Step 5: Delete soft-deleted robot_nodes
@@ -648,10 +648,10 @@ namespace :htm do
648
648
 
649
649
  # Step 7: Delete orphaned robots (no associated memory nodes)
650
650
  purged[:orphaned_robots] = HTM::Models::Robot
651
- .where(Sequel.~(Sequel.exists(
652
- HTM::Models::RobotNode.where(Sequel[:robot_nodes][:robot_id] => Sequel[:robots][:id]).select(1)
653
- )))
654
- .delete
651
+ .where(Sequel.~(Sequel.exists(
652
+ HTM::Models::RobotNode.where(Sequel[:robot_nodes][:robot_id] => Sequel[:robots][:id]).select(1)
653
+ )))
654
+ .delete
655
655
 
656
656
  puts "\nPurge complete!"
657
657
  puts " Orphaned propositions purged: #{purged[:orphaned_propositions]}"
@@ -661,10 +661,9 @@ namespace :htm do
661
661
  puts " Deleted robot_nodes purged: #{purged[:deleted_robot_nodes]}"
662
662
  puts " Deleted nodes purged: #{purged[:deleted_nodes]}"
663
663
  puts " Orphaned robots purged: #{purged[:orphaned_robots]}"
664
- puts " " + "-" * 40
664
+ puts " " + ("-" * 40)
665
665
  puts " Total records purged: #{purged.values.sum}"
666
666
  end
667
-
668
667
  end
669
668
 
670
669
  namespace :doc do
@@ -699,6 +698,6 @@ namespace :htm do
699
698
  end
700
699
 
701
700
  desc "Generate DB docs, YARD API docs, build site, and serve locally"
702
- task :all => [:db, :yard, :build, :serve]
701
+ task all: %i[db yard build serve]
703
702
  end
704
703
  end