swarm_sdk 2.7.13 → 3.0.0.alpha1

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 (183) hide show
  1. checksums.yaml +4 -4
  2. data/lib/swarm_sdk/ruby_llm_patches/chat_callbacks_patch.rb +43 -22
  3. data/lib/swarm_sdk/ruby_llm_patches/init.rb +6 -0
  4. data/lib/swarm_sdk/ruby_llm_patches/mcp_ssl_patch.rb +144 -0
  5. data/lib/swarm_sdk/ruby_llm_patches/tool_concurrency_patch.rb +3 -4
  6. data/lib/swarm_sdk/v3/agent.rb +1165 -0
  7. data/lib/swarm_sdk/v3/agent_builder.rb +533 -0
  8. data/lib/swarm_sdk/v3/agent_definition.rb +330 -0
  9. data/lib/swarm_sdk/v3/configuration.rb +490 -0
  10. data/lib/swarm_sdk/v3/debug_log.rb +86 -0
  11. data/lib/swarm_sdk/v3/event_stream.rb +130 -0
  12. data/lib/swarm_sdk/v3/hooks/context.rb +112 -0
  13. data/lib/swarm_sdk/v3/hooks/result.rb +115 -0
  14. data/lib/swarm_sdk/v3/hooks/runner.rb +128 -0
  15. data/lib/swarm_sdk/v3/mcp/connector.rb +183 -0
  16. data/lib/swarm_sdk/v3/mcp/mcp_error.rb +15 -0
  17. data/lib/swarm_sdk/v3/mcp/server_definition.rb +125 -0
  18. data/lib/swarm_sdk/v3/mcp/ssl_http_transport.rb +103 -0
  19. data/lib/swarm_sdk/v3/mcp/stdio_transport.rb +135 -0
  20. data/lib/swarm_sdk/v3/mcp/tool_proxy.rb +53 -0
  21. data/lib/swarm_sdk/v3/memory/adapters/base.rb +297 -0
  22. data/lib/swarm_sdk/v3/memory/adapters/faiss_support.rb +194 -0
  23. data/lib/swarm_sdk/v3/memory/adapters/filesystem_adapter.rb +212 -0
  24. data/lib/swarm_sdk/v3/memory/adapters/sqlite_adapter.rb +507 -0
  25. data/lib/swarm_sdk/v3/memory/adapters/vector_utils.rb +88 -0
  26. data/lib/swarm_sdk/v3/memory/card.rb +206 -0
  27. data/lib/swarm_sdk/v3/memory/cluster.rb +146 -0
  28. data/lib/swarm_sdk/v3/memory/compressor.rb +496 -0
  29. data/lib/swarm_sdk/v3/memory/consolidator.rb +427 -0
  30. data/lib/swarm_sdk/v3/memory/context_builder.rb +339 -0
  31. data/lib/swarm_sdk/v3/memory/edge.rb +105 -0
  32. data/lib/swarm_sdk/v3/memory/embedder.rb +185 -0
  33. data/lib/swarm_sdk/v3/memory/exposure_tracker.rb +104 -0
  34. data/lib/swarm_sdk/v3/memory/ingestion_pipeline.rb +394 -0
  35. data/lib/swarm_sdk/v3/memory/retriever.rb +289 -0
  36. data/lib/swarm_sdk/v3/memory/store.rb +489 -0
  37. data/lib/swarm_sdk/v3/skills/loader.rb +147 -0
  38. data/lib/swarm_sdk/v3/skills/manifest.rb +45 -0
  39. data/lib/swarm_sdk/v3/sub_task_agent.rb +248 -0
  40. data/lib/swarm_sdk/v3/tools/base.rb +80 -0
  41. data/lib/swarm_sdk/v3/tools/bash.rb +174 -0
  42. data/lib/swarm_sdk/v3/tools/clock.rb +32 -0
  43. data/lib/swarm_sdk/v3/tools/edit.rb +111 -0
  44. data/lib/swarm_sdk/v3/tools/glob.rb +96 -0
  45. data/lib/swarm_sdk/v3/tools/grep.rb +200 -0
  46. data/lib/swarm_sdk/v3/tools/message_teammate.rb +15 -0
  47. data/lib/swarm_sdk/v3/tools/message_user.rb +15 -0
  48. data/lib/swarm_sdk/v3/tools/read.rb +181 -0
  49. data/lib/swarm_sdk/v3/tools/read_tracker.rb +40 -0
  50. data/lib/swarm_sdk/v3/tools/registry.rb +208 -0
  51. data/lib/swarm_sdk/v3/tools/sub_task.rb +183 -0
  52. data/lib/swarm_sdk/v3/tools/think.rb +88 -0
  53. data/lib/swarm_sdk/v3/tools/write.rb +87 -0
  54. data/lib/swarm_sdk/v3.rb +145 -0
  55. metadata +84 -148
  56. data/lib/swarm_sdk/agent/RETRY_LOGIC.md +0 -175
  57. data/lib/swarm_sdk/agent/builder.rb +0 -680
  58. data/lib/swarm_sdk/agent/chat.rb +0 -1432
  59. data/lib/swarm_sdk/agent/chat_helpers/context_tracker.rb +0 -375
  60. data/lib/swarm_sdk/agent/chat_helpers/event_emitter.rb +0 -204
  61. data/lib/swarm_sdk/agent/chat_helpers/hook_integration.rb +0 -480
  62. data/lib/swarm_sdk/agent/chat_helpers/instrumentation.rb +0 -85
  63. data/lib/swarm_sdk/agent/chat_helpers/llm_configuration.rb +0 -290
  64. data/lib/swarm_sdk/agent/chat_helpers/logging_helpers.rb +0 -116
  65. data/lib/swarm_sdk/agent/chat_helpers/serialization.rb +0 -83
  66. data/lib/swarm_sdk/agent/chat_helpers/system_reminder_injector.rb +0 -134
  67. data/lib/swarm_sdk/agent/chat_helpers/system_reminders.rb +0 -79
  68. data/lib/swarm_sdk/agent/chat_helpers/token_tracking.rb +0 -146
  69. data/lib/swarm_sdk/agent/context.rb +0 -115
  70. data/lib/swarm_sdk/agent/context_manager.rb +0 -315
  71. data/lib/swarm_sdk/agent/definition.rb +0 -581
  72. data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +0 -226
  73. data/lib/swarm_sdk/agent/system_prompt_builder.rb +0 -161
  74. data/lib/swarm_sdk/agent/tool_registry.rb +0 -189
  75. data/lib/swarm_sdk/agent_registry.rb +0 -146
  76. data/lib/swarm_sdk/builders/base_builder.rb +0 -553
  77. data/lib/swarm_sdk/claude_code_agent_adapter.rb +0 -205
  78. data/lib/swarm_sdk/concerns/cleanupable.rb +0 -39
  79. data/lib/swarm_sdk/concerns/snapshotable.rb +0 -67
  80. data/lib/swarm_sdk/concerns/validatable.rb +0 -55
  81. data/lib/swarm_sdk/config.rb +0 -367
  82. data/lib/swarm_sdk/configuration/parser.rb +0 -397
  83. data/lib/swarm_sdk/configuration/translator.rb +0 -283
  84. data/lib/swarm_sdk/configuration.rb +0 -165
  85. data/lib/swarm_sdk/context_compactor/metrics.rb +0 -147
  86. data/lib/swarm_sdk/context_compactor/token_counter.rb +0 -102
  87. data/lib/swarm_sdk/context_compactor.rb +0 -335
  88. data/lib/swarm_sdk/context_management/builder.rb +0 -128
  89. data/lib/swarm_sdk/context_management/context.rb +0 -328
  90. data/lib/swarm_sdk/custom_tool_registry.rb +0 -226
  91. data/lib/swarm_sdk/defaults.rb +0 -251
  92. data/lib/swarm_sdk/events_to_messages.rb +0 -199
  93. data/lib/swarm_sdk/hooks/adapter.rb +0 -359
  94. data/lib/swarm_sdk/hooks/context.rb +0 -197
  95. data/lib/swarm_sdk/hooks/definition.rb +0 -80
  96. data/lib/swarm_sdk/hooks/error.rb +0 -29
  97. data/lib/swarm_sdk/hooks/executor.rb +0 -146
  98. data/lib/swarm_sdk/hooks/registry.rb +0 -147
  99. data/lib/swarm_sdk/hooks/result.rb +0 -150
  100. data/lib/swarm_sdk/hooks/shell_executor.rb +0 -256
  101. data/lib/swarm_sdk/hooks/tool_call.rb +0 -35
  102. data/lib/swarm_sdk/hooks/tool_result.rb +0 -62
  103. data/lib/swarm_sdk/log_collector.rb +0 -227
  104. data/lib/swarm_sdk/log_stream.rb +0 -127
  105. data/lib/swarm_sdk/markdown_parser.rb +0 -75
  106. data/lib/swarm_sdk/model_aliases.json +0 -8
  107. data/lib/swarm_sdk/models.json +0 -44002
  108. data/lib/swarm_sdk/models.rb +0 -161
  109. data/lib/swarm_sdk/node_context.rb +0 -245
  110. data/lib/swarm_sdk/observer/builder.rb +0 -81
  111. data/lib/swarm_sdk/observer/config.rb +0 -45
  112. data/lib/swarm_sdk/observer/manager.rb +0 -236
  113. data/lib/swarm_sdk/patterns/agent_observer.rb +0 -160
  114. data/lib/swarm_sdk/permissions/config.rb +0 -239
  115. data/lib/swarm_sdk/permissions/error_formatter.rb +0 -121
  116. data/lib/swarm_sdk/permissions/path_matcher.rb +0 -35
  117. data/lib/swarm_sdk/permissions/validator.rb +0 -173
  118. data/lib/swarm_sdk/permissions_builder.rb +0 -122
  119. data/lib/swarm_sdk/plugin.rb +0 -309
  120. data/lib/swarm_sdk/plugin_registry.rb +0 -101
  121. data/lib/swarm_sdk/proc_helpers.rb +0 -53
  122. data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +0 -117
  123. data/lib/swarm_sdk/restore_result.rb +0 -65
  124. data/lib/swarm_sdk/result.rb +0 -212
  125. data/lib/swarm_sdk/snapshot.rb +0 -156
  126. data/lib/swarm_sdk/snapshot_from_events.rb +0 -397
  127. data/lib/swarm_sdk/state_restorer.rb +0 -476
  128. data/lib/swarm_sdk/state_snapshot.rb +0 -334
  129. data/lib/swarm_sdk/swarm/agent_initializer.rb +0 -648
  130. data/lib/swarm_sdk/swarm/all_agents_builder.rb +0 -195
  131. data/lib/swarm_sdk/swarm/builder.rb +0 -256
  132. data/lib/swarm_sdk/swarm/executor.rb +0 -290
  133. data/lib/swarm_sdk/swarm/hook_triggers.rb +0 -151
  134. data/lib/swarm_sdk/swarm/lazy_delegate_chat.rb +0 -372
  135. data/lib/swarm_sdk/swarm/logging_callbacks.rb +0 -360
  136. data/lib/swarm_sdk/swarm/mcp_configurator.rb +0 -270
  137. data/lib/swarm_sdk/swarm/swarm_registry_builder.rb +0 -67
  138. data/lib/swarm_sdk/swarm/tool_configurator.rb +0 -392
  139. data/lib/swarm_sdk/swarm.rb +0 -843
  140. data/lib/swarm_sdk/swarm_loader.rb +0 -145
  141. data/lib/swarm_sdk/swarm_registry.rb +0 -136
  142. data/lib/swarm_sdk/tools/base.rb +0 -63
  143. data/lib/swarm_sdk/tools/bash.rb +0 -280
  144. data/lib/swarm_sdk/tools/clock.rb +0 -46
  145. data/lib/swarm_sdk/tools/delegate.rb +0 -389
  146. data/lib/swarm_sdk/tools/document_converters/base_converter.rb +0 -83
  147. data/lib/swarm_sdk/tools/document_converters/docx_converter.rb +0 -99
  148. data/lib/swarm_sdk/tools/document_converters/html_converter.rb +0 -101
  149. data/lib/swarm_sdk/tools/document_converters/pdf_converter.rb +0 -78
  150. data/lib/swarm_sdk/tools/document_converters/xlsx_converter.rb +0 -194
  151. data/lib/swarm_sdk/tools/edit.rb +0 -145
  152. data/lib/swarm_sdk/tools/glob.rb +0 -166
  153. data/lib/swarm_sdk/tools/grep.rb +0 -235
  154. data/lib/swarm_sdk/tools/image_extractors/docx_image_extractor.rb +0 -43
  155. data/lib/swarm_sdk/tools/image_extractors/pdf_image_extractor.rb +0 -167
  156. data/lib/swarm_sdk/tools/image_formats/tiff_builder.rb +0 -65
  157. data/lib/swarm_sdk/tools/mcp_tool_stub.rb +0 -198
  158. data/lib/swarm_sdk/tools/multi_edit.rb +0 -236
  159. data/lib/swarm_sdk/tools/path_resolver.rb +0 -92
  160. data/lib/swarm_sdk/tools/read.rb +0 -261
  161. data/lib/swarm_sdk/tools/registry.rb +0 -205
  162. data/lib/swarm_sdk/tools/scratchpad/scratchpad_list.rb +0 -117
  163. data/lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb +0 -97
  164. data/lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb +0 -108
  165. data/lib/swarm_sdk/tools/stores/read_tracker.rb +0 -96
  166. data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +0 -273
  167. data/lib/swarm_sdk/tools/stores/storage.rb +0 -142
  168. data/lib/swarm_sdk/tools/stores/todo_manager.rb +0 -65
  169. data/lib/swarm_sdk/tools/think.rb +0 -100
  170. data/lib/swarm_sdk/tools/todo_write.rb +0 -237
  171. data/lib/swarm_sdk/tools/web_fetch.rb +0 -264
  172. data/lib/swarm_sdk/tools/write.rb +0 -112
  173. data/lib/swarm_sdk/transcript_builder.rb +0 -278
  174. data/lib/swarm_sdk/utils.rb +0 -68
  175. data/lib/swarm_sdk/validation_result.rb +0 -33
  176. data/lib/swarm_sdk/version.rb +0 -5
  177. data/lib/swarm_sdk/workflow/agent_config.rb +0 -95
  178. data/lib/swarm_sdk/workflow/builder.rb +0 -227
  179. data/lib/swarm_sdk/workflow/executor.rb +0 -497
  180. data/lib/swarm_sdk/workflow/node_builder.rb +0 -593
  181. data/lib/swarm_sdk/workflow/transformer_executor.rb +0 -250
  182. data/lib/swarm_sdk/workflow.rb +0 -589
  183. data/lib/swarm_sdk.rb +0 -718
