Module: ClaudeMemory::Dashboard::ScopedFactResolver
- Defined in:
- lib/claude_memory/dashboard/scoped_fact_resolver.rb
Overview
Resolves fact IDs from recall/context-injection event details back to the facts they actually referenced, respecting scope. Fact IDs autoincrement per-DB, so a bare numeric ID is ambiguous — project fact #1 and global fact #1 are different facts.
Reads in priority order:
-
top_facts_by_scope (new, authoritative) — already scope-tagged
-
top_fact_ids + single-scope results_by_scope — historical events from before the fix; if the recall only touched one scope, every ID must belong to that scope
-
top_fact_ids alone — last-resort fallback; default to project
Every reader in the dashboard goes through this so the scope bug can’t reappear in one spot while being fixed in another.
Class Method Summary collapse
- .extract_top_facts_by_scope(details) ⇒ Object
-
.flat_pairs(scoped_ids) ⇒ Array<Array(String, Integer)>
Flat list of unique scoped pairs — handy for counting unique facts referenced across a set of events.
-
.resolve(manager, scoped_ids) ⇒ Array<Hash>
Resolve an entire => [ids] hash into ordered fact rows.
-
.scoped_ids_from_details(details) ⇒ Hash{String => Array<Integer>}
Normalize event details into a => [ids] hash.
-
.single_scope_from(results_by_scope) ⇒ Object
If a recall’s results came from exactly one scope, every fact ID must belong to that scope.
Class Method Details
.extract_top_facts_by_scope(details) ⇒ Object
75 76 77 78 79 80 81 82 |
# File 'lib/claude_memory/dashboard/scoped_fact_resolver.rb', line 75 def extract_top_facts_by_scope(details) raw = details[:top_facts_by_scope] || details["top_facts_by_scope"] return {} unless raw.is_a?(Hash) raw.each_with_object({}) do |(scope, ids), acc| cleaned = Array(ids).map(&:to_i).reject(&:zero?) acc[scope.to_s] = cleaned unless cleaned.empty? end end |
.flat_pairs(scoped_ids) ⇒ Array<Array(String, Integer)>
Flat list of unique scoped pairs — handy for counting unique facts referenced across a set of events.
70 71 72 73 |
# File 'lib/claude_memory/dashboard/scoped_fact_resolver.rb', line 70 def flat_pairs(scoped_ids) return [] if scoped_ids.nil? || scoped_ids.empty? scoped_ids.flat_map { |scope, ids| ids.map { |id| [scope.to_s, id.to_i] } }.uniq end |
.resolve(manager, scoped_ids) ⇒ Array<Hash>
Resolve an entire => [ids] hash into ordered fact rows. Preserves the input order per scope so “top fact” ordering survives the round trip.
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
# File 'lib/claude_memory/dashboard/scoped_fact_resolver.rb', line 46 def resolve(manager, scoped_ids) return [] if scoped_ids.nil? || scoped_ids.empty? results = [] scoped_ids.each do |scope, ids| next if ids.nil? || ids.empty? store = manager.store_if_exists(scope.to_s) next unless store rows = store.facts.where(id: ids.map(&:to_i)).all next if rows.empty? index = ids.each_with_index.to_h { |id, i| [id.to_i, i] } rows.sort_by! { |r| index[r[:id]] || Float::INFINITY } presented = FactPresenter.new(store).list_summary(rows) presented.each { |f| results << f.merge(source: scope.to_s) } end results rescue Sequel::DatabaseError => e ClaudeMemory.logger.debug("ScopedFactResolver#resolve failed: #{e.}") [] end |
.scoped_ids_from_details(details) ⇒ Hash{String => Array<Integer>}
Normalize event details into a => [ids] hash. Returns an empty hash when no fact-ID references are present.
28 29 30 31 32 33 34 35 36 37 38 |
# File 'lib/claude_memory/dashboard/scoped_fact_resolver.rb', line 28 def scoped_ids_from_details(details) return {} unless details.is_a?(Hash) = extract_top_facts_by_scope(details) return if .any? flat_ids = Array(details[:top_fact_ids] || details["top_fact_ids"]).map(&:to_i).reject(&:zero?) return {} if flat_ids.empty? scope = single_scope_from(details[:results_by_scope] || details["results_by_scope"]) {scope || "project" => flat_ids} end |
.single_scope_from(results_by_scope) ⇒ Object
If a recall’s results came from exactly one scope, every fact ID must belong to that scope. Returns the scope name, or nil when the recall touched multiple scopes (can’t disambiguate) or none.
87 88 89 90 91 92 |
# File 'lib/claude_memory/dashboard/scoped_fact_resolver.rb', line 87 def single_scope_from(results_by_scope) return nil unless results_by_scope.is_a?(Hash) scopes_with_hits = results_by_scope.reject { |_, count| count.nil? || count.zero? }.keys return nil unless scopes_with_hits.size == 1 scopes_with_hits.first.to_s end |