claude_memory 0.9.1 → 0.11.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.
Files changed (77) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/memory.sqlite3 +0 -0
  3. data/.claude/skills/dashboard/SKILL.md +42 -0
  4. data/.claude-plugin/marketplace.json +1 -1
  5. data/.claude-plugin/plugin.json +1 -1
  6. data/CHANGELOG.md +130 -0
  7. data/CLAUDE.md +30 -6
  8. data/README.md +66 -2
  9. data/db/migrations/015_add_activity_events.rb +26 -0
  10. data/db/migrations/016_add_moment_feedback.rb +22 -0
  11. data/db/migrations/017_add_last_recalled_at.rb +15 -0
  12. data/docs/1_0_punchlist.md +371 -0
  13. data/docs/EXAMPLES.md +41 -2
  14. data/docs/GETTING_STARTED.md +33 -4
  15. data/docs/architecture.md +22 -7
  16. data/docs/audit-queries.md +131 -0
  17. data/docs/dashboard.md +192 -0
  18. data/docs/improvements.md +650 -9
  19. data/docs/influence/cq.md +187 -0
  20. data/docs/plugin.md +13 -6
  21. data/docs/quality_review.md +524 -172
  22. data/docs/reflection_memory_as_accumulating_judgment.md +67 -0
  23. data/lib/claude_memory/activity_log.rb +86 -0
  24. data/lib/claude_memory/commands/census_command.rb +210 -0
  25. data/lib/claude_memory/commands/completion_command.rb +3 -0
  26. data/lib/claude_memory/commands/dashboard_command.rb +54 -0
  27. data/lib/claude_memory/commands/dedupe_conflicts_command.rb +55 -0
  28. data/lib/claude_memory/commands/digest_command.rb +273 -0
  29. data/lib/claude_memory/commands/hook_command.rb +61 -2
  30. data/lib/claude_memory/commands/initializers/hooks_configurator.rb +7 -4
  31. data/lib/claude_memory/commands/reclassify_references_command.rb +56 -0
  32. data/lib/claude_memory/commands/registry.rb +7 -1
  33. data/lib/claude_memory/commands/show_command.rb +90 -0
  34. data/lib/claude_memory/commands/skills/distill-transcripts.md +13 -1
  35. data/lib/claude_memory/commands/stats_command.rb +131 -2
  36. data/lib/claude_memory/commands/sweep_command.rb +2 -0
  37. data/lib/claude_memory/configuration.rb +16 -0
  38. data/lib/claude_memory/core/relative_time.rb +9 -0
  39. data/lib/claude_memory/dashboard/api.rb +610 -0
  40. data/lib/claude_memory/dashboard/conflicts.rb +279 -0
  41. data/lib/claude_memory/dashboard/efficacy.rb +127 -0
  42. data/lib/claude_memory/dashboard/fact_presenter.rb +109 -0
  43. data/lib/claude_memory/dashboard/health.rb +175 -0
  44. data/lib/claude_memory/dashboard/index.html +2707 -0
  45. data/lib/claude_memory/dashboard/knowledge.rb +136 -0
  46. data/lib/claude_memory/dashboard/moments.rb +244 -0
  47. data/lib/claude_memory/dashboard/reuse.rb +97 -0
  48. data/lib/claude_memory/dashboard/scoped_fact_resolver.rb +95 -0
  49. data/lib/claude_memory/dashboard/server.rb +211 -0
  50. data/lib/claude_memory/dashboard/timeline.rb +68 -0
  51. data/lib/claude_memory/dashboard/trust.rb +454 -0
  52. data/lib/claude_memory/distill/bare_conclusion_detector.rb +71 -0
  53. data/lib/claude_memory/distill/reference_material_detector.rb +78 -0
  54. data/lib/claude_memory/hook/auto_memory_mirror.rb +112 -0
  55. data/lib/claude_memory/hook/context_injector.rb +97 -3
  56. data/lib/claude_memory/hook/handler.rb +191 -3
  57. data/lib/claude_memory/mcp/handlers/management_handlers.rb +8 -0
  58. data/lib/claude_memory/mcp/query_guide.rb +11 -0
  59. data/lib/claude_memory/mcp/text_summary.rb +29 -0
  60. data/lib/claude_memory/mcp/tool_definitions.rb +13 -0
  61. data/lib/claude_memory/mcp/tools.rb +148 -0
  62. data/lib/claude_memory/publish.rb +13 -21
  63. data/lib/claude_memory/recall/stale_detector.rb +67 -0
  64. data/lib/claude_memory/resolve/predicate_policy.rb +2 -0
  65. data/lib/claude_memory/resolve/resolver.rb +41 -11
  66. data/lib/claude_memory/store/llm_cache.rb +68 -0
  67. data/lib/claude_memory/store/metrics_aggregator.rb +96 -0
  68. data/lib/claude_memory/store/schema_manager.rb +1 -1
  69. data/lib/claude_memory/store/sqlite_store.rb +47 -143
  70. data/lib/claude_memory/store/store_manager.rb +29 -0
  71. data/lib/claude_memory/sweep/maintenance.rb +216 -0
  72. data/lib/claude_memory/sweep/recall_timestamp_refresher.rb +83 -0
  73. data/lib/claude_memory/sweep/sweeper.rb +2 -0
  74. data/lib/claude_memory/templates/hooks.example.json +5 -0
  75. data/lib/claude_memory/version.rb +1 -1
  76. data/lib/claude_memory.rb +24 -0
  77. metadata +51 -1
@@ -1,285 +1,637 @@
1
1
  # Code Quality Review - Ruby Best Practices
2
2
 
3
- **Review Date:** 2026-03-19
4
- **Previous Review:** 2026-03-09
5
- **Last Quality Update:** 2026-03-20 (13 items completed)
3
+ **Review Date:** 2026-04-28
4
+ **Previous Review:** 2026-04-22 (6 days ago)
5
+ **Last Quality Update:** 2026-04-22 (4 items completed — LLMCache + MetricsAggregator extractions, Publish DRY, Dashboard specs)
6
+ **Codebase Growth:** 17,014 → 19,025 LOC (+2,011, +12% in 6 days)
7
+
8
+ > **Post-review update (2026-04-28, same session):** Items #34, #32, #35, #36 (quick wins + missing command specs) and the first two of the six proposed `Dashboard::API` extractions (#31: `Dashboard::Timeline` + `Dashboard::Health`) landed before the v0.10.0 release commit. `dashboard/api.rb` dropped 807 → 607 LOC (-200, -25%), reversing the regression and bringing it back under the 2026-04-22 baseline (627). The remaining four extractions (`RecallQuery`, `RecallTriggerFinder`, `UserPromptExtractor`, `FactsQuery`) are deferred to 0.10.1. Findings below describe the *pre-update* state captured at review time.
9
+
10
+ ---
11
+
12
+ ## Post-0.11 Investigation: Hallucination Rate Metric Calibration (2026-04-30)
13
+
14
+ When #48 (hallucination-rate metric) was first run against this project's real DB, it surfaced numbers that *looked* alarming:
15
+
16
+ - Quality score: 39/100
17
+ - Bare conclusions: 34 / 59 active facts (57.6%)
18
+ - 7-day rejection rate: 27 of 32 facts (84.4%)
19
+
20
+ The first read was that the LLM extractor was producing noise faster than usable knowledge. Per `improvements.md` #60, four causes were proposed; diagnostics ran 2026-04-30:
21
+
22
+ | Cause | Verdict | Evidence |
23
+ |---|---|---|
24
+ | Prompt drift in `distill-transcripts.md` | **Confirmed dominant** | 34/35 (97%) bare-conclusion facts pre-date the reason-clause prompt commit `f22d12f` (2026-04-20). Only 1 was created post-commit (and that one is a meta-convention added during this session). |
25
+ | Auto-memory mirror regurgitation | Rejected | 0/35 substring matches in `~/.claude/projects/.../memory/*.md`. Auto-memory mirror only landed in 0.10.0 (2026-04-28), after the bare-fact creation window — temporally impossible to be the source. |
26
+ | `ReferenceMaterialDetector` predicate scope too narrow | Not material | Only 3/35 bare facts are `decision`-predicate; 0 of those match the strong reference-material patterns. Expanding `GUARDED_PREDICATES` would not move the needle on the bare-conclusion count. |
27
+ | Junky corpus / rejection cluster | **Confirmed in single class** | All 27 rejected facts in the 7-day window are `uses_database` (18) or `deployment_platform` (9), all with `session_id=nil` (MCP-originated, almost certainly `/study-repo` runs misattributing external-project tech to this project), all from 2026-04-23 to 04-24. Systemic single-class failure, correctly cleaned up after detection — not ongoing extraction noise. |
28
+
29
+ **What this means for #48 as currently shipped:**
30
+
31
+ The metric is *technically correct* but *pragmatically misleading*. It bakes historical noise (pre-prompt-commit bare conclusions) into a signal that users will read as "ongoing extraction quality." A 57.6% bare-conclusion rate looks like the LLM is broken; in reality the live extraction rate (post-2026-04-20) is ~3% (1 bare fact out of ~30+ created since the prompt commit landed).
32
+
33
+ The 84% rejection rate has a similar structural issue: it counts cleanup of a bursty `/study-repo` regression against the active-facts denominator, not against the actual extraction quality of the live window.
34
+
35
+ **Quick fix shipping now (this session):** restrict `quality_score` and the digest's "Quality" section to facts created within the same 30-day window already used by `token_budget`. Surface a separate "historical" line so users can see both numbers, but the headline is the live one. This makes the metric actionable: a high live bare-conclusion rate = live LLM calibration drift; a high historical rate = legacy data, not a current alarm.
36
+
37
+ **Deferred to 0.12 / 1.x:**
38
+
39
+ 1. The systemic `/study-repo` misattribution failure mode (cause 4) deserves its own guard. External-project READMEs being studied should land in `reference` predicates, not as `uses_database`/`deployment_platform`. Track this as a follow-up entry.
40
+ 2. A backfill/cleanup pass on the 34 historical bare-conclusion facts: either retroactive rejection, or a one-shot reclassification that moves them to a `legacy_observation` predicate that the prompt's reason-clause requirement doesn't apply to.
41
+ 3. The metric's calibration assumes "bare conclusion = bad", but spot-checking shows several flagged facts are perfectly informative ("MCP tools return dual content + structuredContent via TextSummary module") — they describe mechanics implicitly. The vocabulary may itself be too strict; revisit during 1.0 soak with real usage data.
42
+
43
+ **Process win:** the metric did its job — it surfaced a real signal that would otherwise have stayed invisible, and the investigation distinguished historical noise from live calibration. Without #48 we'd have no way to know.
6
44
 
