claude_memory 0.2.0 → 0.3.0
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/.claude/.mind.mv2.o2N83S +0 -0
- data/.claude/CLAUDE.md +1 -0
- data/.claude/rules/claude_memory.generated.md +28 -9
- data/.claude/settings.local.json +9 -1
- data/.claude/skills/check-memory/SKILL.md +77 -0
- data/.claude/skills/improve/SKILL.md +532 -0
- data/.claude/skills/improve/feature-patterns.md +1221 -0
- data/.claude/skills/quality-update/SKILL.md +229 -0
- data/.claude/skills/quality-update/implementation-guide.md +346 -0
- data/.claude/skills/review-commit/SKILL.md +199 -0
- data/.claude/skills/review-for-quality/SKILL.md +154 -0
- data/.claude/skills/review-for-quality/expert-checklists.md +79 -0
- data/.claude/skills/setup-memory/SKILL.md +168 -0
- data/.claude/skills/study-repo/SKILL.md +307 -0
- data/.claude/skills/study-repo/analysis-template.md +323 -0
- data/.claude/skills/study-repo/focus-examples.md +327 -0
- data/CHANGELOG.md +133 -0
- data/CLAUDE.md +130 -11
- data/README.md +117 -10
- data/db/migrations/001_create_initial_schema.rb +117 -0
- data/db/migrations/002_add_project_scoping.rb +33 -0
- data/db/migrations/003_add_session_metadata.rb +42 -0
- data/db/migrations/004_add_fact_embeddings.rb +20 -0
- data/db/migrations/005_add_incremental_sync.rb +21 -0
- data/db/migrations/006_add_operation_tracking.rb +40 -0
- data/db/migrations/007_add_ingestion_metrics.rb +26 -0
- data/docs/.claude/mind.mv2.lock +0 -0
- data/docs/GETTING_STARTED.md +587 -0
- data/docs/RELEASE_NOTES_v0.2.0.md +0 -1
- data/docs/RUBY_COMMUNITY_POST_v0.2.0.md +0 -2
- data/docs/architecture.md +9 -8
- data/docs/auto_init_design.md +230 -0
- data/docs/improvements.md +557 -731
- data/docs/influence/.gitkeep +13 -0
- data/docs/influence/grepai.md +933 -0
- data/docs/influence/qmd.md +2195 -0
- data/docs/plugin.md +257 -11
- data/docs/quality_review.md +472 -1273
- data/docs/remaining_improvements.md +330 -0
- data/lefthook.yml +13 -0
- data/lib/claude_memory/commands/checks/claude_md_check.rb +41 -0
- data/lib/claude_memory/commands/checks/database_check.rb +120 -0
- data/lib/claude_memory/commands/checks/hooks_check.rb +112 -0
- data/lib/claude_memory/commands/checks/reporter.rb +110 -0
- data/lib/claude_memory/commands/checks/snapshot_check.rb +30 -0
- data/lib/claude_memory/commands/doctor_command.rb +12 -129
- data/lib/claude_memory/commands/help_command.rb +1 -0
- data/lib/claude_memory/commands/hook_command.rb +9 -2
- data/lib/claude_memory/commands/index_command.rb +169 -0
- data/lib/claude_memory/commands/ingest_command.rb +1 -1
- data/lib/claude_memory/commands/init_command.rb +5 -197
- data/lib/claude_memory/commands/initializers/database_ensurer.rb +30 -0
- data/lib/claude_memory/commands/initializers/global_initializer.rb +85 -0
- data/lib/claude_memory/commands/initializers/hooks_configurator.rb +156 -0
- data/lib/claude_memory/commands/initializers/mcp_configurator.rb +56 -0
- data/lib/claude_memory/commands/initializers/memory_instructions_writer.rb +135 -0
- data/lib/claude_memory/commands/initializers/project_initializer.rb +111 -0
- data/lib/claude_memory/commands/recover_command.rb +75 -0
- data/lib/claude_memory/commands/registry.rb +5 -1
- data/lib/claude_memory/commands/stats_command.rb +239 -0
- data/lib/claude_memory/commands/uninstall_command.rb +226 -0
- data/lib/claude_memory/core/batch_loader.rb +32 -0
- data/lib/claude_memory/core/concept_ranker.rb +73 -0
- data/lib/claude_memory/core/embedding_candidate_builder.rb +37 -0
- data/lib/claude_memory/core/fact_collector.rb +51 -0
- data/lib/claude_memory/core/fact_query_builder.rb +154 -0
- data/lib/claude_memory/core/fact_ranker.rb +113 -0
- data/lib/claude_memory/core/result_builder.rb +54 -0
- data/lib/claude_memory/core/result_sorter.rb +25 -0
- data/lib/claude_memory/core/scope_filter.rb +61 -0
- data/lib/claude_memory/core/text_builder.rb +29 -0
- data/lib/claude_memory/embeddings/generator.rb +161 -0
- data/lib/claude_memory/embeddings/similarity.rb +69 -0
- data/lib/claude_memory/hook/handler.rb +4 -3
- data/lib/claude_memory/index/lexical_fts.rb +7 -2
- data/lib/claude_memory/infrastructure/operation_tracker.rb +158 -0
- data/lib/claude_memory/infrastructure/schema_validator.rb +206 -0
- data/lib/claude_memory/ingest/content_sanitizer.rb +6 -7
- data/lib/claude_memory/ingest/ingester.rb +99 -15
- data/lib/claude_memory/ingest/metadata_extractor.rb +57 -0
- data/lib/claude_memory/ingest/tool_extractor.rb +71 -0
- data/lib/claude_memory/mcp/response_formatter.rb +331 -0
- data/lib/claude_memory/mcp/server.rb +19 -0
- data/lib/claude_memory/mcp/setup_status_analyzer.rb +73 -0
- data/lib/claude_memory/mcp/tool_definitions.rb +279 -0
- data/lib/claude_memory/mcp/tool_helpers.rb +80 -0
- data/lib/claude_memory/mcp/tools.rb +330 -320
- data/lib/claude_memory/recall/dual_query_template.rb +63 -0
- data/lib/claude_memory/recall.rb +304 -237
- data/lib/claude_memory/resolve/resolver.rb +52 -49
- data/lib/claude_memory/store/sqlite_store.rb +210 -144
- data/lib/claude_memory/store/store_manager.rb +6 -6
- data/lib/claude_memory/sweep/sweeper.rb +6 -0
- data/lib/claude_memory/version.rb +1 -1
- data/lib/claude_memory.rb +35 -3
- metadata +71 -11
- data/.claude/.mind.mv2.aLCUZd +0 -0
- data/.claude/memory.sqlite3 +0 -0
- data/.mcp.json +0 -11
- /data/docs/{feature_adoption_plan.md → plans/feature_adoption_plan.md} +0 -0
- /data/docs/{feature_adoption_plan_revised.md → plans/feature_adoption_plan_revised.md} +0 -0
- /data/docs/{plan.md → plans/plan.md} +0 -0
- /data/docs/{updated_plan.md → plans/updated_plan.md} +0 -0
data/lib/claude_memory/recall.rb
CHANGED
|
@@ -24,8 +24,10 @@ module ClaudeMemory
|
|
|
24
24
|
end
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
-
def initialize(store_or_manager, fts: nil, project_path: nil, env: ENV)
|
|
28
|
-
|
|
27
|
+
def initialize(store_or_manager, fts: nil, project_path: nil, env: ENV, embedding_generator: nil)
|
|
28
|
+
config = Configuration.new(env)
|
|
29
|
+
@project_path = project_path || config.project_dir
|
|
30
|
+
@embedding_generator = embedding_generator || Embeddings::Generator.new
|
|
29
31
|
|
|
30
32
|
if store_or_manager.is_a?(Store::StoreManager)
|
|
31
33
|
@manager = store_or_manager
|
|
@@ -79,49 +81,63 @@ module ClaudeMemory
|
|
|
79
81
|
end
|
|
80
82
|
end
|
|
81
83
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
84
|
+
def facts_by_branch(branch_name, limit: 20, scope: SCOPE_ALL)
|
|
85
|
+
if @legacy_mode
|
|
86
|
+
facts_by_context_legacy(:git_branch, branch_name, limit: limit, scope: scope)
|
|
87
|
+
else
|
|
88
|
+
facts_by_context_dual(:git_branch, branch_name, limit: limit, scope: scope)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
86
91
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
end
|
|
92
|
+
def facts_by_directory(cwd, limit: 20, scope: SCOPE_ALL)
|
|
93
|
+
if @legacy_mode
|
|
94
|
+
facts_by_context_legacy(:cwd, cwd, limit: limit, scope: scope)
|
|
95
|
+
else
|
|
96
|
+
facts_by_context_dual(:cwd, cwd, limit: limit, scope: scope)
|
|
93
97
|
end
|
|
98
|
+
end
|
|
94
99
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
end
|
|
100
|
+
def facts_by_tool(tool_name, limit: 20, scope: SCOPE_ALL)
|
|
101
|
+
if @legacy_mode
|
|
102
|
+
facts_by_tool_legacy(tool_name, limit: limit, scope: scope)
|
|
103
|
+
else
|
|
104
|
+
facts_by_tool_dual(tool_name, limit: limit, scope: scope)
|
|
101
105
|
end
|
|
106
|
+
end
|
|
102
107
|
|
|
103
|
-
|
|
108
|
+
def query_semantic(text, limit: 10, scope: SCOPE_ALL, mode: :both)
|
|
109
|
+
if @legacy_mode
|
|
110
|
+
query_semantic_legacy(text, limit: limit, scope: scope, mode: mode)
|
|
111
|
+
else
|
|
112
|
+
query_semantic_dual(text, limit: limit, scope: scope, mode: mode)
|
|
113
|
+
end
|
|
104
114
|
end
|
|
105
115
|
|
|
106
|
-
def
|
|
107
|
-
|
|
116
|
+
def query_concepts(concepts, limit: 10, scope: SCOPE_ALL)
|
|
117
|
+
raise ArgumentError, "Must provide 2-5 concepts" unless (2..5).cover?(concepts.size)
|
|
108
118
|
|
|
109
|
-
if
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
results.concat(project_results)
|
|
114
|
-
end
|
|
119
|
+
if @legacy_mode
|
|
120
|
+
query_concepts_legacy(concepts, limit: limit, scope: scope)
|
|
121
|
+
else
|
|
122
|
+
query_concepts_dual(concepts, limit: limit, scope: scope)
|
|
115
123
|
end
|
|
124
|
+
end
|
|
116
125
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
126
|
+
private
|
|
127
|
+
|
|
128
|
+
def query_dual(query_text, limit:, scope:)
|
|
129
|
+
template = Recall::DualQueryTemplate.new(@manager)
|
|
130
|
+
results = template.execute(scope: scope, limit: limit) do |store, source|
|
|
131
|
+
query_single_store(store, query_text, limit: limit, source: source)
|
|
123
132
|
end
|
|
133
|
+
dedupe_and_sort(results, limit)
|
|
134
|
+
end
|
|
124
135
|
|
|
136
|
+
def query_index_dual(query_text, limit:, scope:)
|
|
137
|
+
template = Recall::DualQueryTemplate.new(@manager)
|
|
138
|
+
results = template.execute(scope: scope, limit: limit) do |store, source|
|
|
139
|
+
query_index_single_store(store, query_text, limit: limit, source: source)
|
|
140
|
+
end
|
|
125
141
|
dedupe_and_sort_index(results, limit)
|
|
126
142
|
end
|
|
127
143
|
|
|
@@ -138,22 +154,7 @@ module ClaudeMemory
|
|
|
138
154
|
end
|
|
139
155
|
|
|
140
156
|
def dedupe_and_sort_index(results, limit)
|
|
141
|
-
|
|
142
|
-
unique_results = []
|
|
143
|
-
|
|
144
|
-
results.each do |result|
|
|
145
|
-
sig = "#{result[:subject]}:#{result[:predicate]}:#{result[:object_preview]}"
|
|
146
|
-
next if seen_signatures.include?(sig)
|
|
147
|
-
|
|
148
|
-
seen_signatures.add(sig)
|
|
149
|
-
unique_results << result
|
|
150
|
-
end
|
|
151
|
-
|
|
152
|
-
# Sort by source priority (project first)
|
|
153
|
-
unique_results.sort_by do |item|
|
|
154
|
-
source_priority = (item[:source] == :project) ? 0 : 1
|
|
155
|
-
[source_priority]
|
|
156
|
-
end.first(limit)
|
|
157
|
+
Core::FactRanker.dedupe_and_sort_index(results, limit)
|
|
157
158
|
end
|
|
158
159
|
|
|
159
160
|
def query_single_store(store, query_text, limit:, source:)
|
|
@@ -161,27 +162,22 @@ module ClaudeMemory
|
|
|
161
162
|
content_ids = fts.search(query_text, limit: limit * 3)
|
|
162
163
|
return [] if content_ids.empty?
|
|
163
164
|
|
|
164
|
-
#
|
|
165
|
-
|
|
166
|
-
ordered_fact_ids = []
|
|
167
|
-
|
|
165
|
+
# Build provenance map for ordered collection
|
|
166
|
+
provenance_by_content = {}
|
|
168
167
|
content_ids.each do |content_id|
|
|
169
|
-
|
|
168
|
+
provenance_by_content[content_id] = store.provenance
|
|
170
169
|
.select(:fact_id)
|
|
171
170
|
.where(content_item_id: content_id)
|
|
172
171
|
.all
|
|
173
|
-
|
|
174
|
-
provenance_records.each do |prov|
|
|
175
|
-
fact_id = prov[:fact_id]
|
|
176
|
-
next if seen_fact_ids.include?(fact_id)
|
|
177
|
-
|
|
178
|
-
seen_fact_ids.add(fact_id)
|
|
179
|
-
ordered_fact_ids << fact_id
|
|
180
|
-
break if ordered_fact_ids.size >= limit
|
|
181
|
-
end
|
|
182
|
-
break if ordered_fact_ids.size >= limit
|
|
183
172
|
end
|
|
184
173
|
|
|
174
|
+
# Collect fact IDs in content order, deduplicated
|
|
175
|
+
ordered_fact_ids = Core::FactCollector.collect_ordered_fact_ids(
|
|
176
|
+
provenance_by_content,
|
|
177
|
+
content_ids,
|
|
178
|
+
limit
|
|
179
|
+
)
|
|
180
|
+
|
|
185
181
|
return [] if ordered_fact_ids.empty?
|
|
186
182
|
|
|
187
183
|
# Batch query all facts at once
|
|
@@ -191,129 +187,47 @@ module ClaudeMemory
|
|
|
191
187
|
receipts_by_fact_id = batch_find_receipts(store, ordered_fact_ids)
|
|
192
188
|
|
|
193
189
|
# Build results maintaining order
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
receipts: receipts_by_fact_id[fact_id] || [],
|
|
201
|
-
source: source
|
|
202
|
-
}
|
|
203
|
-
end.compact
|
|
190
|
+
Core::ResultBuilder.build_results(
|
|
191
|
+
ordered_fact_ids,
|
|
192
|
+
facts_by_id: facts_by_id,
|
|
193
|
+
receipts_by_fact_id: receipts_by_fact_id,
|
|
194
|
+
source: source
|
|
195
|
+
)
|
|
204
196
|
end
|
|
205
197
|
|
|
206
198
|
def batch_find_facts(store, fact_ids)
|
|
207
|
-
store
|
|
208
|
-
.left_join(:entities, id: :subject_entity_id)
|
|
209
|
-
.select(
|
|
210
|
-
Sequel[:facts][:id],
|
|
211
|
-
Sequel[:facts][:predicate],
|
|
212
|
-
Sequel[:facts][:object_literal],
|
|
213
|
-
Sequel[:facts][:status],
|
|
214
|
-
Sequel[:facts][:confidence],
|
|
215
|
-
Sequel[:facts][:valid_from],
|
|
216
|
-
Sequel[:facts][:valid_to],
|
|
217
|
-
Sequel[:facts][:created_at],
|
|
218
|
-
Sequel[:entities][:canonical_name].as(:subject_name),
|
|
219
|
-
Sequel[:facts][:scope],
|
|
220
|
-
Sequel[:facts][:project_path]
|
|
221
|
-
)
|
|
222
|
-
.where(Sequel[:facts][:id] => fact_ids)
|
|
223
|
-
.all
|
|
224
|
-
.each_with_object({}) { |fact, hash| hash[fact[:id]] = fact }
|
|
199
|
+
Core::FactQueryBuilder.batch_find_facts(store, fact_ids)
|
|
225
200
|
end
|
|
226
201
|
|
|
227
202
|
def batch_find_receipts(store, fact_ids)
|
|
228
|
-
store
|
|
229
|
-
.left_join(:content_items, id: :content_item_id)
|
|
230
|
-
.select(
|
|
231
|
-
Sequel[:provenance][:id],
|
|
232
|
-
Sequel[:provenance][:fact_id],
|
|
233
|
-
Sequel[:provenance][:quote],
|
|
234
|
-
Sequel[:provenance][:strength],
|
|
235
|
-
Sequel[:content_items][:session_id],
|
|
236
|
-
Sequel[:content_items][:occurred_at]
|
|
237
|
-
)
|
|
238
|
-
.where(Sequel[:provenance][:fact_id] => fact_ids)
|
|
239
|
-
.all
|
|
240
|
-
.group_by { |receipt| receipt[:fact_id] }
|
|
203
|
+
Core::FactQueryBuilder.batch_find_receipts(store, fact_ids)
|
|
241
204
|
end
|
|
242
205
|
|
|
243
206
|
def dedupe_and_sort(results, limit)
|
|
244
|
-
|
|
245
|
-
unique_results = []
|
|
246
|
-
|
|
247
|
-
results.each do |result|
|
|
248
|
-
fact = result[:fact]
|
|
249
|
-
sig = "#{fact[:subject_name]}:#{fact[:predicate]}:#{fact[:object_literal]}"
|
|
250
|
-
next if seen_signatures.include?(sig)
|
|
251
|
-
|
|
252
|
-
seen_signatures.add(sig)
|
|
253
|
-
unique_results << result
|
|
254
|
-
end
|
|
255
|
-
|
|
256
|
-
unique_results.sort_by do |item|
|
|
257
|
-
source_priority = (item[:source] == :project) ? 0 : 1
|
|
258
|
-
[source_priority, item[:fact][:created_at]]
|
|
259
|
-
end.first(limit)
|
|
207
|
+
Core::FactRanker.dedupe_and_sort(results, limit)
|
|
260
208
|
end
|
|
261
209
|
|
|
262
210
|
def changes_dual(since:, limit:, scope:)
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
project_changes = fetch_changes(@manager.project_store, since, limit)
|
|
269
|
-
project_changes.each { |c| c[:source] = :project }
|
|
270
|
-
results.concat(project_changes)
|
|
271
|
-
end
|
|
211
|
+
template = Recall::DualQueryTemplate.new(@manager)
|
|
212
|
+
results = template.execute(scope: scope, limit: limit) do |store, source|
|
|
213
|
+
changes = fetch_changes(store, since, limit)
|
|
214
|
+
Core::ResultSorter.annotate_source(changes, source)
|
|
215
|
+
changes
|
|
272
216
|
end
|
|
273
|
-
|
|
274
|
-
if scope == SCOPE_ALL || scope == SCOPE_GLOBAL
|
|
275
|
-
@manager.ensure_global! if @manager.global_exists?
|
|
276
|
-
if @manager.global_store
|
|
277
|
-
global_changes = fetch_changes(@manager.global_store, since, limit)
|
|
278
|
-
global_changes.each { |c| c[:source] = :global }
|
|
279
|
-
results.concat(global_changes)
|
|
280
|
-
end
|
|
281
|
-
end
|
|
282
|
-
|
|
283
|
-
results.sort_by { |c| c[:created_at] }.reverse.first(limit)
|
|
217
|
+
Core::ResultSorter.sort_by_timestamp(results, limit)
|
|
284
218
|
end
|
|
285
219
|
|
|
286
220
|
def fetch_changes(store, since, limit)
|
|
287
|
-
store
|
|
288
|
-
.select(:id, :subject_entity_id, :predicate, :object_literal, :status, :created_at, :scope, :project_path)
|
|
289
|
-
.where { created_at >= since }
|
|
290
|
-
.order(Sequel.desc(:created_at))
|
|
291
|
-
.limit(limit)
|
|
292
|
-
.all
|
|
221
|
+
Core::FactQueryBuilder.fetch_changes(store, since, limit)
|
|
293
222
|
end
|
|
294
223
|
|
|
295
224
|
def conflicts_dual(scope:)
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
project_conflicts = @manager.project_store.open_conflicts
|
|
302
|
-
project_conflicts.each { |c| c[:source] = :project }
|
|
303
|
-
results.concat(project_conflicts)
|
|
304
|
-
end
|
|
305
|
-
end
|
|
306
|
-
|
|
307
|
-
if scope == SCOPE_ALL || scope == SCOPE_GLOBAL
|
|
308
|
-
@manager.ensure_global! if @manager.global_exists?
|
|
309
|
-
if @manager.global_store
|
|
310
|
-
global_conflicts = @manager.global_store.open_conflicts
|
|
311
|
-
global_conflicts.each { |c| c[:source] = :global }
|
|
312
|
-
results.concat(global_conflicts)
|
|
313
|
-
end
|
|
225
|
+
template = Recall::DualQueryTemplate.new(@manager)
|
|
226
|
+
template.execute(scope: scope) do |store, source|
|
|
227
|
+
conflicts = store.open_conflicts
|
|
228
|
+
Core::ResultSorter.annotate_source(conflicts, source)
|
|
229
|
+
conflicts
|
|
314
230
|
end
|
|
315
|
-
|
|
316
|
-
results
|
|
317
231
|
end
|
|
318
232
|
|
|
319
233
|
def explain_from_store(store, fact_id)
|
|
@@ -330,56 +244,23 @@ module ClaudeMemory
|
|
|
330
244
|
end
|
|
331
245
|
|
|
332
246
|
def find_fact_from_store(store, fact_id)
|
|
333
|
-
store
|
|
334
|
-
.left_join(:entities, id: :subject_entity_id)
|
|
335
|
-
.select(
|
|
336
|
-
Sequel[:facts][:id],
|
|
337
|
-
Sequel[:facts][:predicate],
|
|
338
|
-
Sequel[:facts][:object_literal],
|
|
339
|
-
Sequel[:facts][:status],
|
|
340
|
-
Sequel[:facts][:confidence],
|
|
341
|
-
Sequel[:facts][:valid_from],
|
|
342
|
-
Sequel[:facts][:valid_to],
|
|
343
|
-
Sequel[:facts][:created_at],
|
|
344
|
-
Sequel[:entities][:canonical_name].as(:subject_name),
|
|
345
|
-
Sequel[:facts][:scope],
|
|
346
|
-
Sequel[:facts][:project_path]
|
|
347
|
-
)
|
|
348
|
-
.where(Sequel[:facts][:id] => fact_id)
|
|
349
|
-
.first
|
|
247
|
+
Core::FactQueryBuilder.find_fact(store, fact_id)
|
|
350
248
|
end
|
|
351
249
|
|
|
352
250
|
def find_receipts_from_store(store, fact_id)
|
|
353
|
-
store
|
|
354
|
-
.left_join(:content_items, id: :content_item_id)
|
|
355
|
-
.select(
|
|
356
|
-
Sequel[:provenance][:id],
|
|
357
|
-
Sequel[:provenance][:quote],
|
|
358
|
-
Sequel[:provenance][:strength],
|
|
359
|
-
Sequel[:content_items][:session_id],
|
|
360
|
-
Sequel[:content_items][:occurred_at]
|
|
361
|
-
)
|
|
362
|
-
.where(Sequel[:provenance][:fact_id] => fact_id)
|
|
363
|
-
.all
|
|
251
|
+
Core::FactQueryBuilder.find_receipts(store, fact_id)
|
|
364
252
|
end
|
|
365
253
|
|
|
366
254
|
def find_superseded_by_from_store(store, fact_id)
|
|
367
|
-
store
|
|
368
|
-
.where(to_fact_id: fact_id, link_type: "supersedes")
|
|
369
|
-
.select_map(:from_fact_id)
|
|
255
|
+
Core::FactQueryBuilder.find_superseded_by(store, fact_id)
|
|
370
256
|
end
|
|
371
257
|
|
|
372
258
|
def find_supersedes_from_store(store, fact_id)
|
|
373
|
-
store
|
|
374
|
-
.where(from_fact_id: fact_id, link_type: "supersedes")
|
|
375
|
-
.select_map(:to_fact_id)
|
|
259
|
+
Core::FactQueryBuilder.find_supersedes(store, fact_id)
|
|
376
260
|
end
|
|
377
261
|
|
|
378
262
|
def find_conflicts_from_store(store, fact_id)
|
|
379
|
-
store
|
|
380
|
-
.select(:id, :fact_a_id, :fact_b_id, :status)
|
|
381
|
-
.where(Sequel.or(fact_a_id: fact_id, fact_b_id: fact_id))
|
|
382
|
-
.all
|
|
263
|
+
Core::FactQueryBuilder.find_conflicts(store, fact_id)
|
|
383
264
|
end
|
|
384
265
|
|
|
385
266
|
def query_legacy(query_text, limit:, scope:)
|
|
@@ -454,47 +335,19 @@ module ClaudeMemory
|
|
|
454
335
|
end
|
|
455
336
|
|
|
456
337
|
def fact_matches_scope?(fact, scope)
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
fact_scope = fact[:scope] || "project"
|
|
460
|
-
fact_project = fact[:project_path]
|
|
461
|
-
|
|
462
|
-
case scope
|
|
463
|
-
when SCOPE_PROJECT
|
|
464
|
-
fact_scope == "project" && fact_project == @project_path
|
|
465
|
-
when SCOPE_GLOBAL
|
|
466
|
-
fact_scope == "global"
|
|
467
|
-
else
|
|
468
|
-
true
|
|
469
|
-
end
|
|
338
|
+
Core::ScopeFilter.matches?(fact, scope, @project_path)
|
|
470
339
|
end
|
|
471
340
|
|
|
472
341
|
def apply_scope_filter(dataset, scope)
|
|
473
|
-
|
|
474
|
-
when SCOPE_PROJECT
|
|
475
|
-
dataset.where(scope: "project", project_path: @project_path)
|
|
476
|
-
when SCOPE_GLOBAL
|
|
477
|
-
dataset.where(scope: "global")
|
|
478
|
-
else
|
|
479
|
-
dataset
|
|
480
|
-
end
|
|
342
|
+
Core::ScopeFilter.apply_to_dataset(dataset, scope, @project_path)
|
|
481
343
|
end
|
|
482
344
|
|
|
483
345
|
def sort_by_scope_priority(facts_with_provenance)
|
|
484
|
-
facts_with_provenance
|
|
485
|
-
fact = item[:fact]
|
|
486
|
-
is_current_project = fact[:project_path] == @project_path
|
|
487
|
-
is_global = fact[:scope] == "global"
|
|
488
|
-
|
|
489
|
-
[is_current_project ? 0 : 1, is_global ? 0 : 1]
|
|
490
|
-
end
|
|
346
|
+
Core::FactRanker.sort_by_scope_priority(facts_with_provenance, @project_path)
|
|
491
347
|
end
|
|
492
348
|
|
|
493
349
|
def find_provenance_by_content(content_id)
|
|
494
|
-
@legacy_store
|
|
495
|
-
.select(:id, :fact_id, :content_item_id, :quote, :strength)
|
|
496
|
-
.where(content_item_id: content_id)
|
|
497
|
-
.all
|
|
350
|
+
Core::FactQueryBuilder.find_provenance_by_content(@legacy_store, content_id)
|
|
498
351
|
end
|
|
499
352
|
|
|
500
353
|
def find_fact(fact_id)
|
|
@@ -504,5 +357,219 @@ module ClaudeMemory
|
|
|
504
357
|
def find_receipts(fact_id)
|
|
505
358
|
find_receipts_from_store(@legacy_store, fact_id)
|
|
506
359
|
end
|
|
360
|
+
|
|
361
|
+
# Context-aware query helpers
|
|
362
|
+
|
|
363
|
+
def facts_by_context_dual(column, value, limit:, scope:)
|
|
364
|
+
template = Recall::DualQueryTemplate.new(@manager)
|
|
365
|
+
results = template.execute(scope: scope, limit: limit) do |store, source|
|
|
366
|
+
facts_by_context_single(store, column, value, limit: limit, source: source)
|
|
367
|
+
end
|
|
368
|
+
dedupe_and_sort(results, limit)
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
def facts_by_context_legacy(column, value, limit:, scope:)
|
|
372
|
+
facts_by_context_single(@legacy_store, column, value, limit: limit, source: :legacy)
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
def facts_by_context_single(store, column, value, limit:, source:)
|
|
376
|
+
# Find content items matching the context
|
|
377
|
+
content_ids = store.content_items
|
|
378
|
+
.where(column => value)
|
|
379
|
+
.select(:id)
|
|
380
|
+
.map { |row| row[:id] }
|
|
381
|
+
|
|
382
|
+
return [] if content_ids.empty?
|
|
383
|
+
|
|
384
|
+
# Find facts linked to those content items via provenance
|
|
385
|
+
fact_ids = store.provenance
|
|
386
|
+
.where(content_item_id: content_ids)
|
|
387
|
+
.select(:fact_id)
|
|
388
|
+
.distinct
|
|
389
|
+
.map { |row| row[:fact_id] }
|
|
390
|
+
|
|
391
|
+
return [] if fact_ids.empty?
|
|
392
|
+
|
|
393
|
+
# Batch fetch facts and their provenance
|
|
394
|
+
facts_by_id = batch_find_facts(store, fact_ids)
|
|
395
|
+
receipts_by_fact_id = batch_find_receipts(store, fact_ids)
|
|
396
|
+
|
|
397
|
+
results = Core::ResultBuilder.build_results(
|
|
398
|
+
fact_ids,
|
|
399
|
+
facts_by_id: facts_by_id,
|
|
400
|
+
receipts_by_fact_id: receipts_by_fact_id,
|
|
401
|
+
source: source
|
|
402
|
+
)
|
|
403
|
+
results.take(limit)
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
def facts_by_tool_dual(tool_name, limit:, scope:)
|
|
407
|
+
template = Recall::DualQueryTemplate.new(@manager)
|
|
408
|
+
results = template.execute(scope: scope, limit: limit) do |store, source|
|
|
409
|
+
facts_by_tool_single(store, tool_name, limit: limit, source: source)
|
|
410
|
+
end
|
|
411
|
+
dedupe_and_sort(results, limit)
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
def facts_by_tool_legacy(tool_name, limit:, scope:)
|
|
415
|
+
facts_by_tool_single(@legacy_store, tool_name, limit: limit, source: :legacy)
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
def facts_by_tool_single(store, tool_name, limit:, source:)
|
|
419
|
+
# Find content items where the tool was used
|
|
420
|
+
content_ids = store.tool_calls
|
|
421
|
+
.where(tool_name: tool_name)
|
|
422
|
+
.select(:content_item_id)
|
|
423
|
+
.distinct
|
|
424
|
+
.map { |row| row[:content_item_id] }
|
|
425
|
+
|
|
426
|
+
return [] if content_ids.empty?
|
|
427
|
+
|
|
428
|
+
# Find facts linked to those content items via provenance
|
|
429
|
+
fact_ids = store.provenance
|
|
430
|
+
.where(content_item_id: content_ids)
|
|
431
|
+
.select(:fact_id)
|
|
432
|
+
.distinct
|
|
433
|
+
.map { |row| row[:fact_id] }
|
|
434
|
+
|
|
435
|
+
return [] if fact_ids.empty?
|
|
436
|
+
|
|
437
|
+
# Batch fetch facts and their provenance
|
|
438
|
+
facts_by_id = batch_find_facts(store, fact_ids)
|
|
439
|
+
receipts_by_fact_id = batch_find_receipts(store, fact_ids)
|
|
440
|
+
|
|
441
|
+
results = Core::ResultBuilder.build_results(
|
|
442
|
+
fact_ids,
|
|
443
|
+
facts_by_id: facts_by_id,
|
|
444
|
+
receipts_by_fact_id: receipts_by_fact_id,
|
|
445
|
+
source: source
|
|
446
|
+
)
|
|
447
|
+
results.take(limit)
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
# Semantic search helpers
|
|
451
|
+
|
|
452
|
+
def query_semantic_dual(text, limit:, scope:, mode:)
|
|
453
|
+
template = Recall::DualQueryTemplate.new(@manager)
|
|
454
|
+
results = template.execute(scope: scope, limit: limit) do |store, source|
|
|
455
|
+
query_semantic_single(store, text, limit: limit * 3, mode: mode, source: source)
|
|
456
|
+
end
|
|
457
|
+
dedupe_and_sort(results, limit)
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
def query_semantic_legacy(text, limit:, scope:, mode:)
|
|
461
|
+
query_semantic_single(@legacy_store, text, limit: limit, mode: mode, source: :legacy)
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
def query_semantic_single(store, text, limit:, mode:, source:)
|
|
465
|
+
vector_results = []
|
|
466
|
+
text_results = []
|
|
467
|
+
|
|
468
|
+
# Vector search mode
|
|
469
|
+
if mode == :vector || mode == :both
|
|
470
|
+
vector_results = search_by_vector(store, text, limit, source)
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
# Text search mode (FTS)
|
|
474
|
+
if mode == :text || mode == :both
|
|
475
|
+
text_results = search_by_fts(store, text, limit, source)
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
# Merge and deduplicate
|
|
479
|
+
merge_search_results(vector_results, text_results, limit)
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
def search_by_vector(store, query_text, limit, source)
|
|
483
|
+
# Generate query embedding
|
|
484
|
+
query_embedding = @embedding_generator.generate(query_text)
|
|
485
|
+
|
|
486
|
+
# Load facts with embeddings
|
|
487
|
+
facts_data = store.facts_with_embeddings(limit: 5000)
|
|
488
|
+
return [] if facts_data.empty?
|
|
489
|
+
|
|
490
|
+
# Parse embeddings and prepare candidates
|
|
491
|
+
candidates = Core::EmbeddingCandidateBuilder.build_candidates(facts_data)
|
|
492
|
+
|
|
493
|
+
return [] if candidates.empty?
|
|
494
|
+
|
|
495
|
+
# Calculate similarities and rank
|
|
496
|
+
top_matches = Embeddings::Similarity.top_k(query_embedding, candidates, limit)
|
|
497
|
+
|
|
498
|
+
# Batch fetch full fact details
|
|
499
|
+
fact_ids = top_matches.map { |m| m[:candidate][:fact_id] }
|
|
500
|
+
facts_by_id = batch_find_facts(store, fact_ids)
|
|
501
|
+
receipts_by_fact_id = batch_find_receipts(store, fact_ids)
|
|
502
|
+
|
|
503
|
+
# Build results with similarity scores
|
|
504
|
+
Core::ResultBuilder.build_results_with_scores(
|
|
505
|
+
top_matches,
|
|
506
|
+
facts_by_id: facts_by_id,
|
|
507
|
+
receipts_by_fact_id: receipts_by_fact_id,
|
|
508
|
+
source: source
|
|
509
|
+
)
|
|
510
|
+
end
|
|
511
|
+
|
|
512
|
+
def search_by_fts(store, query_text, limit, source)
|
|
513
|
+
# Use existing FTS search infrastructure
|
|
514
|
+
fts = Index::LexicalFTS.new(store)
|
|
515
|
+
content_ids = fts.search(query_text, limit: limit * 2)
|
|
516
|
+
|
|
517
|
+
return [] if content_ids.empty?
|
|
518
|
+
|
|
519
|
+
# Find facts from content items
|
|
520
|
+
fact_ids = store.provenance
|
|
521
|
+
.where(content_item_id: content_ids)
|
|
522
|
+
.select(:fact_id)
|
|
523
|
+
.distinct
|
|
524
|
+
.map { |row| row[:fact_id] }
|
|
525
|
+
|
|
526
|
+
return [] if fact_ids.empty?
|
|
527
|
+
|
|
528
|
+
# Batch fetch facts
|
|
529
|
+
facts_by_id = batch_find_facts(store, fact_ids)
|
|
530
|
+
receipts_by_fact_id = batch_find_receipts(store, fact_ids)
|
|
531
|
+
|
|
532
|
+
results = Core::ResultBuilder.build_results(
|
|
533
|
+
fact_ids,
|
|
534
|
+
facts_by_id: facts_by_id,
|
|
535
|
+
receipts_by_fact_id: receipts_by_fact_id,
|
|
536
|
+
source: source,
|
|
537
|
+
similarity: 0.5 # Default score for FTS results
|
|
538
|
+
)
|
|
539
|
+
results.take(limit)
|
|
540
|
+
end
|
|
541
|
+
|
|
542
|
+
def merge_search_results(vector_results, text_results, limit)
|
|
543
|
+
Core::FactRanker.merge_search_results(vector_results, text_results, limit)
|
|
544
|
+
end
|
|
545
|
+
|
|
546
|
+
# Multi-concept search helpers
|
|
547
|
+
|
|
548
|
+
def query_concepts_dual(concepts, limit:, scope:)
|
|
549
|
+
template = Recall::DualQueryTemplate.new(@manager)
|
|
550
|
+
results = template.execute(scope: scope, limit: limit) do |store, source|
|
|
551
|
+
query_concepts_single(store, concepts, limit: limit * 2, source: source)
|
|
552
|
+
end
|
|
553
|
+
# Deduplicate and sort by average similarity
|
|
554
|
+
dedupe_by_fact_id(results, limit)
|
|
555
|
+
end
|
|
556
|
+
|
|
557
|
+
def query_concepts_legacy(concepts, limit:, scope:)
|
|
558
|
+
query_concepts_single(@legacy_store, concepts, limit: limit, source: :legacy)
|
|
559
|
+
end
|
|
560
|
+
|
|
561
|
+
def query_concepts_single(store, concepts, limit:, source:)
|
|
562
|
+
# I/O: Search each concept independently with higher limit for intersection
|
|
563
|
+
concept_results = concepts.map do |concept|
|
|
564
|
+
search_by_vector(store, concept, limit * 5, source)
|
|
565
|
+
end
|
|
566
|
+
|
|
567
|
+
# Pure logic: Rank by average similarity across all concepts
|
|
568
|
+
Core::ConceptRanker.rank_by_concepts(concept_results, limit)
|
|
569
|
+
end
|
|
570
|
+
|
|
571
|
+
def dedupe_by_fact_id(results, limit)
|
|
572
|
+
Core::FactRanker.dedupe_by_fact_id(results, limit)
|
|
573
|
+
end
|
|
507
574
|
end
|
|
508
575
|
end
|