htm 0.0.1 → 0.0.10

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 (184) hide show
  1. checksums.yaml +4 -4
  2. data/.aigcm_msg +1 -0
  3. data/.architecture/reviews/comprehensive-codebase-review.md +577 -0
  4. data/.claude/settings.local.json +92 -0
  5. data/.envrc +1 -0
  6. data/.irbrc +283 -80
  7. data/.tbls.yml +31 -0
  8. data/CHANGELOG.md +314 -16
  9. data/CLAUDE.md +603 -0
  10. data/README.md +76 -5
  11. data/Rakefile +5 -0
  12. data/SETUP.md +132 -101
  13. data/db/migrate/{20250101000001_enable_extensions.rb → 00001_enable_extensions.rb} +0 -1
  14. data/db/migrate/00002_create_robots.rb +11 -0
  15. data/db/migrate/00003_create_file_sources.rb +20 -0
  16. data/db/migrate/00004_create_nodes.rb +65 -0
  17. data/db/migrate/00005_create_tags.rb +13 -0
  18. data/db/migrate/00006_create_node_tags.rb +18 -0
  19. data/db/migrate/00007_create_robot_nodes.rb +26 -0
  20. data/db/migrate/00009_add_working_memory_to_robot_nodes.rb +12 -0
  21. data/db/schema.sql +390 -36
  22. data/docs/api/database.md +19 -232
  23. data/docs/api/embedding-service.md +1 -7
  24. data/docs/api/htm.md +305 -364
  25. data/docs/api/index.md +1 -7
  26. data/docs/api/long-term-memory.md +342 -590
  27. data/docs/api/yard/HTM/ActiveRecordConfig.md +23 -0
  28. data/docs/api/yard/HTM/AuthorizationError.md +11 -0
  29. data/docs/api/yard/HTM/CircuitBreaker.md +92 -0
  30. data/docs/api/yard/HTM/CircuitBreakerOpenError.md +34 -0
  31. data/docs/api/yard/HTM/Configuration.md +175 -0
  32. data/docs/api/yard/HTM/Database.md +99 -0
  33. data/docs/api/yard/HTM/DatabaseError.md +14 -0
  34. data/docs/api/yard/HTM/EmbeddingError.md +18 -0
  35. data/docs/api/yard/HTM/EmbeddingService.md +58 -0
  36. data/docs/api/yard/HTM/Error.md +11 -0
  37. data/docs/api/yard/HTM/JobAdapter.md +39 -0
  38. data/docs/api/yard/HTM/LongTermMemory.md +342 -0
  39. data/docs/api/yard/HTM/NotFoundError.md +17 -0
  40. data/docs/api/yard/HTM/Observability.md +107 -0
  41. data/docs/api/yard/HTM/QueryTimeoutError.md +19 -0
  42. data/docs/api/yard/HTM/Railtie.md +27 -0
  43. data/docs/api/yard/HTM/ResourceExhaustedError.md +13 -0
  44. data/docs/api/yard/HTM/TagError.md +18 -0
  45. data/docs/api/yard/HTM/TagService.md +67 -0
  46. data/docs/api/yard/HTM/Timeframe/Result.md +24 -0
  47. data/docs/api/yard/HTM/Timeframe.md +40 -0
  48. data/docs/api/yard/HTM/TimeframeExtractor/Result.md +24 -0
  49. data/docs/api/yard/HTM/TimeframeExtractor.md +45 -0
  50. data/docs/api/yard/HTM/ValidationError.md +20 -0
  51. data/docs/api/yard/HTM/WorkingMemory.md +131 -0
  52. data/docs/api/yard/HTM.md +80 -0
  53. data/docs/api/yard/index.csv +179 -0
  54. data/docs/api/yard-reference.md +51 -0
  55. data/docs/architecture/adrs/001-postgresql-timescaledb.md +1 -1
  56. data/docs/architecture/adrs/003-ollama-embeddings.md +1 -1
  57. data/docs/architecture/adrs/010-redis-working-memory-rejected.md +2 -27
  58. data/docs/architecture/adrs/index.md +2 -13
  59. data/docs/architecture/hive-mind.md +165 -166
  60. data/docs/architecture/index.md +2 -2
  61. data/docs/architecture/overview.md +5 -171
  62. data/docs/architecture/two-tier-memory.md +1 -35
  63. data/docs/assets/images/adr-010-current-architecture.svg +37 -0
  64. data/docs/assets/images/adr-010-proposed-architecture.svg +48 -0
  65. data/docs/assets/images/adr-dependency-tree.svg +93 -0
  66. data/docs/assets/images/class-hierarchy.svg +55 -0
  67. data/docs/assets/images/exception-hierarchy.svg +45 -0
  68. data/docs/assets/images/htm-architecture-overview.svg +83 -0
  69. data/docs/assets/images/htm-complete-memory-flow.svg +160 -0
  70. data/docs/assets/images/htm-context-assembly-flow.svg +148 -0
  71. data/docs/assets/images/htm-eviction-process.svg +141 -0
  72. data/docs/assets/images/htm-memory-addition-flow.svg +138 -0
  73. data/docs/assets/images/htm-memory-recall-flow.svg +152 -0
  74. data/docs/assets/images/htm-node-states.svg +123 -0
  75. data/docs/assets/images/project-structure.svg +78 -0
  76. data/docs/assets/images/test-directory-structure.svg +38 -0
  77. data/{dbdoc → docs/database}/README.md +127 -125
  78. data/docs/database/public.file_sources.md +42 -0
  79. data/docs/database/public.file_sources.svg +211 -0
  80. data/{dbdoc → docs/database}/public.node_tags.md +7 -8
  81. data/docs/database/public.node_tags.svg +239 -0
  82. data/{dbdoc → docs/database}/public.nodes.md +22 -17
  83. data/docs/database/public.nodes.svg +271 -0
  84. data/docs/database/public.robot_nodes.md +46 -0
  85. data/docs/database/public.robot_nodes.svg +243 -0
  86. data/{dbdoc → docs/database}/public.robots.md +2 -3
  87. data/docs/database/public.robots.svg +161 -0
  88. data/docs/database/public.tags.svg +139 -0
  89. data/{dbdoc → docs/database}/schema.json +941 -630
  90. data/docs/database/schema.svg +282 -0
  91. data/docs/development/index.md +1 -29
  92. data/docs/development/schema.md +134 -309
  93. data/docs/development/testing.md +1 -9
  94. data/docs/getting-started/index.md +47 -0
  95. data/docs/{installation.md → getting-started/installation.md} +2 -2
  96. data/docs/{quick-start.md → getting-started/quick-start.md} +5 -5
  97. data/docs/guides/adding-memories.md +295 -643
  98. data/docs/guides/recalling-memories.md +36 -1
  99. data/docs/guides/search-strategies.md +85 -51
  100. data/docs/images/htm-er-diagram.svg +156 -0
  101. data/docs/index.md +16 -31
  102. data/docs/multi_framework_support.md +4 -4
  103. data/examples/README.md +280 -0
  104. data/examples/basic_usage.rb +18 -16
  105. data/examples/cli_app/htm_cli.rb +146 -8
  106. data/examples/cli_app/temp.log +93 -0
  107. data/examples/custom_llm_configuration.rb +1 -2
  108. data/examples/example_app/app.rb +11 -14
  109. data/examples/file_loader_usage.rb +177 -0
  110. data/examples/robot_groups/lib/robot_group.rb +419 -0
  111. data/examples/robot_groups/lib/working_memory_channel.rb +140 -0
  112. data/examples/robot_groups/multi_process.rb +286 -0
  113. data/examples/robot_groups/robot_worker.rb +136 -0
  114. data/examples/robot_groups/same_process.rb +229 -0
  115. data/examples/sinatra_app/Gemfile +1 -0
  116. data/examples/sinatra_app/Gemfile.lock +166 -0
  117. data/examples/sinatra_app/app.rb +219 -24
  118. data/examples/timeframe_demo.rb +276 -0
  119. data/lib/htm/active_record_config.rb +10 -3
  120. data/lib/htm/circuit_breaker.rb +202 -0
  121. data/lib/htm/configuration.rb +313 -80
  122. data/lib/htm/database.rb +67 -36
  123. data/lib/htm/embedding_service.rb +39 -2
  124. data/lib/htm/errors.rb +131 -11
  125. data/lib/htm/{sinatra.rb → integrations/sinatra.rb} +87 -12
  126. data/lib/htm/job_adapter.rb +10 -3
  127. data/lib/htm/jobs/generate_embedding_job.rb +5 -4
  128. data/lib/htm/jobs/generate_tags_job.rb +4 -0
  129. data/lib/htm/loaders/markdown_loader.rb +263 -0
  130. data/lib/htm/loaders/paragraph_chunker.rb +112 -0
  131. data/lib/htm/long_term_memory.rb +601 -321
  132. data/lib/htm/models/file_source.rb +99 -0
  133. data/lib/htm/models/node.rb +116 -12
  134. data/lib/htm/models/robot.rb +53 -4
  135. data/lib/htm/models/robot_node.rb +51 -0
  136. data/lib/htm/models/tag.rb +302 -0
  137. data/lib/htm/observability.rb +395 -0
  138. data/lib/htm/tag_service.rb +60 -3
  139. data/lib/htm/tasks.rb +29 -0
  140. data/lib/htm/timeframe.rb +194 -0
  141. data/lib/htm/timeframe_extractor.rb +307 -0
  142. data/lib/htm/version.rb +1 -1
  143. data/lib/htm/working_memory.rb +165 -70
  144. data/lib/htm.rb +352 -133
  145. data/lib/tasks/doc.rake +300 -0
  146. data/lib/tasks/files.rake +299 -0
  147. data/lib/tasks/htm.rake +188 -2
  148. data/lib/tasks/jobs.rake +10 -12
  149. data/lib/tasks/tags.rake +194 -0
  150. data/mkdocs.yml +91 -9
  151. data/notes/ARCHITECTURE_REVIEW.md +1167 -0
  152. data/notes/IMPLEMENTATION_SUMMARY.md +606 -0
  153. data/notes/MULTI_FRAMEWORK_IMPLEMENTATION.md +451 -0
  154. data/notes/next_steps.md +100 -0
  155. data/notes/plan.md +627 -0
  156. data/notes/tag_ontology_enhancement_ideas.md +222 -0
  157. data/notes/timescaledb_removal_summary.md +200 -0
  158. metadata +177 -37
  159. data/db/migrate/20250101000002_create_robots.rb +0 -14
  160. data/db/migrate/20250101000003_create_nodes.rb +0 -42
  161. data/db/migrate/20250101000005_create_tags.rb +0 -38
  162. data/db/migrate/20250101000007_add_node_vector_indexes.rb +0 -30
  163. data/dbdoc/public.node_tags.svg +0 -112
  164. data/dbdoc/public.nodes.svg +0 -118
  165. data/dbdoc/public.robots.svg +0 -90
  166. data/dbdoc/public.tags.svg +0 -60
  167. data/dbdoc/schema.svg +0 -154
  168. data/{dbdoc → docs/database}/public.node_stats.md +0 -0
  169. data/{dbdoc → docs/database}/public.node_stats.svg +0 -0
  170. data/{dbdoc → docs/database}/public.nodes_tags.md +0 -0
  171. data/{dbdoc → docs/database}/public.nodes_tags.svg +0 -0
  172. data/{dbdoc → docs/database}/public.ontology_structure.md +0 -0
  173. data/{dbdoc → docs/database}/public.ontology_structure.svg +0 -0
  174. data/{dbdoc → docs/database}/public.operations_log.md +0 -0
  175. data/{dbdoc → docs/database}/public.operations_log.svg +0 -0
  176. data/{dbdoc → docs/database}/public.relationships.md +0 -0
  177. data/{dbdoc → docs/database}/public.relationships.svg +0 -0
  178. data/{dbdoc → docs/database}/public.robot_activity.md +0 -0
  179. data/{dbdoc → docs/database}/public.robot_activity.svg +0 -0
  180. data/{dbdoc → docs/database}/public.schema_migrations.md +0 -0
  181. data/{dbdoc → docs/database}/public.schema_migrations.svg +0 -0
  182. data/{dbdoc → docs/database}/public.tags.md +3 -3
  183. /data/{dbdoc → docs/database}/public.topic_relationships.md +0 -0
  184. /data/{dbdoc → docs/database}/public.topic_relationships.svg +0 -0