7
45
  ---
8
46
 
9
47
  ## Executive Summary
10
48
 
11
- The codebase has grown from 11,392 to 12,239 LOC since the Mar 9 review. A configurable embedding provider system was added (+847 LOC across 5 new files and 8 modified files). The three watch-list files have grown further: `tools.rb` (728→745), `recall.rb` (681→727), `sqlite_store.rb` (547→547, unchanged). All three remain above 500 lines.
49
+ Six days, +2,011 LOC. The headline finding: **the watch-list item from 2026-04-22 (#28 extract per-endpoint helpers from `Dashboard::API`) was not just deferred, it actively regressed.** `dashboard/api.rb` grew from 627 807 LOC (+180, +29%), is now the only file in `lib/` over 750 lines, and gained four new methods all exceeding 15 lines. Method-size pressure increased: the previous worst case (`recall` at 39 lines) is now `timeline` at 52 lines, and the file has 11 methods over 15 lines (vs 11 last review) but with a higher mean.
50
+
51
+ The codebase is otherwise healthy. Five **new dashboard subsystems** (`moments.rb`, `reuse.rb`, `trust.rb`, `scoped_fact_resolver.rb`, plus `efficacy.rb` carried over) shipped with **direct spec files**. Three new schema migrations (v15/v16/v17) all wrap DDL with idempotent `create_table?` / `add_column` and have **per-migration specs plus round-trip specs from v12, v13, and v14 forward to v17** (a deliberate process improvement noted in `feedback_round_trip_migration_specs.md`). Four new sweep operations (`dedupe_open_conflicts`, `reclassify_references`) gained spec coverage in `sweep/maintenance_spec.rb`.
52
+
53
+ **What regressed:**
54
+ - `dashboard/api.rb` 627 → 807 LOC (+180). Watch-list item not addressed.
55
+ - `sweep/maintenance.rb` 334 → 456 LOC (+122). Two of the four new methods are 50+ lines (`dedupe_open_conflicts` 58, `restore_multi_value_supersessions` 57).
56
+ - Sleep-based test latency grew. `dashboard/moments_spec.rb` and `dashboard/api_spec.rb` add 4 more `sleep 1.1` calls (+4.4s wall). Total sleep-based test cost in suite is now ~8.4s, up from ~4s.
57
+ - One new code smell: `digest_command.rb:128` calls `Dashboard::Trust.new(manager).send(:utilization)` — reaches into a private method instead of exposing `utilization` on the public Trust API.
12
58
 
13
- Five items were resolved in the Mar 19 quality update session: the Shortcuts scope bug (correctness), ApiAdapter exception typing, silent exception logging (3 locations), dead Configuration accessors removed, and `index_database` decomposed into focused methods. No known correctness bugs remain.
59
+ **What was resolved or improved since 2026-04-22:**
60
+ - Round-trip migration specs from v12/v13/v14 → v17 added (release-blocker per `feedback_round_trip_migration_specs.md`).
61
+ - Per-migration specs for v13–v17 added under `spec/claude_memory/store/migrations/`.
62
+ - New dashboard subsystems shipped *with* specs (good pattern — Reuse, Moments, Trust, Knowledge, ScopedFactResolver all have direct specs).
63
+ - `lib/claude_memory/store/sqlite_store.rb` only grew 40 LOC (544 → 584); regrowth controlled.
14
64
 
15
- **Resolved this session:** 13 items (#1 Tools god object, #2 Recall legacy/dual strategy, #3 Shortcuts scope, #4 SQLiteStore extraction, #9 test coverage for critical files, NEW-1 ApiError, NEW-2 dead accessors, NEW-3 silent rescues, #5 index_database decomposition, #6 promote_fact transaction, #7 provenance nil content_item_id, #12 Resolver mutable state, #19 SnippetExtractor DRY)
16
- **Resolved since last review:** 3 additional items (ExportCommand N+1, `discover_other_projects` bare rescue, embedding test coverage)
17
- **Total remaining:** 12 items (0 high, 5 medium, 4 low, 5 carried forward — all structural work complete)
65
+ **New this review:** 4 items. 1 high-priority (Dashboard::API extraction, now urgent), 1 medium (`sweep/maintenance.rb` size), 1 low (`Time.parse` duplication across dashboard files), 1 quick win (`.send(:utilization)` smell in digest).
18
66
 
19
67
  ### Current Strengths
20
68
 
21
- - Functional core: 20+ pure logic classes with zero I/O
22
- - Domain objects: properly frozen and self-validating
23
- - Null object pattern: NullFact, NullExplanation
24
- - Result monad: Core::Result for Success/Failure
25
- - 100% frozen_string_literal compliance (117 files)
26
- - 1.84:1 test-to-code ratio (22,563 spec : 12,239 lib)
27
- - New embedding subsystem: shared RSpec examples, duck-typed providers, Data.define value object
28
- - Zero N+1 patterns in hot paths
29
- - Proper batch loading via FactQueryBuilder
30
- - Content-addressed dedup in IndexCommand
31
- - DimensionCheck value object (functional core, no side effects)
32
- - Zero known correctness bugs
69
+ - Migrations now ship with per-migration specs **and** cross-version round-trip specs — a deliberate release-readiness improvement that landed during this window
70
+ - New dashboard subsystems all have direct specs; spec count grew 156 → 188 files (+32)
71
+ - Domain objects, frozen string literals, transaction wrapping, no raw SQL, no N+1 in hot paths — all preserved
72
+ - Five files >300 LOC last review; eight now, but mostly because of new modules carrying single responsibilities (Moments 244, Trust 284, Conflicts 285), not god-object regrowth
33
73
 
34
74
  ---
35
75
 
36
76
  ## 1. Sandi Metz Perspective
37
77
 
38
78
  ### What's Been Fixed ✅
39
- - New embedding providers follow duck typing contract (no base class inheritance)
40
- - Shared RSpec examples verify provider contract (`spec/support/shared_examples/embedding_provider.rb`)
41
- - `set_meta`/`get_meta` promoted to public API (needed by DimensionCheck, VectorIndex)
42
- - ExportCommand N+1 eliminated with batch loading
43
- - **Shortcuts scope bug fixed** scopes changed from symbols to strings to match DualQueryTemplate comparisons
44
- - **`index_database` decomposed** — split 130-line method into 5 focused methods: `index_database` (orchestrator), `handle_dimension_mismatch`, `find_facts_to_index`, `run_indexing`, `process_batch`, `report_dedup_stats`
45
- - **Tools god object eliminated** — split 745-line `Tools` class into thin 104-line dispatcher + 6 handler modules: `QueryHandlers` (90), `ShortcutHandlers` (37), `ContextHandlers` (38), `ManagementHandlers` (124), `StatsHandlers` (188), `SetupHandlers` (211). No file exceeds 211 lines. Public API unchanged.
46
- - **Recall legacy/dual duplication eliminated** — split 727-line `Recall` into thin 94-line facade + strategy engines: `DualEngine` (101), `LegacyEngine` (134), shared `QueryCore` module (357). The `if @legacy_mode` branching in every public method is replaced by engine delegation. Public API unchanged.
79
+
80
+ - `SQLiteStore` regrowth held steady at 584 LOC after the 2026-04-22 LLMCache + MetricsAggregator extractions; only +40 LOC over 6 days, and that's adding two new tables (`moment_feedback`, `activity_events`) with their CRUD wrappers
81
+ - New dashboard subsystems each landed under 300 LOC with focused responsibilities
82
+ - `Moments` (244 LOC) feed-shape construction, no DB writes
83
+ - `Trust` (284 LOC)sidebar aggregations, all reads
84
+ - `Reuse` (97 LOC) top-N "most-used" panel
85
+ - `Knowledge` (136 LOC) fact summary panel
86
+ - `ScopedFactResolver` (95 LOC) pure helper
87
+ - Round-trip migration specs (`round_trip_v12_to_v17_spec.rb` etc.) — Sandi-style "test the contract, not the implementation"
47
88
 
48
89
  ### Critical Issues 🔴
49
90
 
50
- None remaining.
91
+ #### A. `Dashboard::API` regressed: 627 → 807 LOC (+29%) — **carried-forward item became urgent**
92
+
93
+ `lib/claude_memory/dashboard/api.rb` was the watch-list item at the close of the 2026-04-22 review (#28). Six days later, instead of shrinking via per-endpoint extraction, it absorbed:
94
+
95
+ - `find_recall_trigger` (lib/claude_memory/dashboard/api.rb:193) — 32 lines, 5 SQL constructions, calls 3 helpers, JSON-parses event details
96
+ - `extract_user_prompt` (lib/claude_memory/dashboard/api.rb:237) — 29 lines, JSONL parsing, content type narrowing, plumbing-noise filtering
97
+ - `facts` (lib/claude_memory/dashboard/api.rb:373) — 39 lines (was 26), now also handles `stale_only` filtering with cross-store exclusion
98
+ - `facts_seen_in_recent_recalls` (lib/claude_memory/dashboard/api.rb:418) — 20 lines, scoped-pair aggregation
99
+ - `efficacy` (lib/claude_memory/dashboard/api.rb:439) — 31 lines (was 23), now branches on session_id with time-window correlation
100
+ - New micro-endpoints: `moments`, `trust`, `knowledge`, `reuse`, `moment_feedback`, `clear_moment_feedback`, `fact_detail`, `promote_fact`, `reject_fact`
101
+
102
+ The class now has **42 methods** (up from ~31) and **8 methods over 20 lines**. The methods that delegate cleanly (`conflicts`, `moments`, `trust`, `knowledge`, `reuse` — all 1-liners) are the right pattern; the rest of the file should follow that pattern.
103
+
104
+ **Method size table (current state):**
105
+
106
+ | Method | Line | Size | Concern |
107
+ |---|---|---|---|
108
+ | `timeline` | 471 | 52 | 3 separate Sequel aggregations + Ruby-side merge — should be `Dashboard::Timeline` |
109
+ | `vec_health` | 759 | 46 | Branchy status derivation over coverage stats |
110
+ | `recall` | 315 | 41 | Result flattening + bare rescue + actionable-hint branching |
111
+ | `facts` | 373 | 39 | Pagination + filter + cross-store stale exclusion |
112
+ | `activity_detail` | 149 | 37 | Joined fetch + linked facts + recall-trigger correlation |
113
+ | `hooks_health` | 704 | 32 | Multi-state status with fix messages |
114
+ | `find_recall_trigger` | 193 | 32 | Time-window query with session_id fallback |
115
+ | `efficacy` | 439 | 31 | Session-scope vs window-scope branching |
116
+ | `extract_user_prompt` | 237 | 29 | JSONL reverse-walk + plumbing filter |
117
+ | `session_summary` | 119 | 29 | Multi-event-type aggregation |
118
+ | `db_stats` | 647 | 28 | Predicate counts + entity counts + size stats |
119
+
120
+ **Proposed extractions** (each candidate is testable in isolation):
121
+
122
+ ```ruby
123
+ # lib/claude_memory/dashboard/timeline.rb — pure aggregation
124
+ class Timeline
125
+ def initialize(manager) = @manager = manager
126
+ def days = { days: build_days }
127
+ private
128
+ def build_days
129
+ return [] unless store
130
+ fact_rows, content_rows, event_rows = load_aggregations
131
+ merge_into_days(fact_rows, content_rows, event_rows)
132
+ end
133
+ end
134
+
135
+ # lib/claude_memory/dashboard/health.rb — already 4 health checks (db, hooks, vec, vectors)
136
+ class Health
137
+ def report = { status: overall(checks), checks: checks, version: VERSION }
138
+ private
139
+ def checks = [db_health("global"), db_health("project"), hooks_health, vec_health]
140
+ end
141
+
142
+ # lib/claude_memory/dashboard/recall_query.rb — wraps live recall + actionable error mapping
143
+ class RecallQuery
144
+ def call(params) = format_response(run(params))
145
+ end
146
+
147
+ # lib/claude_memory/dashboard/recall_trigger_finder.rb — pure time-window correlation
148
+ # lib/claude_memory/dashboard/user_prompt_extractor.rb — pure JSONL parsing
149
+ # lib/claude_memory/dashboard/facts_query.rb — pagination + stale exclusion
150
+ ```
151
+
152
+ After these extractions `api.rb` should drop to **~250 LOC** of routing-and-delegation. The pattern was already proven by `Conflicts` / `Moments` / `Trust` / `Knowledge` / `Reuse`.
153
+
154
+ **File:** `lib/claude_memory/dashboard/api.rb`
155
+ **Effort:** 4–6 hours (5 extractions, each with a focused spec)
156
+ **Priority:** 🔴 — was medium last review, escalates to high because the trend line points at 1,000+ LOC by next sprint if uncorrected
157
+ **Expert principle:** Sandi Metz SRP; Bernhardt boundaries; Beck simple design
51
158
 
52
- ### High Priority Issues
159
+ ### Medium Issues 🟡
160
+
161
+ #### B. `sweep/maintenance.rb` grew 334 → 456 LOC (+122, +37%)
162
+
163
+ Last review noted maintenance.rb at 334 (after dropping from 456 earlier — see the review's appendix B). It's now back at 456. Two large methods landed:
53
164
 
54
- None remaining. All >500-line files have been decomposed.
165
+ - `dedupe_open_conflicts` (lib/claude_memory/sweep/maintenance.rb:273) 58 lines, multi-step transaction (group resolve duplicates → reattach provenance → reject losers → mark conflicts resolved)
166
+ - `reclassify_references` (lib/claude_memory/sweep/maintenance.rb:340) — 26 lines, transactional cleanup that requires `Distill::ReferenceMaterialDetector`
167
+
168
+ Plus the pre-existing `restore_multi_value_supersessions` (line 185, 57 lines).
169
+
170
+ These are all *one-shot historical cleanups* (per their docstrings). They don't belong in the regular sweep cycle — they're admin operations. Two options:
171
+
172
+ 1. **Extract to `Sweep::HistoricalCleanup`** — a separate module for one-shot data fixes
173
+ 2. **Keep in Maintenance but extract long methods** — e.g. `dedupe_open_conflicts` calls `pair_key`, but the inner per-group logic (lib/claude_memory/sweep/maintenance.rb:294-326) is 32 lines that could be `resolve_duplicate_group(keeper, duplicates)`
174
+
175
+ **File:** `lib/claude_memory/sweep/maintenance.rb`
176
+ **Effort:** 2 hours
177
+ **Priority:** 🟡 Medium
178
+ **Expert principle:** Sandi Metz SRP; Beck single level of abstraction
179
+
180
+ #### C. `digest_command.rb:128` calls private API via `.send`
181
+
182
+ ```ruby
183
+ # lib/claude_memory/commands/digest_command.rb:128
184
+ util = Dashboard::Trust.new(manager).send(:utilization)
185
+ ```
186
+
187
+ This is the only `.send` to a private method in `lib/`. Two paths forward:
188
+
189
+ ```ruby
190
+ # Option 1: Promote utilization to public on Trust (it already returns a documented Hash shape)
191
+ # lib/claude_memory/dashboard/trust.rb — remove `private` annotation above utilization
192
+
193
+ # Option 2: Extract Dashboard::Utilization as its own object
194
+ class Utilization
195
+ def initialize(manager) = @manager = manager
196
+ def report = { extracted:, used:, used_from_extracted:, ratio_pct:, window_days: }
197
+ end
198
+ ```
199
+
200
+ Option 2 is cleaner — Trust currently *also* exposes `utilization` indirectly through `snapshot`, so users have two paths to the same data. Extracting the calculator gives Digest, Trust, and any future caller one canonical interface.
201
+
202
+ **File:** `lib/claude_memory/commands/digest_command.rb:128`
203
+ **Effort:** 30 minutes
204
+ **Priority:** 🟡 Medium (works correctly, but tells future readers "private is negotiable")
205
+ **Expert principle:** Avdi Grimm tell-don't-ask; Sandi Metz dependency clarity
206
+
207
+ ### Low Issues
208
+
209
+ | # | Issue | File:Line | Effort |
210
+ |---|---|---|---|
211
+ | 8 | `upsert_content_item` 11 keyword params (carried) | `store/sqlite_store.rb:193` | 1 hour |
212
+ | 32 | `parse_timestamp` duplicated in `dashboard/api.rb:565` and `dashboard/conflicts.rb:278` | both | 15 min |
213
+ | 33 | `stores_for(scope)` / `facts_stores_for(scope)` near-identical pattern | `dashboard/conflicts.rb:160`, `dashboard/api.rb:589` | 30 min |
55
214
 
56
215
  ---
57
216
 
58
217
  ## 2. Jeremy Evans Perspective
59
218
 
60
219
  ### What's Been Fixed ✅
61
- - Batch queries in Recall pipeline (FactQueryBuilder)
62
- - Transaction wrapping in Resolver
63
- - ExportCommand N+1 eliminated
64
- - `discover_other_projects` now catches specific exception types (`Sequel::DatabaseError, Extralite::Error, IOError`)
65
- - **promote_fact transaction boundary fixed** — project data read before global transaction (already correct in code; verified and confirmed)
66
- - **Provenance nil content_item_id fixed** — removed mandatory `content_item_id` validation from `Domain::Provenance`, allowing nil for promoted facts
67
220
 
68
- ### Medium Issues 🟡
221
+ - Migrations v15, v16, v17 all wrap DDL in idempotent `create_table?` / `add_column` and provide `down` blocks (v14's down is intentionally a no-op with comment)
222
+ - `Trust#extracted_fact_pairs` (lib/claude_memory/dashboard/trust.rb:231) and `used_fact_pairs` (line 248) batch via `select(:id)` + iteration — no per-row queries
223
+ - `Conflicts#load_facts_for_rows` (lib/claude_memory/dashboard/conflicts.rb:235) batches with `where(id: ids).as_hash(:id)` — explicit N+1 prevention
69
224
 
70
- | # | Issue | File:Line | Effort |
71
- |---|-------|-----------|--------|
72
- | 8 | **upsert_content_item has 11 keyword parameters** | `store/sqlite_store.rb:158-184` | 1 hour |
225
+ ### Raw SQL Audit
226
+
227
+ No new raw SQL. The handful of `Sequel.lit` calls in `dashboard/api.rb` are all `DATE(...)` group-by helpers (lines 479, 487, 494) — required because Sequel doesn't have a portable `DATE(timestamp_string)` extractor for SQLite.
73
228
 
74
- Exceeds the 5-parameter guideline. Suggests the method is doing too much.
229
+ ### Transaction Safety
75
230
 
76
- **Fix:** Introduce a `ContentItemAttributes` value object.
231
+ New transactional methods all wrap correctly:
232
+ - `Sweep::Maintenance#dedupe_open_conflicts` — wraps in `@store.db.transaction` (line 289)
233
+ - `Sweep::Maintenance#reclassify_references` — wraps in `@store.db.transaction` (line 349)
234
+ - `SQLiteStore#upsert_moment_feedback` — wraps in `@db.transaction` (line 128)
235
+
236
+ ### N+1 Audit (new dashboard panels)
237
+
238
+ - `Moments#build_moment` (lib/claude_memory/dashboard/moments.rb:125) calls `resolve_content` and `extracted_facts` per row. **Potential N+1 if a feed page surfaces 50 ingest moments.** `extracted_facts` runs `store.db[:facts].join(:provenance).where(content_item_id:)` per moment.
239
+ - `Trust#count_open_conflicts` (lib/claude_memory/dashboard/trust.rb:145) → `Conflicts#distinct_open_counts` walks both stores. Acceptable (fixed cardinality of 2).
240
+ - `Trust#used_fact_pairs` (lib/claude_memory/dashboard/trust.rb:248) loads up to N=500 events without limit. Could grow unbounded. Recommend explicit `.limit(...)` for safety.
241
+
242
+ **Recommendation:**
243
+ - Batch `extracted_facts` in `Moments`: collect all `content_item_id`s up front, run one `where(content_item_id: ids)` join, group results in Ruby.
244
+ - Add explicit `.limit` to `used_fact_pairs` (10,000 is a safe ceiling for a 30-day window).
245
+
246
+ **File:** `lib/claude_memory/dashboard/moments.rb:125,231`
247
+ **Effort:** 45 minutes
248
+ **Priority:** 🟡 Medium (will only bite at scale; fix proactively)
249
+ **Expert principle:** Jeremy Evans dataset hygiene
77
250
 
78
251
  ---
79
252
 
80
253
  ## 3. Kent Beck Perspective
81
254
 
82
255
  ### What's Been Fixed ✅
83
- - New embedding subsystem has full test coverage (4 spec files, shared examples)
84
- - `generator_spec.rb` now tests `name`/`dimensions` contract
85
- - DimensionCheck tested for all 3 states (`:fresh`, `:match`, `:mismatch`)
86
- - ApiAdapter tested with HTTP mocks (no WebMock dependency)
87
- - **5 critical untested files now have specs** — `similarity.rb` (10 tests), `metadata_extractor.rb` (9 tests), `tool_extractor.rb` (7 tests), `recover_command.rb` (3 tests), `schema_validator.rb` (6 tests). Total: +36 specs, suite now at 1444.
256
+
257
+ - **Migration spec coverage hit gold standard.** Per-migration specs for v13/v14/v15/v16/v17 + cross-version round-trips from v12, v13, and v14 all forward to v17. That's the canonical "test the seam" pattern. The lessons from `feedback_round_trip_migration_specs.md` are now codified in green tests.
258
+ - New commands `digest_command.rb` and `census_command.rb` shipped with direct specs
259
+ - New dashboard modules all have direct specs (`moments_spec.rb`, `reuse_spec.rb`, `trust_spec.rb`, `knowledge_spec.rb`, `scoped_fact_resolver_spec.rb`)
88
260
 
89
261
  ### High Priority Issues
90
262
 
91
- None remaining. All high-priority items are resolved.
263
+ #### D. Two new commands shipped without specs
92
264
 
93
- ### Remaining Untested Files (lower priority)
265
+ | Command | LOC | Spec? |
266
+ |---|---|---|
267
+ | `commands/dedupe_conflicts_command.rb` | 55 | ❌ none |
268
+ | `commands/reclassify_references_command.rb` | 56 | ❌ none |
94
269
 
95
- Thin CLI wrappers that delegate to already-tested classes:
96
- - `commands/stats_command.rb`, `commands/export_command.rb`
97
- - `commands/changes_command.rb`, `commands/conflicts_command.rb`, `commands/explain_command.rb`
98
- - `commands/recall_command.rb`, `commands/search_command.rb`
99
- - `commands/sweep_command.rb`, `commands/publish_command.rb`, `commands/ingest_command.rb`
100
- - `commands/db_init_command.rb`
101
- - `commands/checks/` (6 files), `commands/initializers/` (5 files)
102
- - `mcp/tool_helpers.rb`, `embeddings/fastembed_adapter.rb`, `distill/distiller.rb`
270
+ Both are thin wrappers over `Sweep::Maintenance` (which *is* tested), but the CLI-layer concerns — option parsing, scope routing, output format, dry-run flag flow-through — are uncovered.
103
271
 
104
- ### Medium Issues 🟡
272
+ The output format in particular has logic worth pinning:
273
+ - `dedupe_conflicts_command.rb:38-52` decides `DRY RUN` vs `DEDUPE`, separator length, decisions header
274
+ - `reclassify_references_command.rb:38-53` truncates objects to 100 chars + ellipsis
105
275
 
106
- | # | Issue | File:Line | Effort |
107
- |---|-------|-----------|--------|
108
- | 10 | **Sleep-based tests add 4+ seconds** | `spec/ingest/ingester_spec.rb:43,65,81` | 1 hour |
276
+ **Proposed:** Mirror `digest_command_spec.rb` (or `census_command_spec.rb`) test option parsing, dry-run paths, and stdout shape via injected `StringIO`.
109
277
 
110
- Three `sleep 1.01` calls wait for filesystem mtime changes. `publish_spec.rb:189` has `sleep 1.1`.
278
+ **Effort:** 30 min each (60 min total)
279
+ **Priority:** High — these are admin commands that mutate data; CLI ergonomics belong under test
111
280
 
112
- **Fix:** Mock `File.mtime` or inject a time provider instead of real sleeps.
281
+ #### E. `dashboard/server.rb` still untested
113
282
 
114
- | # | Issue | File:Line | Effort |
115
- |---|-------|-----------|--------|
116
- | 11 | **No shared test factory** | `spec/spec_helper.rb` | 1 hour |
283
+ Carried over from 2026-04-22. The file has grown 189 → 211 LOC (+22) due to new endpoints (moments feedback POST/DELETE, conflict reject_similar). All branching is inside the request router (`handle_moments`, `handle_conflicts`).
284
+
285
+ WEBrick HTTP testing is awkward but not impossible `Rack::MockRequest` works against the API class directly. Alternatively, exercise the routing by injecting a stub WEBrick request object.
286
+
287
+ **Effort:** 1.5 hours
288
+ **Priority:** Medium-Low
289
+
290
+ ### Sleep-Based Test Latency Increased
291
+
292
+ Total sleep-based test cost in `bundle exec rspec`:
293
+
294
+ | Spec | sleep total | Notes |
295
+ |---|---|---|
296
+ | `spec/claude_memory/ingest/ingester_spec.rb` | 3.03s | mtime resolution, carried |
297
+ | `spec/claude_memory/publish_spec.rb` | 1.1s | carried |
298
+ | `spec/claude_memory/recall_spec.rb` | 0.01s | carried |
299
+ | `spec/claude_memory/dashboard/moments_spec.rb` | 2.2s | **NEW** ordering of activity events |
300
+ | `spec/claude_memory/dashboard/api_spec.rb` | 2.2s | **NEW** activity ordering tests |
301
+ | **Total** | **~8.5s** | up from ~4s last review |
302
+
303
+ The dashboard sleeps are because activity_events ordering depends on `occurred_at` ISO timestamps, and successive inserts in <1s produce the same timestamp. Two fixes:
304
+
305
+ ```ruby
306
+ # Option 1: Inject explicit timestamps (already supported via insert column)
307
+ store.activity_events.insert(occurred_at: Time.now.utc.iso8601, ...)
308
+ store.activity_events.insert(occurred_at: (Time.now + 1).utc.iso8601, ...)
309
+
310
+ # Option 2: Stub Time.now via Timecop or RSpec's allow(Time).to receive(:now)
311
+ ```
117
312
 
118
- `spec_helper.rb` is only 21 lines. ~20 test files independently define `create_fact` and `create_content_with_fact` helpers. The canonical pattern from `tools_spec.rb:275` should be extracted.
313
+ Option 1 requires no extra dep. Either eliminates 4.4s of wall time.
119
314
 
120
- **Fix:** Create `spec/support/database_factory.rb` with shared helpers, require from spec_helper.
315
+ **File:** `spec/claude_memory/dashboard/moments_spec.rb:130,132`, `api_spec.rb:332,359`
316
+ **Effort:** 30 minutes
317
+ **Priority:** 🟡 Medium (test speed degrades CI loop)
318
+ **Expert principle:** Kent Beck fast feedback
319
+
320
+ ### Carried-Forward Issues 🟡
321
+
322
+ | # | Issue | File:Line | Effort |
323
+ |---|---|---|---|
324
+ | 11 | No shared test factory | `spec/spec_helper.rb` | 1 hour |
121
325
 
122
326
  ---
123
327
 
124
328
  ## 4. Avdi Grimm Perspective
125
329
 
126
330
  ### What's Been Fixed ✅
127
- - DimensionCheck returns a Result value object — no exceptions, no side effects
128
- - `Embeddings.resolve` raises `ArgumentError` with clear message for unknown providers
129
- - ApiAdapter raises with descriptive messages for missing API keys
130
- - Duck typing for embedding providers (no base class)
131
- - **ApiAdapter now uses typed `ApiError < StandardError`** instead of bare `raise "message"`
132
- - **Resolver mutable state resolved** — verified that `project_path` and `scope` are already threaded as parameters through the entire method chain; no mutable instance state exists
133
331
 
134
- ### Carried Forward Issues 🟡
332
+ - New code uses scoped rescues (`rescue Sequel::DatabaseError, JSON::ParserError`) over bare rescues by default. Of 18 new rescue clauses in dashboard files, **13 are scoped to specific exception types**, 5 are bare and all return safe defaults
333
+ - `Result` pattern preserved in embeddings paths
334
+ - `Core::RelativeTime.format` used consistently across new dashboard modules
335
+
336
+ ### Bare Rescue Audit (full lib/, current count: 19 bare rescues)
337
+
338
+ The count grew from 5 → 19 because new dashboard code added 5 in `api.rb`. All are defensive (return safe shape):
339
+
340
+ | Location | Context | Returns | Verdict |
341
+ |---|---|---|---|
342
+ | `mcp/handlers/stats_handlers.rb:102` | `fts_legacy?` | `false` | Acceptable — boolean check |
343
+ | `mcp/instructions_builder.rb:147` | `vec_available?` | `false` | Acceptable |
344
+ | `sweep/maintenance.rb:140` | FTS prune | skips row | Acceptable |
345
+ | `commands/hook_command.rb:102` | forked handler | `nil` | Required |
346
+ | `commands/stats_command.rb:276` | `check_fts_format` | no-op | Acceptable |
347
+ | **`dashboard/api.rb:340` (new)** | recall live query | error hash | Acceptable — wide net for unfamiliar errors from Recall pipeline |
348
+ | **`dashboard/api.rb:672` (new)** | `db_stats` aggregation | `{exists:, error:}` | Acceptable |
349
+ | **`dashboard/api.rb:693` (new)** | `db_health` introspection | error hash | Acceptable |
350
+ | **`dashboard/api.rb:728` (new)** | `hooks_health` JSON read | error hash | Acceptable |
351
+ | **`dashboard/api.rb:797` (new)** | `vec_health` | error hash | Acceptable |
352
+
353
+ Verdict: per `Style/RescueStandardError` in Standard Ruby (rejected explicit-rescue change in last review), these are correct. **No action.**
354
+
355
+ ### Carried-Forward Issues 🟡
135
356
 
136
357
  | # | Issue | File:Line | Effort |
137
- |---|-------|-----------|--------|
138
- | 13 | **Inconsistent payload validation in hooks** | `hook/handler.rb:17-53` | 30 min |
358
+ |---|---|---|---|
359
+ | 13 | Inconsistent payload validation | `hook/handler.rb:53-82` | 30 min |
360
+
361
+ Verified still present.
362
+
363
+ ### New Concern
364
+
365
+ #### F. `digest_command.rb:128` reaches into `Trust`'s private API
139
366
 
140
- `ingest` uses `.fetch("field")` with fallback, `sweep` uses `.fetch("budget", default)`, `publish` uses `.fetch("mode", "shared")`. No consistent validation pattern.
367
+ Documented above (#C). Repeating here under the Avdi lens: the explicit `.send` is a public-API smell. Either the method shouldn't be private, or there should be a public wrapper. Choose.
141
368
 
142
369
  ---
143
370
 
144
371
  ## 5. Gary Bernhardt Perspective
145
372
 
146
373
  ### What's Been Fixed ✅
147
- - DimensionCheck is pure: takes store + provider, returns immutable Result. No hidden side effects.
148
- - `clear_stale_embeddings` was moved from hidden infrastructure setup to explicit command-level call.
149
- - VectorIndex#clear! encapsulates vec0 table knowledge (no raw SQL in command).
150
- - **Dead Configuration embedding accessors removed** — resolver and ApiAdapter read ENV directly, no unused indirection.
151
374
 
152
- ### Carried Forward Issues 🟡
375
+ - New dashboard modules continue to honor the imperative-shell / functional-core split:
376
+ - `Trust` does only reads + transformation (no writes)
377
+ - `Moments` does reads + transformation
378
+ - `Reuse` does reads + transformation
379
+ - `Efficacy::Reporter` is **pure** (no DB) — takes events, returns a hash — Bernhardt's dream
380
+ - `Knowledge#summary` returns shaped data; UI logic stays out of the model
381
+ - New value-object-y data: `KIND_TO_EVENT_TYPES`, `FEED_EVENT_TYPES` are frozen module constants
153
382
 
154
- | # | Issue | File:Line | Effort |
155
- |---|-------|-----------|--------|
156
- | 14 | **I/O mixed with logic in discover_other_projects** | `mcp/tools.rb:565-614` | 1 hour |
383
+ ### Boundaries
157
384
 
158
- SQL queries, filesystem checks, database connections in a loop, and error handling all mixed together.
385
+ ```
386
+ HTTP layer: Dashboard::Server (211 LOC, untested) ← imperative shell
387
+ JSON layer: Dashboard::API (807 LOC ⚠ growing) ← needs to shrink to routing
388
+ Subsystems: Conflicts, Moments, Trust, Knowledge, Reuse ← functional core (good)
389
+ Pure helpers: Efficacy::Reporter, ScopedFactResolver ← pure (excellent)
390
+ Query layer: Recall, store datasets ← impure but isolated
391
+ ```
159
392
 
160
- | # | Issue | File:Line | Effort |
161
- |---|-------|-----------|--------|
162
- | 15 | **Sweeper mutable state** | `sweep/sweeper.rb:16-17` | 20 min |
393
+ `API` is the wrong layer to be doing JSONL parsing (`extract_user_prompt`), time-window correlation (`find_recall_trigger`), or 3-source aggregation (`timeline`). Each of those wants to be its own pure object.
394
+
395
+ ### Test Speed Regression
396
+
397
+ Sleep-based tests are dollar-bills the suite is burning every CI run. Eliminating them is functional-core hygiene — the test should pin behavior, not wait for clock state.
398
+
399
+ ### Carried-Forward Issues 🟡
163
400
 
164
401
  | # | Issue | File:Line | Effort |
165
- |---|-------|-----------|--------|
166
- | 16 | **Dir.chdir in publish tests** | `spec/publish_spec.rb:14` | 15 min |
402
+ |---|---|---|---|
403
+ | 15 | Sweeper mutable state | `sweep/sweeper.rb:16-17` | 20 min |
404
+ | 16 | `Dir.chdir` in publish tests | `spec/publish_spec.rb:14` | 15 min |
167
405
 
168
406
  ---
169
407
 
170
408
  ## 6. General Ruby Idioms
171
409
 
172
- ### What's Been Fixed ✅
173
- - **Silent exception swallowing resolved** — 3 bare rescue blocks now log via `ClaudeMemory.logger.debug(...)`:
174
- - `mcp/instructions_builder.rb:29`
175
- - `hook/context_injector.rb:47`
176
- - `commands/checks/vec_check.rb:55`
410
+ ### New Items
177
411
 
178
- - **SnippetExtractor range calculation DRY** extracted `snippet_range` method to eliminate duplicated start/end index computation between `extract_with_lines` and `build_snippet`
179
-
180
- ### Carried Forward Issues
412
+ | # | Issue | File:Line | Severity | Effort |
413
+ |---|---|---|---|---|
414
+ | 31 | `Dashboard::API` 807 LOC, 11 methods >15 lines (regression of #28) | `dashboard/api.rb` | 🔴 High | 4–6 hours |
415
+ | 32 | `parse_timestamp(value)` duplicated verbatim in api.rb:565 and conflicts.rb:278 | both | 🟢 Low | 15 min |
416
+ | 33 | `stores_for` / `facts_stores_for` near-identical between Conflicts and API | `conflicts.rb:160`, `api.rb:589` | 🟢 Low | 30 min |
417
+ | 34 | `digest_command.rb:128` uses `.send(:utilization)` to call private | `digest_command.rb:128` | 🟡 Medium | 30 min |
418
+ | 35 | Sleep-based dashboard tests add 4.4s to suite | `dashboard/{moments,api}_spec.rb` | 🟡 Medium | 30 min |
419
+ | 36 | DedupeConflictsCommand and ReclassifyReferencesCommand untested | `commands/` | High | 60 min |
420
+ | 37 | `sweep/maintenance.rb` regrew to 456 LOC; 3 methods >50 lines | `sweep/maintenance.rb` | 🟡 Medium | 2 hours |
421
+ | 38 | `Moments#extracted_facts` per-moment join (potential N+1 at 50-row pages) | `moments.rb:231` | 🟡 Medium | 30 min |
422
+
423
+ ### Carried-Forward Items
181
424
 
182
425
  | # | Issue | File:Line | Severity | Effort |
183
- |---|-------|-----------|----------|--------|
184
- | 17 | **ResponseFormatter duplication** | `mcp/response_formatter.rb:27-280` | 🟡 Medium | 1 hour |
185
- | 18 | **Publish section generator repetition** | `publish.rb:100-154` | Low | 30 min |
426
+ |---|---|---|---|---|
427
+ | 17 | ResponseFormatter duplication | `mcp/response_formatter.rb` | 🟡 Medium | 1 hour |
428
+ | 28 | ~~Dashboard::API method extraction~~ **escalated to #31** | | | |
429
+ | 8 | `upsert_content_item` 11 keyword params | `store/sqlite_store.rb:193` | 🟢 Low | 1 hour |
430
+ | 10 | Sleep-based ingester tests | `spec/ingest/ingester_spec.rb` | 🟢 Low | 1 hour |
431
+ | 11 | No shared test factory | `spec/spec_helper.rb` | 🟢 Low | 1 hour |
186
432
 
187
433
  ---
188
434
 
189
435
  ## 7. Positive Observations
190
436
 
191
- - **Batch loading architecture**: `FactQueryBuilder` and `BatchLoader` eliminate N+1 patterns in all hot query paths
192
- - **Consistent dependency injection**: All commands accept `stdout`, `stderr`, `stdin` for testability
193
- - **Clean module boundaries**: Each module has clear responsibilities with minimal cross-coupling
194
- - **Proper Sequel usage**: Datasets used consistently, raw SQL avoided almost entirely
195
- - **Excellent domain modeling**: Fact, Entity, Provenance are immutable value objects with validation
196
- - **Good file organization**: ~1 class per file, consistent naming, clear module nesting
197
- - **Strong test culture**: 1.84:1 test-to-code ratio, behavior-focused tests
198
- - **Infrastructure abstractions**: `FileSystem`, `InMemoryFileSystem` enable fast tests
199
- - **Core::Result monad**: Consistent Success/Failure pattern throughout
200
- - **New embedding subsystem**: Clean duck typing with shared RSpec examples verifying provider contract. DimensionCheck is a textbook value object — pure function, immutable result, no side effects. The resolver uses simple case/when (no over-engineered factory/registry).
201
- - **VectorIndex#clear!**: Properly encapsulates destructive vec0 operation behind the abstraction boundary
437
+ - **Migration discipline** round-trip specs, per-migration specs, idempotent DDL. The "treat round-trip migration specs as a release blocker" lesson from `feedback_round_trip_migration_specs.md` got operationalized in 5 days
438
+ - **New commands ship with specs** DigestCommand and CensusCommand both got direct specs; the two that didn't (Dedupe + Reclassify) are 55-line wrappers over already-tested Maintenance methods, so the gap is small
439
+ - **Dashboard subsystem decomposition** when 5 new panels (Moments, Reuse, Trust, Knowledge, ScopedFactResolver) all land as their own classes with their own specs, the module-extraction muscle is strong
440
+ - **`Efficacy::Reporter` purity** 128 LOC, zero I/O, takes events and returns shape. Spec is fast and readable. This is the model the rest of dashboard/ should converge on
441
+ - **No raw SQL added; no N+1 in hot paths; transaction safety maintained** — across +2,011 LOC in 6 days
202
442
 
203
443
  ---
204
444
 
205
445
  ## 8. Priority Refactoring Recommendations
206
446
 
207
- ### High Priority (Next Week)
447
+ ### High Priority (This Week — pre-0.10.0 release)
208
448
 
209
- None remaining. All high-priority items are resolved.
449
+ | # | Item | File:Line | Effort | Impact |
450
+ |---|---|---|---|---|
451
+ | 31 | Extract `Dashboard::Timeline` / `Health` / `RecallQuery` / `RecallTriggerFinder` / `UserPromptExtractor` / `FactsQuery` from API | `dashboard/api.rb` | 4–6 hours | API drops 807→~250 LOC; reverses regression |
452
+ | 36 | Add `dedupe_conflicts_command_spec.rb` + `reclassify_references_command_spec.rb` | `spec/claude_memory/commands/` | 1 hour | CLI surface tested |
210
453
 
211
454
  ### Medium Priority (Next Sprint)
212
- | # | Item | Effort | Impact |
213
- |---|------|--------|--------|
214
- | 8 | ContentItemAttributes value object | 1 hour | Readability |
215
- | 10 | Replace sleep-based tests with mocks | 1 hour | Test speed |
216
- | 11 | Shared test factory | 1 hour | DRY |
217
- | 17 | ResponseFormatter base method | 1 hour | DRY |
218
- | 14 | Separate I/O in discover_other_projects | 1 hour | Boundaries |
455
+
456
+ | # | Item | File:Line | Effort |
457
+ |---|---|---|---|
458
+ | 34 | Promote `Trust#utilization` to public OR extract `Dashboard::Utilization` | `dashboard/trust.rb`, `digest_command.rb:128` | 30 min |
459
+ | 35 | Replace sleep-based dashboard tests with explicit timestamps | `dashboard/{moments,api}_spec.rb` | 30 min |
460
+ | 37 | Extract long methods from `sweep/maintenance.rb` (`dedupe_open_conflicts`, `restore_multi_value_supersessions`) OR move one-shot cleanups to `Sweep::HistoricalCleanup` | `sweep/maintenance.rb` | 2 hours |
461
+ | 38 | Batch `Moments#extracted_facts` to avoid 50-row N+1 | `moments.rb:231` | 30 min |
462
+ | 17 | ResponseFormatter consolidation (carried) | `mcp/response_formatter.rb` | 1 hour |
463
+ | 13 | Payload validator for hook events (carried) | `hook/handler.rb` | 30 min |
464
+ | E | `dashboard/server_spec.rb` (carried) | `spec/claude_memory/dashboard/` | 1.5 hours |
219
465
 
220
466
  ### Low Priority (Later)
221
- | # | Item | Effort | Impact |
222
- |---|------|--------|--------|
223
- | 13 | Payload validator for hooks | 30 min | Consistency |
224
- | 15 | Sweeper mutable state | 20 min | Immutability |
225
- | 16 | Dir.chdir in tests | 15 min | Test isolation |
226
- | 18 | Publish section builder | 30 min | DRY |
227
-
228
- ### Carried Forward (Low Priority from Earlier Reviews)
229
- | # | Item | Original # |
230
- |---|------|-----------|
231
- | 20 | DateTime migration (string timestamps) | Feb 4 #17 |
232
- | 21 | Command manager helper (`with_manager`) | Feb 4 #19 |
233
- | 22 | release_connections polymorphism | Feb 4 #20 |
234
- | 23 | Provenance batch insert (`multi_insert`) | Feb 4 #22 |
235
- | 25 | Result objects for all queries | Feb 4 #24 |
467
+
468
+ | # | Item | Effort |
469
+ |---|---|---|
470
+ | 32 | DRY `parse_timestamp` (`api.rb:565` ↔ `conflicts.rb:278`) | 15 min |
471
+ | 33 | DRY `stores_for` / `facts_stores_for` | 30 min |
472
+ | 8 | `ContentItemAttributes` value object | 1 hour |
473
+ | 10 | Replace sleep-based ingester tests | 1 hour |
474
+ | 11 | Shared test factory | 1 hour |
475
+ | 15 | Sweeper mutable state | 20 min |
476
+ | 16 | `Dir.chdir` in publish tests | 15 min |
477
+
478
+ ### Quick Wins (Today)
479
+
480
+ | # | Item | Effort |
481
+ |---|---|---|
482
+ | 32 | Extract `parse_timestamp` to `Core::RelativeTime` (it already lives there as a value module) | 15 min |
483
+ | 34 | Promote `Trust#utilization` to public | 5 min |
484
+ | 35 | Inject timestamps into dashboard spec inserts | 30 min |
236
485
 
237
486
  ---
238
487
 
239
488
  ## 9. Conclusion
240
489
 
241
- The codebase maintains its strong architectural foundation. Nine quality items were resolved this session across two passes: the Shortcuts scope correctness bug, ApiAdapter exception typing, silent exception logging, dead code removal, method decomposition, promote_fact transaction verification, provenance nil validation fix, Resolver state verification, and SnippetExtractor DRY extraction.
490
+ In 6 days the codebase grew 12% (+2,011 LOC). Most of that growth was healthy five new dashboard subsystems with specs, three migrations with both per-version and round-trip specs, two new admin commands wrapping already-tested maintenance methods. Migration discipline in particular leveled up: the lesson from `feedback_round_trip_migration_specs.md` shipped as actual release-blocking spec coverage.
491
+
492
+ **The headline regression is `Dashboard::API`.** Last review marked it medium-priority for per-endpoint extraction. Six days later it's gained 180 LOC, four new methods over 15 lines, and one method (`timeline`) that's now 52 lines. This is the file that most rewards extraction — it's already surrounded by collaborators (`Conflicts`, `Moments`, `Trust`, `Knowledge`, `Reuse`) that prove the per-endpoint pattern works. Doing the extraction now reverses the trend; deferring lets it accumulate another 200 LOC by next review.
493
+
494
+ **Recommended next-action set, in order:**
242
495
 
243
- No known correctness bugs remain. All three >500-line god objects have been eliminated: `Tools` (745→104), `Recall` (727→94), `SQLiteStore` (547→386). Zero critical or high-priority issues remain. All critical untested files now have specs (+36 tests). The remaining 12 items are medium/low priority (thin CLI wrappers, DRY improvements, carried-forward items).
496
+ 1. **`/quality-update`** to apply #31 (Dashboard::API extraction) and #36 (missing command specs). Target: api.rb 300 LOC, all commands tested.
497
+ 2. Quick wins #32 + #34 + #35 in the same session (~75 min total).
498
+ 3. Schedule #37 and #38 for the next sprint — neither is urgent but both compound if left alone.
499
+ 4. After #31 lands, `/review-for-quality` again pre-0.10.0 release to confirm the regression closed.
500
+
501
+ The 0.10.0 release should not ship with `dashboard/api.rb` at 807 LOC — the per-endpoint extraction is well-defined, well-precedented, and small-batch (5 extractions × ~1hr each). Doing it before tag is the difference between landing 0.10.0 with a healthy dashboard subsystem vs. burying tech debt in the headline feature of the release.
244
502
 
245
503
  ---
246
504
 
247
505
  ## Appendix A: Metrics Comparison
248
506
 
249
- | Metric | Jan 29 | Feb 4 | Mar 9 | Mar 19 |
250
- |--------|--------|-------|-------|--------|
251
- | Ruby files (lib) | ~85 | 104 | 112 | **117** |
252
- | LOC (lib) | ~8,000 | 9,982 | 11,392 | **12,239** |
253
- | LOC (spec) | | 17,693 | 21,632 | **22,563** |
254
- | Pure logic classes | 17+ | 20+ | 20+ | **22+** |
255
- | Test files | 74+ | 98 | 128 | **122** |
256
- | Test-to-code ratio | ~1.5:1 | 1.77:1 | 1.90:1 | **1.84:1** |
257
- | Files >500 lines | 0 | 2 | 3 | **0** |
258
- | Bare rescues (silent) | 0 | 0 | 1 | **0** |
259
- | N+1 patterns (hot paths) | 0 | 0 | 0 | **0** ✅ |
260
- | N+1 patterns (cold paths) | | | 1 | **0** ✅ |
261
- | Untested lib files | | | 16 | **~7 critical** |
262
- | Known correctness bugs | | | | **0** ✅ |
507
+ | Metric | Mar 9 | Mar 19 | Apr 22 (review) | Apr 22 (after update) | **Apr 28 (this review)** |
508
+ |---|---|---|---|---|---|
509
+ | Ruby files (lib) | 112 | 117 | 148 | 150 | **161** (+11 new modules) |
510
+ | LOC (lib) | 11,392 | 12,239 | 17,014 | 17,031 | **19,025** (+2,011) |
511
+ | LOC (spec) | 21,632 | 22,563 | 28,074 | 28,490 | **31,079** (+2,605) |
512
+ | Spec files | 128 | 122 | 154 | 156 | **188** (+32) |
513
+ | Test-to-code ratio | 1.90:1 | 1.84:1 | 1.65:1 | 1.67:1 | **1.63:1** ⬇️ |
514
+ | Files >500 lines | 3 | 0 | 2 | 1 | **2** ⬆️ (api.rb 807, sqlite_store.rb 584) |
515
+ | Files >300 lines | 9 | 9 | 10 | 8 | **8** (same count, different mix) |
516
+ | Bare rescues (justified) | 1 | 0 | 5 | 5 | **19** (14 new, all defensive) |
517
+ | Bare rescues (unsafe) | 0 | 0 | 0 | 0 | **0** ✅ |
518
+ | N+1 patterns (hot paths) | 0 | 0 | 0 | 0 | **0** ✅ |
519
+ | Pure logic classes | 20+ | 22+ | 25+ | 27+ | **32+** (+5 new dashboard modules) |
520
+ | Migration round-trip specs | 0 | 0 | 0 | 0 | **3** (v12→v17, v13→v17, v14→v17) ✅ |
521
+ | Per-migration specs | 0 | 0 | 0 | 0 | **13** (001–017 minus a few) ✅ |
522
+ | Sleep-based test cost | — | — | ~4s | ~4s | **~8.5s** ⬆️ |
523
+ | Untested new commands | — | — | 0 | 0 | **2** (dedupe-conflicts, reclassify-references) |
524
+ | Known correctness bugs | — | 0 | 0 | 0 | **0** ✅ |
263
525
 
264
526
  ## Appendix B: File Size Report
265
527
 
266
- | File | Mar 9 | Mar 19 | Trend |
267
- |------|-------|--------|-------|
268
- | `mcp/tools.rb` | 728 | **104** | ⬇️ -624 (extracted to 6 handler modules) |
269
- | `recall.rb` | 681 | **94** | ⬇️ -587 (extracted to engine + query_core) |
270
- | `store/sqlite_store.rb` | 547 | **386** | ⬇️ -161 (extracted retry_handler + schema_manager) |
271
- | `mcp/response_formatter.rb` | 394 | 396 | ⬆️ +2 |
272
- | `mcp/tool_definitions.rb` | 303 | 334 | ⬆️ +31 |
273
- | `commands/index_command.rb` | 224 | 272 | ⬆️ +48 |
274
- | `mcp/text_summary.rb` | 257 | 258 | ⬆️ +1 |
275
- | `commands/stats_command.rb` | 239 | 250 | ⬆️ +11 |
276
- | `commands/uninstall_command.rb` | 226 | 226 | |
277
- | `publish.rb` | 221 | 221 | — |
278
- | `infrastructure/schema_validator.rb` | 215 | 215 | |
279
- | `commands/hook_command.rb` | 214 | 214 | |
280
- | `resolve/resolver.rb` | | 195 | new to watch |
281
- | `index/vector_index.rb` | | 184 | new to watch |
528
+ | File | Mar 19 | Apr 22 (review) | Apr 22 (after update) | **Apr 28 (this review)** | Trend |
529
+ |---|---|---|---|---|---|
530
+ | `dashboard/api.rb` | | 627 🆕 | 627 | **807** | ⬆️ +180 (+29%) **regression** |
531
+ | `store/sqlite_store.rb` | 386 | 683 | 544 | **584** | ⬆️ +40 (new tables) |
532
+ | `mcp/tool_definitions.rb` | 334 | 459 | 459 | **459** | |
533
+ | `sweep/maintenance.rb` | | 334 | 334 | **456** | ⬆️ +122 — new |
534
+ | `mcp/response_formatter.rb` | 396 | 397 | 397 | **397** | — |
535
+ | `commands/stats_command.rb` | 250 | 346 | 346 | **383** | ⬆️ +37 |
536
+ | `recall/query_core.rb` | 357 | 371 | 371 | **371** | — |
537
+ | `mcp/text_summary.rb` | 258 | 313 | 313 | **313** | — |
538
+ | `dashboard/conflicts.rb` | | 195 | 195 | **285** | ⬆️ +90 (dedup grouping logic) |
539
+ | `dashboard/trust.rb` | | | — | **284** | 🆕 new feed-first sidebar |
540
+ | `resolve/resolver.rb` | 195 | 254 | 254 | **268** | ⬆️ +14 (dedupe + scope_hint fix) |
541
+ | `mcp/tools.rb` | 104 | 249 | 249 | **264** | ⬆️ +15 |
542
+ | `commands/index_command.rb` | 272 | 259 | 259 | **259** | — |
543
+ | `commands/hook_command.rb` | 214 | 215 | 215 | **249** | ⬆️ +34 |
544
+ | `publish.rb` | 221 | 256 | 248 | **248** | — |
545
+ | `dashboard/moments.rb` | — | — | — | **244** | 🆕 feed primitive |
546
+ | `commands/uninstall_command.rb` | 226 | 226 | 226 | **226** | — |
547
+ | `hook/context_injector.rb` | — | 214 | 214 | **225** | ⬆️ +11 |
548
+ | `store/store_manager.rb` | — | 215 | 215 | **215** | — |
549
+ | `infrastructure/schema_validator.rb` | 215 | 215 | 215 | **215** | — |
550
+ | `commands/census_command.rb` | — | — | — | **210** | 🆕 predicate census |
551
+ | `mcp/handlers/setup_handlers.rb` | 211 | 211 | 211 | **211** | — |
552
+ | `dashboard/server.rb` | — | 189 | 189 | **211** | ⬆️ +22 (new endpoints) |
553
+ | `embeddings/model_registry.rb` | — | — | — | **210** | 🆕 |
554
+ | `mcp/server.rb` | — | 206 | 206 | **206** | — |
555
+ | `mcp/handlers/stats_handlers.rb` | — | 205 | 205 | **205** | — |
556
+ | `commands/initializers/hooks_configurator.rb` | — | — | — | **200** | — |
557
+ | `commands/embeddings_command.rb` | — | — | — | **198** | — |
558
+ | `ingest/ingester.rb` | — | — | — | **190** | — |
559
+ | `index/vector_index.rb` | 184 | 184 | 184 | **184** | — |
560
+ | `commands/digest_command.rb` | — | — | — | **181** | 🆕 weekly digest |
561
+ | `mcp/handlers/management_handlers.rb` | — | — | — | **177** | — |
562
+ | `ingest/observation_compressor.rb` | — | — | — | **177** | 🆕 tool-specific compression |
563
+ | `recall.rb` | 94 | 175 | 175 | **175** | — |
564
+ | `core/fact_query_builder.rb` | — | — | — | **174** | — |
565
+ | `mcp/error_classifier.rb` | — | — | — | **171** | — |
566
+ | `embeddings/generator.rb` | — | — | — | **165** | — |
567
+ | `index/lexical_fts.rb` | — | — | — | **153** | — |
568
+ | `dashboard/knowledge.rb` | — | — | — | **136** | 🆕 |
569
+ | `dashboard/efficacy.rb` | — | 127 | 127 | **127** | — |
570
+ | `dashboard/fact_presenter.rb` | — | 109 | 109 | **109** | — |
571
+ | `dashboard/reuse.rb` | — | — | — | **97** | 🆕 |
572
+ | `dashboard/scoped_fact_resolver.rb` | — | — | — | **95** | 🆕 |
573
+ | `commands/reclassify_references_command.rb` | — | — | — | **56** | 🆕 (untested) |
574
+ | `commands/dedupe_conflicts_command.rb` | — | — | — | **55** | 🆕 (untested) |
575
+
576
+ ## Appendix C: Methods >15 Lines in Watch-List Files
577
+
578
+ ### `dashboard/api.rb` (807 LOC, **42 methods**)
579
+
580
+ | Method | Line | Size | Action |
581
+ |---|---|---|---|
582
+ | `timeline` | 471 | 52 | Extract `Dashboard::Timeline` |
583
+ | `vec_health` | 759 | 46 | Extract into `Dashboard::Health` |
584
+ | `recall` | 315 | 41 | Extract `Dashboard::RecallQuery` |
585
+ | `facts` | 373 | 39 | Extract `Dashboard::FactsQuery` |
586
+ | `activity_detail` | 149 | 37 | Extract event-detail builder |
587
+ | `hooks_health` | 704 | 32 | Extract into `Dashboard::Health` |
588
+ | `find_recall_trigger` | 193 | 32 | Extract `Dashboard::RecallTriggerFinder` |
589
+ | `efficacy` | 439 | 31 | Move session-window logic into `Efficacy::Loader` |
590
+ | `extract_user_prompt` | 237 | 29 | Extract `Dashboard::UserPromptExtractor` |
591
+ | `session_summary` | 119 | 29 | Extract aggregator |
592
+ | `db_stats` | 647 | 28 | Extract into `Dashboard::Health` |
593
+ | `db_health` | 676 | 25 | Extract into `Dashboard::Health` |
594
+ | `load_content_item` | 603 | 21 | Could move into `FactPresenter` or its own loader |
595
+ | `activity` | 48 | 20 | Acceptable — thin wrapper |
596
+ | `facts_seen_in_recent_recalls` | 418 | 20 | Move into `Dashboard::FactsQuery` |
597
+ | `collect_configured_hook_types` | 739 | 19 | Move into `Dashboard::Health` |
598
+ | `serialize_recall_fact` | 545 | 19 | Move into `Dashboard::RecallQuery` |
599
+ | `health` | 14 | 18 | Becomes 3-liner after `Dashboard::Health` extraction |
600
+ | `reject_fact` | 294 | 16 | Acceptable — public surface |
601
+
602
+ ### `sweep/maintenance.rb` (456 LOC)
603
+
604
+ | Method | Line | Size | Action |
605
+ |---|---|---|---|
606
+ | `dedupe_open_conflicts` | 273 | 58 | Extract per-group `resolve_duplicate_group` helper |
607
+ | `restore_multi_value_supersessions` | 185 | 57 | Already documented; could extract `compute_restore_decisions` |
608
+ | `dedupe_multi_value_facts` | 58 | 34 | Acceptable — well-bounded transactional op |
609
+ | `reclassify_references` | 340 | 26 | Acceptable |
610
+ | `prune_old_content` | 130 | 16 | Acceptable |
611
+
612
+ ### `store/sqlite_store.rb` (584 LOC)
613
+
614
+ | Method | Line | Size | Notes |
615
+ |---|---|---|---|
616
+ | `upsert_content_item` | 193 | 27 | 11 kwargs (carried #8) |
617
+ | `reject_fact` | 410 | 25 | Conflict resolution in transaction |
618
+ | `insert_fact` | 332 | 22 | Many optional fields |
619
+ | `upsert_moment_feedback` | 123 | 21 | New — transaction with retry |
620
+ | `update_fact` | 373 | 19 | Generic update via allowed-keys |
621
+
622
+ ---
623
+
624
+ ## Historical Reviews
625
+
626
+ Earlier reviews (Jan 29, Feb 4, Mar 9, Mar 19) tracked the codebase from ~8,000 → 12,239 LOC. Their highlights, preserved here:
627
+
628
+ - **Jan 29 (initial)** — Identified Tools and Recall god-object risks; introduced first metrics baseline.
629
+ - **Feb 4** — Carried-forward items #17–#25 (DateTime migration, command manager helper, release_connections polymorphism, provenance batch insert, result objects). All still low-priority and open.
630
+ - **Mar 9** — Three files >500 LOC; bare rescue counted; vector index work landed.
631
+ - **Mar 19** — Successful refactor wave: `RetryHandler` + `SchemaManager` extracted from `SQLiteStore` (547 → 386); `Tools` reduced to 104-line dispatcher with 6 handler modules; `Recall` to 94-line facade. **Established the module-inclusion pattern** that has been reused successfully for LLMCache, MetricsAggregator, and the dashboard subsystems.
632
+
633
+ The 2026-04-22 review absorbed the 39% codebase growth (+4,775 LOC) without correctness regressions and resolved its top two watch-items (`SQLiteStore` regrowth, dashboard test coverage). It left `Dashboard::API` extraction as a medium-priority watch item — which the present review (2026-04-28) escalates to high-priority based on the 180-LOC regression in 6 days.
282
634
 
283
635
  ---
284
636
 
285
- **Next review:** After Recall strategy pattern refactoring or SQLiteStore extraction
637
+ **Next review:** After #31 (Dashboard::API extraction) lands, or pre-0.10.0 release tag.