@@ -0,0 +1,194 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmSDK
4
+ module V3
5
+ module Memory
6
+ module Adapters
7
+ # Shared FAISS vector index support for adapters
8
+ #
9
+ # Provides FAISS-based vector search and index management that can
10
+ # be included by any adapter that stores embeddings as Ruby arrays.
11
+ # The FAISS index (IndexFlatIP with L2-normalized vectors) handles
12
+ # efficient top-k nearest neighbor search.
13
+ #
14
+ # ## Requirements for including classes
15
+ #
16
+ # The including class must:
17
+ # - Call {#initialize_faiss!} from its constructor
18
+ # - Define a `faiss_directory` method returning the path for FAISS files
19
+ # - Implement `list_cards` (used by {#rebuild_index})
20
+ #
21
+ # @example Including in a custom adapter
22
+ # class MyAdapter < Base
23
+ # include VectorUtils # pairwise similarity
24
+ # include FaissSupport # FAISS vector index
25
+ #
26
+ # def initialize(directory)
27
+ # super()
28
+ # @directory = directory
29
+ # initialize_faiss!
30
+ # end
31
+ #
32
+ # private
33
+ #
34
+ # def faiss_directory
35
+ # @directory
36
+ # end
37
+ # end
38
+ module FaissSupport
39
+ # Embedding dimensions for MiniLM-L6-v2 model
40
+ EMBEDDING_DIMENSIONS = 384
41
+
42
+ # Search the FAISS index for similar vectors
43
+ #
44
+ # @param embedding [Array<Float>] Query embedding
45
+ # @param top_k [Integer] Maximum number of results
46
+ # @param threshold [Float] Minimum cosine similarity to include
47
+ # @return [Array<Hash>] Array of `{ id: String, similarity: Float }`
48
+ def vector_search(embedding, top_k:, threshold: 0.0)
49
+ return [] unless @faiss_index && @faiss_index.ntotal > 0
50
+
51
+ normalized = normalize_vector(embedding)
52
+ effective_k = [top_k, @faiss_index.ntotal].min
53
+ # Explicit Numo conversion for safe FAISS C extension boundary crossing
54
+ query_vector = Numo::SFloat.cast([normalized])
55
+ distances, ids = @faiss_index.search(query_vector, effective_k)
56
+
57
+ index_to_id = @id_to_index.invert
58
+ # FAISS returns Numo::NArray — convert to Ruby arrays
59
+ id_row = ids.to_a.flatten
60
+ dist_row = distances.to_a.flatten
61
+ results = []
62
+ id_row.each_with_index do |idx, i|
63
+ next if idx < 0
64
+
65
+ card_id = index_to_id[idx]
66
+ next unless card_id
67
+
68
+ similarity = dist_row[i]
69
+ next if similarity < threshold
70
+
71
+ results << { id: card_id, similarity: similarity }
72
+ end
73
+
74
+ results
75
+ end
76
+
77
+ # Rebuild the FAISS index from all stored cards
78
+ #
79
+ # @return [void]
80
+ def rebuild_index
81
+ @faiss_index = create_faiss_index
82
+ @id_to_index = {}
83
+ @next_index_id = 0
84
+
85
+ list_cards.each do |card|
86
+ add_to_vector_index(card) if card.embedding
87
+ end
88
+ end
89
+
90
+ private
91
+
92
+ # Initialize FAISS instance variables
93
+ #
94
+ # Must be called from the including class's constructor.
95
+ #
96
+ # @return [void]
97
+ def initialize_faiss!
98
+ @id_to_index = {}
99
+ @next_index_id = 0
100
+ @faiss_index = nil
101
+ end
102
+
103
+ # Create a new FAISS IndexIDMap backed by IndexFlatIP
104
+ #
105
+ # @return [Faiss::IndexIDMap] New FAISS index
106
+ def create_faiss_index
107
+ require "faiss"
108
+ # Keep a reference to the inner index to prevent GC from collecting
109
+ # it while IndexIDMap holds a raw C pointer to it. Without this,
110
+ # Ruby's GC can free the inner index, leaving IndexIDMap with a
111
+ # dangling pointer that causes segfaults on add/search.
112
+ @faiss_inner_index = Faiss::IndexFlatIP.new(EMBEDDING_DIMENSIONS)
113
+ Faiss::IndexIDMap.new(@faiss_inner_index)
114
+ end
115
+
116
+ # Add a card's embedding to the FAISS index
117
+ #
118
+ # @param card [Card] Card with embedding
119
+ # @return [void]
120
+ def add_to_vector_index(card)
121
+ @faiss_index ||= create_faiss_index
122
+ normalized = normalize_vector(card.embedding)
123
+ index_id = @next_index_id
124
+ @next_index_id += 1
125
+ # Explicit Numo conversion avoids segfaults from implicit
126
+ # Ruby Array → Numo cast at the FAISS C extension boundary.
127
+ vectors = Numo::SFloat.cast([normalized])
128
+ ids = Numo::Int64.cast([index_id])
129
+ @faiss_index.add_with_ids(vectors, ids)
130
+ @id_to_index[card.id] = index_id
131
+ end
132
+
133
+ # Remove a card from the vector index
134
+ #
135
+ # Removes the ID mapping. Full FAISS rebuild needed for actual removal.
136
+ #
137
+ # @param card_id [String] Card ID
138
+ # @return [void]
139
+ def remove_from_vector_index(card_id)
140
+ @id_to_index.delete(card_id)
141
+ end
142
+
143
+ # L2-normalize a vector for cosine similarity via inner product
144
+ #
145
+ # @param vec [Array<Float>] Input vector
146
+ # @return [Array<Float>] Normalized vector
147
+ def normalize_vector(vec)
148
+ magnitude = Math.sqrt(vec.sum { |v| v * v })
149
+ return vec if magnitude.zero?
150
+
151
+ vec.map { |v| v / magnitude }
152
+ end
153
+
154
+ # Save the FAISS index and ID mapping to disk
155
+ #
156
+ # @return [void]
157
+ def save_faiss_index
158
+ return unless @faiss_index
159
+
160
+ path = File.join(faiss_directory, "index.faiss")
161
+ @faiss_index.save(path)
162
+
163
+ # Save ID mapping separately
164
+ mapping_path = File.join(faiss_directory, "index_mapping.json")
165
+ File.write(mapping_path, JSON.generate(
166
+ id_to_index: @id_to_index,
167
+ next_index_id: @next_index_id,
168
+ ))
169
+ end
170
+
171
+ # Load the FAISS index and ID mapping from disk
172
+ #
173
+ # @return [void]
174
+ def load_faiss_index
175
+ path = File.join(faiss_directory, "index.faiss")
176
+ mapping_path = File.join(faiss_directory, "index_mapping.json")
177
+
178
+ if File.exist?(path) && File.exist?(mapping_path)
179
+ require "faiss"
180
+ @faiss_index = Faiss::Index.load(path)
181
+ mapping = JSON.parse(File.read(mapping_path))
182
+ @id_to_index = mapping["id_to_index"] || {}
183
+ @next_index_id = mapping["next_index_id"] || 0
184
+ else
185
+ @faiss_index = nil
186
+ @id_to_index = {}
187
+ @next_index_id = 0
188
+ end
189
+ end
190
+ end
191
+ end
192
+ end
193
+ end
194
+ end
@@ -0,0 +1,212 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmSDK
4
+ module V3
5
+ module Memory
6
+ module Adapters
7
+ # Filesystem-based storage adapter with FAISS vector index
8
+ #
9
+ # Stores cards as individual JSON files, edges and clusters in
10
+ # aggregate JSON files, and vectors in a FAISS IndexFlatIP index
11
+ # (cosine similarity via L2-normalized vectors + inner product).
12
+ #
13
+ # Directory structure:
14
+ # .swarm/memory/
15
+ # ├── cards/
16
+ # │ ├── card_a1b2c3.json
17
+ # │ └── ...
18
+ # ├── edges.json
19
+ # ├── clusters.json
20
+ # └── index.faiss
21
+ #
22
+ # @example
23
+ # adapter = FilesystemAdapter.new("/path/to/.swarm/memory")
24
+ # adapter.load
25
+ # adapter.write_card(card)
26
+ # adapter.save
27
+ class FilesystemAdapter < Base
28
+ include VectorUtils
29
+ include FaissSupport
30
+
31
+ # @return [String] Root directory for storage
32
+ attr_reader :directory
33
+
34
+ # @param directory [String] Root directory for memory storage
35
+ def initialize(directory)
36
+ super()
37
+ @directory = File.expand_path(directory)
38
+ @edges = []
39
+ @clusters = []
40
+ initialize_faiss!
41
+ end
42
+
43
+ # --- Card CRUD ---
44
+
45
+ # @param card [Card] Card to write
46
+ # @return [void]
47
+ def write_card(card)
48
+ ensure_directories!
49
+ path = card_path(card.id)
50
+ File.write(path, JSON.pretty_generate(card.to_h))
51
+ add_to_vector_index(card) if card.embedding
52
+ end
53
+
54
+ # @param id [String] Card ID
55
+ # @return [Card, nil]
56
+ def read_card(id)
57
+ path = card_path(id)
58
+ return unless File.exist?(path)
59
+
60
+ data = JSON.parse(File.read(path))
61
+ Card.from_h(data)
62
+ end
63
+
64
+ # @param id [String] Card ID
65
+ # @return [void]
66
+ def delete_card(id)
67
+ path = card_path(id)
68
+ File.delete(path) if File.exist?(path)
69
+ remove_from_vector_index(id)
70
+ end
71
+
72
+ # @param prefix [String, nil] ID prefix filter
73
+ # @return [Array<Card>]
74
+ def list_cards(prefix: nil)
75
+ ensure_directories!
76
+ pattern = prefix ? "#{prefix}*.json" : "*.json"
77
+ Dir.glob(File.join(cards_dir, pattern)).map do |path|
78
+ data = JSON.parse(File.read(path))
79
+ Card.from_h(data)
80
+ end
81
+ end
82
+
83
+ # --- Edge CRUD ---
84
+
85
+ # @param edge [Edge] Edge to write
86
+ # @return [void]
87
+ def write_edge(edge)
88
+ @edges << edge
89
+ end
90
+
91
+ # @param card_id [String] Card ID
92
+ # @param type [Symbol, nil] Filter by edge type
93
+ # @return [Array<Edge>]
94
+ def edges_for(card_id, type: nil)
95
+ results = @edges.select { |e| e.from_id == card_id || e.to_id == card_id }
96
+ results = results.select { |e| e.type == type } if type
97
+ results
98
+ end
99
+
100
+ # @param card_id [String] Card ID
101
+ # @return [void]
102
+ def delete_edges_for(card_id)
103
+ @edges.reject! { |e| e.from_id == card_id || e.to_id == card_id }
104
+ end
105
+
106
+ # --- Cluster CRUD ---
107
+
108
+ # @param cluster [Cluster] Cluster to write
109
+ # @return [void]
110
+ def write_cluster(cluster)
111
+ idx = @clusters.index { |c| c.id == cluster.id }
112
+ if idx
113
+ @clusters[idx] = cluster
114
+ else
115
+ @clusters << cluster
116
+ end
117
+ end
118
+
119
+ # @param id [String] Cluster ID
120
+ # @return [Cluster, nil]
121
+ def read_cluster(id)
122
+ @clusters.find { |c| c.id == id }
123
+ end
124
+
125
+ # @return [Array<Cluster>]
126
+ def list_clusters
127
+ @clusters.dup
128
+ end
129
+
130
+ # --- Persistence ---
131
+
132
+ # Save edges, clusters, and FAISS index to disk
133
+ #
134
+ # @return [void]
135
+ def save
136
+ ensure_directories!
137
+ save_edges
138
+ save_clusters
139
+ save_faiss_index
140
+ end
141
+
142
+ # Load edges, clusters, and FAISS index from disk
143
+ #
144
+ # @return [void]
145
+ def load
146
+ ensure_directories!
147
+ load_edges
148
+ load_clusters
149
+ load_faiss_index
150
+ end
151
+
152
+ private
153
+
154
+ # @return [String] Path to the FAISS index directory
155
+ def faiss_directory
156
+ @directory
157
+ end
158
+
159
+ # @return [String] Path to cards directory
160
+ def cards_dir
161
+ File.join(@directory, "cards")
162
+ end
163
+
164
+ # @param id [String] Card ID
165
+ # @return [String] Path to card JSON file
166
+ def card_path(id)
167
+ File.join(cards_dir, "#{id}.json")
168
+ end
169
+
170
+ # Ensure required directories exist
171
+ #
172
+ # @return [void]
173
+ def ensure_directories!
174
+ FileUtils.mkdir_p(cards_dir)
175
+ end
176
+
177
+ # --- Serialization ---
178
+
179
+ # @return [void]
180
+ def save_edges
181
+ path = File.join(@directory, "edges.json")
182
+ File.write(path, JSON.pretty_generate(@edges.map(&:to_h)))
183
+ end
184
+
185
+ # @return [void]
186
+ def load_edges
187
+ path = File.join(@directory, "edges.json")
188
+ return unless File.exist?(path)
189
+
190
+ data = JSON.parse(File.read(path))
191
+ @edges = data.map { |h| Edge.from_h(h) }
192
+ end
193
+
194
+ # @return [void]
195
+ def save_clusters
196
+ path = File.join(@directory, "clusters.json")
197
+ File.write(path, JSON.pretty_generate(@clusters.map(&:to_h)))
198
+ end
199
+
200
+ # @return [void]
201
+ def load_clusters
202
+ path = File.join(@directory, "clusters.json")
203
+ return unless File.exist?(path)
204
+
205
+ data = JSON.parse(File.read(path))
206
+ @clusters = data.map { |h| Cluster.from_h(h) }
207
+ end
208
+ end
209
+ end
210
+ end
211
+ end
212
+ end