@@ -29,10 +29,28 @@ class HTM
29
29
  scope :root_level, -> { where("name NOT LIKE '%:%'") }
30
30
 
31
31
  # Class methods
32
+
33
+ # Find tags with a given prefix (hierarchical query)
34
+ #
35
+ # @param prefix [String] Tag prefix to match (e.g., "database" matches "database:postgresql")
36
+ # @return [ActiveRecord::Relation] Tags matching the prefix
37
+ #
38
+ # @example Find all database-related tags
39
+ # Tag.find_by_topic_prefix("database")
40
+ # # => [#<Tag name: "database:postgresql">, #<Tag name: "database:mysql">]
41
+ #
32
42
  def self.find_by_topic_prefix(prefix)
33
43
  where("name LIKE ?", "#{prefix}%")
34
44
  end
35
45
 
46
+ # Get the most frequently used tags
47
+ #
48
+ # @param limit [Integer] Maximum number of tags to return (default: 10)
49
+ # @return [ActiveRecord::Relation] Tags with usage_count attribute
50
+ #
51
+ # @example Get top 5 most used tags
52
+ # Tag.popular_tags(5).each { |t| puts "#{t.name}: #{t.usage_count}" }
53
+ #
36
54
  def self.popular_tags(limit = 10)
37
55
  joins(:node_tags)
