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.
- checksums.yaml +4 -4
- data/.irbrc +2 -3
- data/.rubocop.yml +184 -0
- data/CHANGELOG.md +46 -0
- data/README.md +2 -0
- data/Rakefile +93 -12
- data/db/migrate/00008_create_node_relationships.rb +54 -0
- data/db/migrate/00009_fix_node_relationships_column_types.rb +17 -0
- data/db/schema.sql +124 -1
- data/docs/api/database.md +35 -57
- data/docs/api/embedding-service.md +1 -1
- data/docs/api/index.md +26 -15
- data/docs/api/working-memory.md +8 -8
- data/docs/architecture/index.md +5 -7
- data/docs/architecture/overview.md +5 -8
- data/docs/assets/images/htm-architecture-overview.svg +1 -1
- data/docs/assets/images/htm-context-assembly-flow.svg +2 -2
- data/docs/assets/images/htm-layered-architecture.svg +3 -3
- data/docs/assets/images/two-tier-memory-architecture.svg +1 -1
- data/docs/database/README.md +1 -0
- data/docs/database_rake_tasks.md +20 -28
- data/docs/development/contributing.md +5 -5
- data/docs/development/index.md +4 -7
- data/docs/development/schema.md +71 -1
- data/docs/development/setup.md +40 -82
- data/docs/development/testing.md +1 -1
- data/docs/examples/file-loading.md +4 -4
- data/docs/examples/mcp-client.md +1 -1
- data/docs/getting-started/quick-start.md +4 -4
- data/docs/guides/adding-memories.md +14 -1
- data/docs/guides/configuration.md +5 -5
- data/docs/guides/context-assembly.md +4 -4
- data/docs/guides/file-loading.md +12 -12
- data/docs/guides/getting-started.md +2 -2
- data/docs/guides/long-term-memory.md +7 -27
- data/docs/guides/propositions.md +20 -19
- data/docs/guides/recalling-memories.md +5 -5
- data/docs/guides/tags.md +18 -13
- data/docs/multi_framework_support.md +1 -1
- data/docs/robots/hive-mind.md +1 -1
- data/docs/robots/multi-robot.md +2 -2
- data/docs/robots/robot-groups.md +1 -1
- data/docs/robots/two-tier-memory.md +72 -94
- data/docs/setup_local_database.md +8 -54
- data/docs/using_rake_tasks_in_your_app.md +6 -6
- data/examples/01_basic_usage.rb +1 -0
- data/examples/03_custom_llm_configuration.rb +1 -0
- data/examples/04_file_loader_usage.rb +1 -0
- data/examples/05_timeframe_demo.rb +1 -0
- data/examples/06_example_app/app.rb +1 -0
- data/examples/07_cli_app/htm_cli.rb +1 -0
- data/examples/09_mcp_client.rb +1 -0
- data/examples/10_telemetry/demo.rb +1 -0
- data/examples/11_robot_groups/multi_process.rb +1 -0
- data/examples/11_robot_groups/same_process.rb +1 -0
- data/examples/12_rails_app/.envrc +12 -0
- data/examples/12_rails_app/Gemfile +8 -3
- data/examples/12_rails_app/Gemfile.lock +94 -89
- data/examples/12_rails_app/README.md +70 -19
- data/examples/12_rails_app/app/controllers/application_controller.rb +6 -0
- data/examples/12_rails_app/app/controllers/chats_controller.rb +305 -0
- data/examples/12_rails_app/app/controllers/dashboard_controller.rb +3 -0
- data/examples/12_rails_app/app/controllers/files_controller.rb +17 -2
- data/examples/12_rails_app/app/controllers/home_controller.rb +8 -0
- data/examples/12_rails_app/app/controllers/memories_controller.rb +9 -4
- data/examples/12_rails_app/app/controllers/messages_controller.rb +214 -0
- data/examples/12_rails_app/app/controllers/robots_controller.rb +11 -1
- data/examples/12_rails_app/app/controllers/tags_controller.rb +14 -1
- data/examples/12_rails_app/app/javascript/application.js +1 -1
- data/examples/12_rails_app/app/models/application_record.rb +5 -0
- data/examples/12_rails_app/app/models/chat.rb +36 -0
- data/examples/12_rails_app/app/models/message.rb +5 -0
- data/examples/12_rails_app/app/models/model.rb +5 -0
- data/examples/12_rails_app/app/models/tool_call.rb +5 -0
- data/examples/12_rails_app/app/views/chats/index.html.erb +61 -0
- data/examples/12_rails_app/app/views/chats/show.html.erb +213 -0
- data/examples/12_rails_app/app/views/dashboard/index.html.erb +3 -0
- data/examples/12_rails_app/app/views/files/index.html.erb +10 -5
- data/examples/12_rails_app/app/views/files/new.html.erb +4 -2
- data/examples/12_rails_app/app/views/files/show.html.erb +19 -3
- data/examples/12_rails_app/app/views/home/index.html.erb +45 -0
- data/examples/12_rails_app/app/views/layouts/application.html.erb +20 -18
- data/examples/12_rails_app/app/views/memories/_memory_card.html.erb +1 -1
- data/examples/12_rails_app/app/views/memories/deleted.html.erb +3 -1
- data/examples/12_rails_app/app/views/memories/edit.html.erb +2 -0
- data/examples/12_rails_app/app/views/memories/index.html.erb +2 -0
- data/examples/12_rails_app/app/views/memories/new.html.erb +2 -0
- data/examples/12_rails_app/app/views/memories/show.html.erb +4 -2
- data/examples/12_rails_app/app/views/messages/_message.html.erb +20 -0
- data/examples/12_rails_app/app/views/robots/index.html.erb +2 -0
- data/examples/12_rails_app/app/views/robots/new.html.erb +2 -0
- data/examples/12_rails_app/app/views/robots/show.html.erb +2 -0
- data/examples/12_rails_app/app/views/search/index.html.erb +59 -8
- data/examples/12_rails_app/app/views/shared/_navbar.html.erb +75 -29
- data/examples/12_rails_app/app/views/tags/index.html.erb +2 -0
- data/examples/12_rails_app/app/views/tags/show.html.erb +3 -1
- data/examples/12_rails_app/config/application.rb +1 -1
- data/examples/12_rails_app/config/database.yml +9 -5
- data/examples/12_rails_app/config/importmap.rb +1 -1
- data/examples/12_rails_app/config/initializers/htm.rb +9 -2
- data/examples/12_rails_app/config/initializers/ruby_llm.rb +33 -0
- data/examples/12_rails_app/config/routes.rb +39 -23
- data/examples/12_rails_app/db/migrate/20250124000001_create_ruby_llm_tables.rb +34 -0
- data/examples/12_rails_app/db/migrate/20250124000002_create_models_table.rb +28 -0
- data/examples/12_rails_app/db/schema.rb +67 -0
- data/examples/examples_helper.rb +25 -0
- data/lib/htm/circuit_breaker.rb +5 -6
- data/lib/htm/config/builder.rb +12 -12
- data/lib/htm/config/database.rb +21 -27
- data/lib/htm/config/defaults.yml +25 -13
- data/lib/htm/config/validator.rb +12 -18
- data/lib/htm/config.rb +93 -173
- data/lib/htm/database.rb +193 -199
- data/lib/htm/embedding_service.rb +4 -9
- data/lib/htm/integrations/sinatra.rb +7 -7
- data/lib/htm/job_adapter.rb +14 -21
- data/lib/htm/jobs/generate_embedding_job.rb +28 -44
- data/lib/htm/jobs/generate_propositions_job.rb +29 -55
- data/lib/htm/jobs/generate_relationships_job.rb +137 -0
- data/lib/htm/jobs/generate_tags_job.rb +45 -67
- data/lib/htm/loaders/markdown_loader.rb +65 -112
- data/lib/htm/long_term_memory/fulltext_search.rb +1 -1
- data/lib/htm/long_term_memory/hybrid_search.rb +300 -128
- data/lib/htm/long_term_memory/node_operations.rb +2 -2
- data/lib/htm/long_term_memory/relevance_scorer.rb +100 -68
- data/lib/htm/long_term_memory/tag_operations.rb +87 -120
- data/lib/htm/long_term_memory/vector_search.rb +1 -1
- data/lib/htm/long_term_memory.rb +2 -1
- data/lib/htm/mcp/cli.rb +59 -58
- data/lib/htm/mcp/server.rb +5 -6
- data/lib/htm/mcp/tools.rb +30 -36
- data/lib/htm/migration.rb +10 -10
- data/lib/htm/models/node.rb +2 -3
- data/lib/htm/models/node_relationship.rb +72 -0
- data/lib/htm/models/node_tag.rb +2 -2
- data/lib/htm/models/robot_node.rb +2 -2
- data/lib/htm/models/tag.rb +41 -28
- data/lib/htm/observability.rb +45 -51
- data/lib/htm/proposition_service.rb +3 -7
- data/lib/htm/query_cache.rb +13 -15
- data/lib/htm/railtie.rb +1 -2
- data/lib/htm/robot_group.rb +9 -9
- data/lib/htm/sequel_config.rb +1 -0
- data/lib/htm/sql_builder.rb +1 -1
- data/lib/htm/tag_service.rb +2 -6
- data/lib/htm/timeframe.rb +4 -5
- data/lib/htm/timeframe_extractor.rb +42 -83
- data/lib/htm/version.rb +1 -1
- data/lib/htm/workflows/remember_workflow.rb +112 -115
- data/lib/htm/working_memory.rb +21 -26
- data/lib/htm.rb +103 -116
- data/lib/tasks/db.rake +0 -2
- data/lib/tasks/doc.rake +14 -13
- data/lib/tasks/files.rake +5 -12
- data/lib/tasks/htm.rake +70 -71
- data/lib/tasks/jobs.rake +41 -47
- data/lib/tasks/tags.rake +3 -8
- metadata +28 -106
- data/lib/htm/config/section.rb +0 -74
- data/lib/htm/loaders/defaults_loader.rb +0 -166
- 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)
|
|
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
|
|
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!(
|
|
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!(
|
|
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!(
|
|
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
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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")).
|
|
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
|
|
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")).
|
|
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}#{
|
|
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}#{
|
|
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.
|
|
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
|
|
277
|
+
puts " Missing files: #{missing}" if missing.positive?
|
|
285
278
|
|
|
286
|
-
if total_sources
|
|
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
|
-
#
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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:
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
465
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
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
|
-
|
|
542
|
-
|
|
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
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
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
|
-
|
|
558
|
-
|
|
559
|
-
|
|
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
|
-
|
|
563
|
-
|
|
564
|
-
|
|
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
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
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
|
|
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
|
-
|
|
618
|
-
|
|
619
|
-
|
|
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
|
-
|
|
628
|
-
|
|
629
|
-
|
|
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
|
-
|
|
639
|
-
|
|
640
|
-
|
|
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
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
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 :
|
|
701
|
+
task all: %i[db yard build serve]
|
|
703
702
|
end
|
|
704
703
|
end
|