Module: ClaudeMemory::Dashboard::Efficacy::Reporter

Defined in:
lib/claude_memory/dashboard/efficacy.rb

Overview

Pure report calculator for recall activity events. Takes a list of already-loaded events and produces the shaped metrics the dashboard renders. No I/O, no database access — separating compute from loading keeps the aggregation fast-testable and portable across event sources.

Expected event shape (matches ActivityLog.recent output):

{
  id:, event_type:, status:, duration_ms:, session_id:,
  occurred_at:, details: {tool:, query:, result_count:,
                          results_by_scope: {"project" => N, "global" => M}, ...}
}

Constant Summary collapse

RECALL_TRACE_LIMIT =
50
MEMORY_GAPS_LIMIT =
10

Class Method Summary collapse

Class Method Details

.median(values) ⇒ Object

Sorted median — returns 0 for empty input, midpoint average for even counts.



57
58
59
60
61
62
63
64
65
66
# File 'lib/claude_memory/dashboard/efficacy.rb', line 57

def median(values)
  return 0 if values.empty?
  sorted = values.sort
  mid = sorted.size / 2
  if sorted.size.odd?
    sorted[mid]
  else
    ((sorted[mid - 1] + sorted[mid]) / 2.0).round(1)
  end
end

.memory_gaps(events) ⇒ Object



95
96
97
98
99
100
101
102
103
104
105
106
107
# File 'lib/claude_memory/dashboard/efficacy.rb', line 95

def memory_gaps(events)
  events
    .select { |e| (e.dig(:details, :result_count) || 0).zero? && e.dig(:details, :query) }
    .first(MEMORY_GAPS_LIMIT)
    .map { |e|
      {
        tool: e.dig(:details, :tool),
        query: e.dig(:details, :query),
        occurred_at: e[:occurred_at],
        occurred_ago: Core::RelativeTime.format(e[:occurred_at])
      }
    }
end

.percentage(part, whole) ⇒ Object

Percentage with zero-safe denominator, rounded to 1 decimal.



51
52
53
54
# File 'lib/claude_memory/dashboard/efficacy.rb', line 51

def percentage(part, whole)
  return 0 if whole.to_i.zero?
  (part.to_f / whole * 100).round(1)
end

.recall_trace(events) ⇒ Object



109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
# File 'lib/claude_memory/dashboard/efficacy.rb', line 109

def recall_trace(events)
  events.first(RECALL_TRACE_LIMIT).map { |e|
    {
      id: e[:id],
      tool: e.dig(:details, :tool),
      query: e.dig(:details, :query),
      result_count: e.dig(:details, :result_count) || 0,
      duration_ms: e[:duration_ms],
      session_id: e[:session_id],
      status: e[:status],
      occurred_at: e[:occurred_at],
      occurred_ago: Core::RelativeTime.format(e[:occurred_at])
    }
  }
end

.report(events, timeframe: {}) ⇒ Hash

Returns the efficacy payload.

Parameters:

  • events (Array<Hash>)

    recall activity events (post-filter)

  • timeframe (Hash) (defaults to: {})

    session_id: echoed into the response

Returns:

  • (Hash)

    the efficacy payload



28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# File 'lib/claude_memory/dashboard/efficacy.rb', line 28

def report(events, timeframe: {})
  result_counts = events.map { |e| e.dig(:details, :result_count) || 0 }
  latencies = events.map { |e| e[:duration_ms] }.compact
  successful = events.count { |e| e[:status] == "success" && (e.dig(:details, :result_count) || 0) > 0 }
  empty = events.count { |e| e[:status] == "success" && (e.dig(:details, :result_count) || 0) == 0 }

  {
    timeframe: {since: timeframe[:since], session_id: timeframe[:session_id]},
    recall_events: events.size,
    successful_recalls: successful,
    empty_recalls: empty,
    hit_rate: percentage(successful, events.size),
    total_results_served: result_counts.sum,
    median_results_per_query: median(result_counts),
    median_latency_ms: median(latencies),
    tool_mix: tool_mix(events),
    source_contribution: source_contribution(events),
    memory_gaps: memory_gaps(events),
    recall_trace: recall_trace(events)
  }
end

.source_contribution(events) ⇒ Object

Aggregate results_by_scope across events. Reveals where returned facts actually came from — the one question only efficacy can answer.



85
86
87
88
89
90
91
92
93
# File 'lib/claude_memory/dashboard/efficacy.rb', line 85

def source_contribution(events)
  totals = Hash.new(0)
  events.each do |e|
    by_scope = e.dig(:details, :results_by_scope)
    next unless by_scope.is_a?(Hash)
    by_scope.each { |scope, n| totals[scope.to_s] += n.to_i }
  end
  totals.empty? ? [] : totals.map { |scope, count| {scope: scope, count: count} }.sort_by { |r| -r[:count] }
end

.tool_mix(events) ⇒ Object



68
69
70
71
72
73
74
75
76
77
78
79
80
81
# File 'lib/claude_memory/dashboard/efficacy.rb', line 68

def tool_mix(events)
  events
    .group_by { |e| e.dig(:details, :tool) || "(unknown)" }
    .map { |tool, rows|
      hits = rows.count { |r| (r.dig(:details, :result_count) || 0) > 0 }
      {
        tool: tool,
        count: rows.size,
        hits: hits,
        hit_rate: percentage(hits, rows.size)
      }
    }
    .sort_by { |row| -row[:count] }
end