38
56
  .select('tags.*, COUNT(node_tags.id) as usage_count')
@@ -41,27 +59,311 @@ class HTM
41
59
  .limit(limit)
42
60
  end
43
61
 
62
+ # Find or create a tag by name
63
+ #
64
+ # @param name [String] Hierarchical tag name (e.g., "database:postgresql")
65
+ # @return [Tag] The found or created tag
66
+ #
44
67
  def self.find_or_create_by_name(name)
45
68
  find_or_create_by(name: name)
46
69
  end
47
70
 
71
+ # Returns a nested hash tree structure from the current scope
72
+ #
73
+ # @return [Hash] Nested hash representing the tag hierarchy
74
+ #
75
+ # @example Get all tags as a tree
76
+ # Tag.all.tree
77
+ # # => { "database" => { "postgresql" => {}, "mysql" => {} }, "ai" => { "llm" => {} } }
78
+ #
79
+ # @example Get filtered tags as a tree
80
+ # Tag.with_prefix("database").tree
81
+ # # => { "database" => { "postgresql" => {} } }
82
+ #
83
+ def self.tree
84
+ tree = {}
85
+
86
+ all.order(:name).pluck(:name).each do |tag_name|
87
+ parts = tag_name.split(':')
88
+ current = tree
89
+
90
+ parts.each do |part|
91
+ current[part] ||= {}
92
+ current = current[part]
93
+ end
94
+ end
95
+
96
+ tree
97
+ end
98
+
99
+ # Returns a formatted string representation of the tag tree
100
+ #
101
+ # Uses directory-style formatting with ├── and └── characters
102
+ #
103
+ # @return [String] Formatted tree string
104
+ #
105
+ # @example Display all tags as a tree
106
+ # puts Tag.all.tree_string
107
+ # # ├── ai
108
+ # # │ └── llm
109
+ # # └── database
110
+ # # └── postgresql
111
+ #
112
+ def self.tree_string
113
+ format_tree_branch(tree)
114
+ end
115
+
116
+ # Returns a Mermaid flowchart representation of the tag tree
117
+ # Example: puts Tag.all.tree_mermaid
118
+ # Example: Tag.all.tree_mermaid(direction: 'LR') # Left to right
119
+ #
120
+ # @param direction [String] Flow direction: 'TD' (top-down), 'LR' (left-right), 'BT', 'RL'
121
+ # @return [String] Mermaid flowchart syntax
122
+ #
123
+ def self.tree_mermaid(direction: 'TD')
124
+ tree_data = tree
125
+ return "flowchart #{direction}\n empty[No tags]" if tree_data.empty?
126
+
127
+ lines = ["flowchart #{direction}"]
128
+ node_id = 0
129
+ node_ids = {}
130
+
131
+ # Generate Mermaid nodes and connections
132
+ generate_mermaid_nodes(tree_data, nil, lines, node_ids, node_id)
133
+
134
+ lines.join("\n")
135
+ end
136
+
137
+ # Returns an SVG representation of the tag tree
138
+ # Uses dark theme with transparent background
139
+ # Example: File.write('tags.svg', Tag.all.tree_svg)
140
+ #
141
+ # @param title [String] Optional title for the SVG
142
+ # @return [String] SVG markup
143
+ #
144
+ def self.tree_svg(title: 'HTM Tag Hierarchy')
145
+ tree_data = tree
146
+ return empty_tree_svg(title) if tree_data.empty?
147
+
148
+ # Calculate dimensions based on tree structure
149
+ stats = calculate_tree_stats(tree_data)
150
+ node_count = stats[:total_nodes]
151
+ max_depth = stats[:max_depth]
152
+
153
+ # Layout constants
154
+ node_width = 140
155
+ node_height = 30
156
+ h_spacing = 180
157
+ v_spacing = 50
158
+ padding = 40
159
+
160
+ # Calculate positions for all nodes
161
+ positions = {}
162
+ y_offset = [0] # Use array to allow mutation in closure
163
+ calculate_node_positions(tree_data, 0, positions, y_offset, h_spacing, v_spacing)
164
+
165
+ # Calculate SVG dimensions
166
+ width = (max_depth * h_spacing) + node_width + (padding * 2)
167
+ height = (y_offset[0] * v_spacing) + node_height + (padding * 2)
168
+
169
+ # Generate SVG
170
+ generate_tree_svg(tree_data, positions, width, height, padding, node_width, node_height, title)
171
+ end
172
+
173
+ # Format a tree branch recursively (internal helper)
174
+ def self.format_tree_branch(node, is_last_array = [])
175
+ result = ''
176
+ sorted_keys = node.keys.sort
177
+
178
+ sorted_keys.each_with_index do |key, index|
179
+ is_last = (index == sorted_keys.size - 1)
180
+
181
+ # Build prefix from parent branches
182
+ line_prefix = is_last_array.map { |was_last| was_last ? ' ' : '│ ' }.join
183
+
184
+ # Add branch character and key
185
+ branch = is_last ? '└── ' : '├── '
186
+ result += "#{line_prefix}#{branch}#{key}\n"
187
+
188
+ # Recurse into children
189
+ children = node[key]
190
+ unless children.empty?
191
+ result += format_tree_branch(children, is_last_array + [is_last])
192
+ end
193
+ end
194
+
195
+ result
196
+ end
197
+
198
+ # Generate Mermaid nodes recursively (internal helper)
199
+ def self.generate_mermaid_nodes(node, parent_path, lines, node_ids, counter)
200
+ node.keys.sort.each do |key|
201
+ current_path = parent_path ? "#{parent_path}:#{key}" : key
202
+
203
+ # Create unique node ID
204
+ node_id = "n#{counter}"
205
+ node_ids[current_path] = node_id
206
+ counter += 1
207
+
208
+ # Add node definition with styling
209
+ lines << " #{node_id}[\"#{key}\"]"
210
+
211
+ # Add connection from parent
212
+ if parent_path && node_ids[parent_path]
213
+ lines << " #{node_ids[parent_path]} --> #{node_id}"
214
+ end
215
+
216
+ # Recurse into children
217
+ children = node[key]
218
+ counter = generate_mermaid_nodes(children, current_path, lines, node_ids, counter) unless children.empty?
219
+ end
220
+
221
+ counter
222
+ end
223
+
224
+ # Calculate tree statistics (internal helper)
225
+ def self.calculate_tree_stats(node, depth = 0)
226
+ return { total_nodes: 0, max_depth: depth } if node.empty?
227
+
228
+ total = node.keys.size
229
+ max = depth + 1
230
+
231
+ node.each_value do |children|
232
+ child_stats = calculate_tree_stats(children, depth + 1)
233
+ total += child_stats[:total_nodes]
234
+ max = [max, child_stats[:max_depth]].max
235
+ end
236
+
237
+ { total_nodes: total, max_depth: max }
238
+ end
239
+
240
+ # Calculate node positions for SVG layout (internal helper)
241
+ def self.calculate_node_positions(node, depth, positions, y_offset, h_spacing, v_spacing, parent_path = nil)
242
+ node.keys.sort.each do |key|
243
+ current_path = parent_path ? "#{parent_path}:#{key}" : key
244
+
245
+ positions[current_path] = {
246
+ x: depth,
247
+ y: y_offset[0],
248
+ label: key
249
+ }
250
+ y_offset[0] += 1
251
+
252
+ children = node[key]
253
+ calculate_node_positions(children, depth + 1, positions, y_offset, h_spacing, v_spacing, current_path) unless children.empty?
254
+ end
255
+ end
256
+
257
+ # Generate SVG for empty tree (internal helper)
258
+ def self.empty_tree_svg(title)
259
+ <<~SVG
260
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 100">
261
+ <rect width="100%" height="100%" fill="transparent"/>
262
+ <text x="150" y="30" text-anchor="middle" fill="#9CA3AF" font-family="system-ui, sans-serif" font-size="14" font-weight="bold">#{title}</text>
263
+ <text x="150" y="60" text-anchor="middle" fill="#6B7280" font-family="system-ui, sans-serif" font-size="12">No tags found</text>
264
+ </svg>
265
+ SVG
266
+ end
267
+
268
+ # Generate SVG tree visualization (internal helper)
269
+ def self.generate_tree_svg(tree_data, positions, width, height, padding, node_width, node_height, title)
270
+ # Color palette for different depths (dark theme)
271
+ colors = ['#3B82F6', '#8B5CF6', '#EC4899', '#F59E0B', '#10B981', '#6366F1']
272
+
273
+ svg_lines = []
274
+ svg_lines << %(<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 #{width} #{height + 40}">)
275
+ svg_lines << ' <rect width="100%" height="100%" fill="transparent"/>'
276
+
277
+ # Title
278
+ svg_lines << %Q( <text x="#{width / 2}" y="25" text-anchor="middle" fill="#F3F4F6" font-family="system-ui, sans-serif" font-size="16" font-weight="bold">#{title}</text>)
279
+
280
+ # Draw connections first (so they appear behind nodes)
281
+ positions.each do |path, pos|
282
+ parent_path = path.include?(':') ? path.split(':')[0..-2].join(':') : nil
283
+ next unless parent_path && positions[parent_path]
284
+
285
+ parent_pos = positions[parent_path]
286
+ x1 = padding + (parent_pos[:x] * (node_width + 40)) + node_width
287
+ y1 = 40 + padding + (parent_pos[:y] * (node_height + 20)) + (node_height / 2)
288
+ x2 = padding + (pos[:x] * (node_width + 40))
289
+ y2 = 40 + padding + (pos[:y] * (node_height + 20)) + (node_height / 2)
290
+
291
+ # Curved connection line
292
+ mid_x = (x1 + x2) / 2
293
+ svg_lines << %Q( <path d="M#{x1},#{y1} C#{mid_x},#{y1} #{mid_x},#{y2} #{x2},#{y2}" stroke="#4B5563" stroke-width="2" fill="none"/>)
294
+ end
295
+
296
+ # Draw nodes
297
+ positions.each do |path, pos|
298
+ depth = path.count(':')
299
+ color = colors[depth % colors.size]
300
+
301
+ x = padding + (pos[:x] * (node_width + 40))
302
+ y = 40 + padding + (pos[:y] * (node_height + 20))
303
+
304
+ # Node rectangle with rounded corners
305
+ svg_lines << %Q( <rect x="#{x}" y="#{y}" width="#{node_width}" height="#{node_height}" rx="6" fill="#{color}" opacity="0.9"/>)
306
+
307
+ # Node label
308
+ text_x = x + (node_width / 2)
309
+ text_y = y + (node_height / 2) + 4
310
+ svg_lines << %Q( <text x="#{text_x}" y="#{text_y}" text-anchor="middle" fill="#FFFFFF" font-family="system-ui, sans-serif" font-size="11" font-weight="500">#{pos[:label]}</text>)
311
+ end
312
+
313
+ svg_lines << '</svg>'
314
+ svg_lines.join("\n")
315
+ end
316
+
48
317
  # Instance methods
318
+
319
+ # Get the root (top-level) topic of this tag
320
+ #
321
+ # @return [String] The first segment of the hierarchical tag
322
+ #
323
+ # @example
324
+ # tag = Tag.find_by(name: "database:postgresql:extensions")
325
+ # tag.root_topic # => "database"
326
+ #
49
327
  def root_topic
50
328
  name.split(':').first
51
329
  end
52
330
 
331
+ # Get all hierarchy levels of this tag
332
+ #
333
+ # @return [Array<String>] Array of topic segments
334
+ #
335
+ # @example
336
+ # tag = Tag.find_by(name: "database:postgresql:extensions")
337
+ # tag.topic_levels # => ["database", "postgresql", "extensions"]
338
+ #
53
339
  def topic_levels
54
340
  name.split(':')
55
341
  end
56
342
 
343
+ # Get the depth (number of levels) of this tag
344
+ #
345
+ # @return [Integer] Number of hierarchy levels
346
+ #
347
+ # @example
348
+ # Tag.find_by(name: "database").depth # => 1
349
+ # Tag.find_by(name: "database:postgresql").depth # => 2
350
+ #
57
351
  def depth
58
352
  topic_levels.length
59
353
  end
60
354
 
355
+ # Check if this tag is hierarchical (has child levels)
356
+ #
357
+ # @return [Boolean] True if tag contains colons (hierarchy separators)
358
+ #
61
359
  def hierarchical?
62
360
  name.include?(':')
63
361
  end
64
362
 
363
+ # Get the number of nodes using this tag
364
+ #
365
+ # @return [Integer] Count of nodes with this tag
366
+ #
65
367
  def usage_count
66
368
  node_tags.count
67
369
  end