claude_memory 0.9.0 → 0.10.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 (76) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/memory.sqlite3 +0 -0
  3. data/.claude/rules/claude_memory.generated.md +63 -1
  4. data/.claude/skills/dashboard/SKILL.md +42 -0
  5. data/.claude/skills/release/SKILL.md +168 -0
  6. data/.claude-plugin/marketplace.json +1 -1
  7. data/.claude-plugin/plugin.json +1 -1
  8. data/CHANGELOG.md +92 -0
  9. data/CLAUDE.md +21 -5
  10. data/README.md +32 -2
  11. data/db/migrations/015_add_activity_events.rb +26 -0
  12. data/db/migrations/016_add_moment_feedback.rb +22 -0
  13. data/db/migrations/017_add_last_recalled_at.rb +15 -0
  14. data/docs/1_0_punchlist.md +190 -0
  15. data/docs/EXAMPLES.md +41 -2
  16. data/docs/GETTING_STARTED.md +31 -4
  17. data/docs/architecture.md +22 -7
  18. data/docs/audit-queries.md +131 -0
  19. data/docs/dashboard.md +172 -0
  20. data/docs/improvements.md +465 -9
  21. data/docs/influence/cq.md +187 -0
  22. data/docs/plugin.md +13 -6
  23. data/docs/quality_review.md +489 -172
  24. data/docs/reflection_memory_as_accumulating_judgment.md +67 -0
  25. data/lib/claude_memory/activity_log.rb +86 -0
  26. data/lib/claude_memory/commands/census_command.rb +210 -0
  27. data/lib/claude_memory/commands/completion_command.rb +3 -0
  28. data/lib/claude_memory/commands/dashboard_command.rb +54 -0
  29. data/lib/claude_memory/commands/dedupe_conflicts_command.rb +55 -0
  30. data/lib/claude_memory/commands/digest_command.rb +181 -0
  31. data/lib/claude_memory/commands/hook_command.rb +34 -0
  32. data/lib/claude_memory/commands/reclassify_references_command.rb +56 -0
  33. data/lib/claude_memory/commands/registry.rb +6 -1
  34. data/lib/claude_memory/commands/skills/distill-transcripts.md +13 -1
  35. data/lib/claude_memory/commands/stats_command.rb +38 -1
  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 +285 -0
  52. data/lib/claude_memory/distill/reference_material_detector.rb +78 -0
  53. data/lib/claude_memory/hook/auto_memory_mirror.rb +112 -0
  54. data/lib/claude_memory/hook/context_injector.rb +97 -3
  55. data/lib/claude_memory/hook/handler.rb +50 -3
  56. data/lib/claude_memory/mcp/handlers/management_handlers.rb +8 -0
  57. data/lib/claude_memory/mcp/query_guide.rb +11 -0
  58. data/lib/claude_memory/mcp/server.rb +8 -2
  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/version.rb +1 -1
  75. data/lib/claude_memory.rb +22 -0
  76. metadata +50 -1
@@ -1,285 +1,602 @@
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.
6
9
 
7
10
  ---
8
11
 
9
12
  ## Executive Summary
10
13
 
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.
14
+ 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.
15
+
16
+ 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`.
17
+
18
+ **What regressed:**
19
+ - `dashboard/api.rb` 627 → 807 LOC (+180). Watch-list item not addressed.
20
+ - `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).
21
+ - 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.
22
+ - 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
23
 
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.
24
+ **What was resolved or improved since 2026-04-22:**
25
+ - Round-trip migration specs from v12/v13/v14 → v17 added (release-blocker per `feedback_round_trip_migration_specs.md`).
26
+ - Per-migration specs for v13–v17 added under `spec/claude_memory/store/migrations/`.
27
+ - New dashboard subsystems shipped *with* specs (good pattern — Reuse, Moments, Trust, Knowledge, ScopedFactResolver all have direct specs).
28
+ - `lib/claude_memory/store/sqlite_store.rb` only grew 40 LOC (544 → 584); regrowth controlled.
14
29
 
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)
30
+ **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
31
 
19
32
  ### Current Strengths
20
33
 
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
34
+ - Migrations now ship with per-migration specs **and** cross-version round-trip specs — a deliberate release-readiness improvement that landed during this window
35
+ - New dashboard subsystems all have direct specs; spec count grew 156 → 188 files (+32)
36
+ - Domain objects, frozen string literals, transaction wrapping, no raw SQL, no N+1 in hot paths — all preserved
37
+ - 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
38
 
34
39
  ---
35
40
 
36
41
  ## 1. Sandi Metz Perspective
37
42
 
38
43
  ### 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.
44
+
45
+ - `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
46
+ - New dashboard subsystems each landed under 300 LOC with focused responsibilities
47
+ - `Moments` (244 LOC) feed-shape construction, no DB writes
48
+ - `Trust` (284 LOC)sidebar aggregations, all reads
49
+ - `Reuse` (97 LOC) top-N "most-used" panel
50
+ - `Knowledge` (136 LOC) fact summary panel
51
+ - `ScopedFactResolver` (95 LOC) pure helper
52
+ - Round-trip migration specs (`round_trip_v12_to_v17_spec.rb` etc.) — Sandi-style "test the contract, not the implementation"
47
53
 
48
54
  ### Critical Issues 🔴
49
55
 
50
- None remaining.
56
+ #### A. `Dashboard::API` regressed: 627 → 807 LOC (+29%) — **carried-forward item became urgent**
57
+
58
+ `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:
59
+
60
+ - `find_recall_trigger` (lib/claude_memory/dashboard/api.rb:193) — 32 lines, 5 SQL constructions, calls 3 helpers, JSON-parses event details
61
+ - `extract_user_prompt` (lib/claude_memory/dashboard/api.rb:237) — 29 lines, JSONL parsing, content type narrowing, plumbing-noise filtering
62
+ - `facts` (lib/claude_memory/dashboard/api.rb:373) — 39 lines (was 26), now also handles `stale_only` filtering with cross-store exclusion
63
+ - `facts_seen_in_recent_recalls` (lib/claude_memory/dashboard/api.rb:418) — 20 lines, scoped-pair aggregation
64
+ - `efficacy` (lib/claude_memory/dashboard/api.rb:439) — 31 lines (was 23), now branches on session_id with time-window correlation
65
+ - New micro-endpoints: `moments`, `trust`, `knowledge`, `reuse`, `moment_feedback`, `clear_moment_feedback`, `fact_detail`, `promote_fact`, `reject_fact`
66
+
67
+ 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.
68
+
69
+ **Method size table (current state):**
70
+
71
+ | Method | Line | Size | Concern |
72
+ |---|---|---|---|
73
+ | `timeline` | 471 | 52 | 3 separate Sequel aggregations + Ruby-side merge — should be `Dashboard::Timeline` |
74
+ | `vec_health` | 759 | 46 | Branchy status derivation over coverage stats |
75
+ | `recall` | 315 | 41 | Result flattening + bare rescue + actionable-hint branching |
76
+ | `facts` | 373 | 39 | Pagination + filter + cross-store stale exclusion |
77
+ | `activity_detail` | 149 | 37 | Joined fetch + linked facts + recall-trigger correlation |
78
+ | `hooks_health` | 704 | 32 | Multi-state status with fix messages |
79
+ | `find_recall_trigger` | 193 | 32 | Time-window query with session_id fallback |
80
+ | `efficacy` | 439 | 31 | Session-scope vs window-scope branching |
81
+ | `extract_user_prompt` | 237 | 29 | JSONL reverse-walk + plumbing filter |
82
+ | `session_summary` | 119 | 29 | Multi-event-type aggregation |
83
+ | `db_stats` | 647 | 28 | Predicate counts + entity counts + size stats |
84
+
85
+ **Proposed extractions** (each candidate is testable in isolation):
86
+
87
+ ```ruby
88
+ # lib/claude_memory/dashboard/timeline.rb — pure aggregation
89
+ class Timeline
90
+ def initialize(manager) = @manager = manager
91
+ def days = { days: build_days }
92
+ private
93
+ def build_days
94
+ return [] unless store
95
+ fact_rows, content_rows, event_rows = load_aggregations
96
+ merge_into_days(fact_rows, content_rows, event_rows)
97
+ end
98
+ end
99
+
100
+ # lib/claude_memory/dashboard/health.rb — already 4 health checks (db, hooks, vec, vectors)
101
+ class Health
102
+ def report = { status: overall(checks), checks: checks, version: VERSION }
103
+ private
104
+ def checks = [db_health("global"), db_health("project"), hooks_health, vec_health]
105
+ end
106
+
107
+ # lib/claude_memory/dashboard/recall_query.rb — wraps live recall + actionable error mapping
108
+ class RecallQuery
109
+ def call(params) = format_response(run(params))
110
+ end
111
+
112
+ # lib/claude_memory/dashboard/recall_trigger_finder.rb — pure time-window correlation
113
+ # lib/claude_memory/dashboard/user_prompt_extractor.rb — pure JSONL parsing
114
+ # lib/claude_memory/dashboard/facts_query.rb — pagination + stale exclusion
115
+ ```
116
+
117
+ 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`.
118
+
119
+ **File:** `lib/claude_memory/dashboard/api.rb`
120
+ **Effort:** 4–6 hours (5 extractions, each with a focused spec)
121
+ **Priority:** 🔴 — was medium last review, escalates to high because the trend line points at 1,000+ LOC by next sprint if uncorrected
122
+ **Expert principle:** Sandi Metz SRP; Bernhardt boundaries; Beck simple design
51
123
 
52
- ### High Priority Issues
124
+ ### Medium Issues 🟡
125
+
126
+ #### B. `sweep/maintenance.rb` grew 334 → 456 LOC (+122, +37%)
127
+
128
+ 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:
129
+
130
+ - `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)
131
+ - `reclassify_references` (lib/claude_memory/sweep/maintenance.rb:340) — 26 lines, transactional cleanup that requires `Distill::ReferenceMaterialDetector`
132
+
133
+ Plus the pre-existing `restore_multi_value_supersessions` (line 185, 57 lines).
134
+
135
+ 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:
136
+
137
+ 1. **Extract to `Sweep::HistoricalCleanup`** — a separate module for one-shot data fixes
138
+ 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)`
139
+
140
+ **File:** `lib/claude_memory/sweep/maintenance.rb`
141
+ **Effort:** 2 hours
142
+ **Priority:** 🟡 Medium
143
+ **Expert principle:** Sandi Metz SRP; Beck single level of abstraction
144
+
145
+ #### C. `digest_command.rb:128` calls private API via `.send`
146
+
147
+ ```ruby
148
+ # lib/claude_memory/commands/digest_command.rb:128
149
+ util = Dashboard::Trust.new(manager).send(:utilization)
150
+ ```
151
+
152
+ This is the only `.send` to a private method in `lib/`. Two paths forward:
153
+
154
+ ```ruby
155
+ # Option 1: Promote utilization to public on Trust (it already returns a documented Hash shape)
156
+ # lib/claude_memory/dashboard/trust.rb — remove `private` annotation above utilization
157
+
158
+ # Option 2: Extract Dashboard::Utilization as its own object
159
+ class Utilization
160
+ def initialize(manager) = @manager = manager
161
+ def report = { extracted:, used:, used_from_extracted:, ratio_pct:, window_days: }
162
+ end
163
+ ```
164
+
165
+ 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.
53
166
 
54
- None remaining. All >500-line files have been decomposed.
167
+ **File:** `lib/claude_memory/commands/digest_command.rb:128`
168
+ **Effort:** 30 minutes
169
+ **Priority:** 🟡 Medium (works correctly, but tells future readers "private is negotiable")
170
+ **Expert principle:** Avdi Grimm tell-don't-ask; Sandi Metz dependency clarity
171
+
172
+ ### Low Issues
173
+
174
+ | # | Issue | File:Line | Effort |
175
+ |---|---|---|---|
176
+ | 8 | `upsert_content_item` 11 keyword params (carried) | `store/sqlite_store.rb:193` | 1 hour |
177
+ | 32 | `parse_timestamp` duplicated in `dashboard/api.rb:565` and `dashboard/conflicts.rb:278` | both | 15 min |
178
+ | 33 | `stores_for(scope)` / `facts_stores_for(scope)` near-identical pattern | `dashboard/conflicts.rb:160`, `dashboard/api.rb:589` | 30 min |
55
179
 
56
180
  ---
57
181
 
58
182
  ## 2. Jeremy Evans Perspective
59
183
 
60
184
  ### 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
185
 
68
- ### Medium Issues 🟡
186
+ - 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)
187
+ - `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
188
+ - `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
189
 
70
- | # | Issue | File:Line | Effort |
71
- |---|-------|-----------|--------|
72
- | 8 | **upsert_content_item has 11 keyword parameters** | `store/sqlite_store.rb:158-184` | 1 hour |
190
+ ### Raw SQL Audit
191
+
192
+ 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.
193
+
194
+ ### Transaction Safety
195
+
196
+ New transactional methods all wrap correctly:
197
+ - `Sweep::Maintenance#dedupe_open_conflicts` — wraps in `@store.db.transaction` (line 289)
198
+ - `Sweep::Maintenance#reclassify_references` — wraps in `@store.db.transaction` (line 349)
199
+ - `SQLiteStore#upsert_moment_feedback` — wraps in `@db.transaction` (line 128)
200
+
201
+ ### N+1 Audit (new dashboard panels)
73
202
 
74
- Exceeds the 5-parameter guideline. Suggests the method is doing too much.
203
+ - `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.
204
+ - `Trust#count_open_conflicts` (lib/claude_memory/dashboard/trust.rb:145) → `Conflicts#distinct_open_counts` walks both stores. Acceptable (fixed cardinality of 2).
205
+ - `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.
75
206
 
76
- **Fix:** Introduce a `ContentItemAttributes` value object.
207
+ **Recommendation:**
208
+ - 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.
209
+ - Add explicit `.limit` to `used_fact_pairs` (10,000 is a safe ceiling for a 30-day window).
210
+
211
+ **File:** `lib/claude_memory/dashboard/moments.rb:125,231`
212
+ **Effort:** 45 minutes
213
+ **Priority:** 🟡 Medium (will only bite at scale; fix proactively)
214
+ **Expert principle:** Jeremy Evans dataset hygiene
77
215
 
78
216
  ---
79
217
 
80
218
  ## 3. Kent Beck Perspective
81
219
 
82
220
  ### 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.
221
+
222
+ - **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.
223
+ - New commands `digest_command.rb` and `census_command.rb` shipped with direct specs
224
+ - 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
225
 
89
226
  ### High Priority Issues
90
227
 
91
- None remaining. All high-priority items are resolved.
228
+ #### D. Two new commands shipped without specs
92
229
 
93
- ### Remaining Untested Files (lower priority)
230
+ | Command | LOC | Spec? |
231
+ |---|---|---|
232
+ | `commands/dedupe_conflicts_command.rb` | 55 | ❌ none |
233
+ | `commands/reclassify_references_command.rb` | 56 | ❌ none |
94
234
 
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`
235
+ 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
236
 
104
- ### Medium Issues 🟡
237
+ The output format in particular has logic worth pinning:
238
+ - `dedupe_conflicts_command.rb:38-52` decides `DRY RUN` vs `DEDUPE`, separator length, decisions header
239
+ - `reclassify_references_command.rb:38-53` truncates objects to 100 chars + ellipsis
105
240
 
106
- | # | Issue | File:Line | Effort |
107
- |---|-------|-----------|--------|
108
- | 10 | **Sleep-based tests add 4+ seconds** | `spec/ingest/ingester_spec.rb:43,65,81` | 1 hour |
241
+ **Proposed:** Mirror `digest_command_spec.rb` (or `census_command_spec.rb`) test option parsing, dry-run paths, and stdout shape via injected `StringIO`.
109
242
 
110
- Three `sleep 1.01` calls wait for filesystem mtime changes. `publish_spec.rb:189` has `sleep 1.1`.
243
+ **Effort:** 30 min each (60 min total)
244
+ **Priority:** High — these are admin commands that mutate data; CLI ergonomics belong under test
111
245
 
112
- **Fix:** Mock `File.mtime` or inject a time provider instead of real sleeps.
246
+ #### E. `dashboard/server.rb` still untested
113
247
 
114
- | # | Issue | File:Line | Effort |
115
- |---|-------|-----------|--------|
116
- | 11 | **No shared test factory** | `spec/spec_helper.rb` | 1 hour |
248
+ 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`).
249
+
250
+ 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.
251
+
252
+ **Effort:** 1.5 hours
253
+ **Priority:** Medium-Low
254
+
255
+ ### Sleep-Based Test Latency Increased
256
+
257
+ Total sleep-based test cost in `bundle exec rspec`:
117
258
 
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.
259
+ | Spec | sleep total | Notes |
260
+ |---|---|---|
261
+ | `spec/claude_memory/ingest/ingester_spec.rb` | 3.03s | mtime resolution, carried |
262
+ | `spec/claude_memory/publish_spec.rb` | 1.1s | carried |
263
+ | `spec/claude_memory/recall_spec.rb` | 0.01s | carried |
264
+ | `spec/claude_memory/dashboard/moments_spec.rb` | 2.2s | **NEW** ordering of activity events |
265
+ | `spec/claude_memory/dashboard/api_spec.rb` | 2.2s | **NEW** activity ordering tests |
266
+ | **Total** | **~8.5s** | up from ~4s last review |
119
267
 
120
- **Fix:** Create `spec/support/database_factory.rb` with shared helpers, require from spec_helper.
268
+ 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:
269
+
270
+ ```ruby
271
+ # Option 1: Inject explicit timestamps (already supported via insert column)
272
+ store.activity_events.insert(occurred_at: Time.now.utc.iso8601, ...)
273
+ store.activity_events.insert(occurred_at: (Time.now + 1).utc.iso8601, ...)
274
+
275
+ # Option 2: Stub Time.now via Timecop or RSpec's allow(Time).to receive(:now)
276
+ ```
277
+
278
+ Option 1 requires no extra dep. Either eliminates 4.4s of wall time.
279
+
280
+ **File:** `spec/claude_memory/dashboard/moments_spec.rb:130,132`, `api_spec.rb:332,359`
281
+ **Effort:** 30 minutes
282
+ **Priority:** 🟡 Medium (test speed degrades CI loop)
283
+ **Expert principle:** Kent Beck fast feedback
284
+
285
+ ### Carried-Forward Issues 🟡
286
+
287
+ | # | Issue | File:Line | Effort |
288
+ |---|---|---|---|
289
+ | 11 | No shared test factory | `spec/spec_helper.rb` | 1 hour |
121
290
 
122
291
  ---
123
292
 
124
293
  ## 4. Avdi Grimm Perspective
125
294
 
126
295
  ### 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
296
 
134
- ### Carried Forward Issues 🟡
297
+ - 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
298
+ - `Result` pattern preserved in embeddings paths
299
+ - `Core::RelativeTime.format` used consistently across new dashboard modules
300
+
301
+ ### Bare Rescue Audit (full lib/, current count: 19 bare rescues)
302
+
303
+ The count grew from 5 → 19 because new dashboard code added 5 in `api.rb`. All are defensive (return safe shape):
304
+
305
+ | Location | Context | Returns | Verdict |
306
+ |---|---|---|---|
307
+ | `mcp/handlers/stats_handlers.rb:102` | `fts_legacy?` | `false` | Acceptable — boolean check |
308
+ | `mcp/instructions_builder.rb:147` | `vec_available?` | `false` | Acceptable |
309
+ | `sweep/maintenance.rb:140` | FTS prune | skips row | Acceptable |
310
+ | `commands/hook_command.rb:102` | forked handler | `nil` | Required |
311
+ | `commands/stats_command.rb:276` | `check_fts_format` | no-op | Acceptable |
312
+ | **`dashboard/api.rb:340` (new)** | recall live query | error hash | Acceptable — wide net for unfamiliar errors from Recall pipeline |
313
+ | **`dashboard/api.rb:672` (new)** | `db_stats` aggregation | `{exists:, error:}` | Acceptable |
314
+ | **`dashboard/api.rb:693` (new)** | `db_health` introspection | error hash | Acceptable |
315
+ | **`dashboard/api.rb:728` (new)** | `hooks_health` JSON read | error hash | Acceptable |
316
+ | **`dashboard/api.rb:797` (new)** | `vec_health` | error hash | Acceptable |
317
+
318
+ Verdict: per `Style/RescueStandardError` in Standard Ruby (rejected explicit-rescue change in last review), these are correct. **No action.**
319
+
320
+ ### Carried-Forward Issues 🟡
135
321
 
136
322
  | # | Issue | File:Line | Effort |
137
- |---|-------|-----------|--------|
138
- | 13 | **Inconsistent payload validation in hooks** | `hook/handler.rb:17-53` | 30 min |
323
+ |---|---|---|---|
324
+ | 13 | Inconsistent payload validation | `hook/handler.rb:53-82` | 30 min |
325
+
326
+ Verified still present.
327
+
328
+ ### New Concern
139
329
 
140
- `ingest` uses `.fetch("field")` with fallback, `sweep` uses `.fetch("budget", default)`, `publish` uses `.fetch("mode", "shared")`. No consistent validation pattern.
330
+ #### F. `digest_command.rb:128` reaches into `Trust`'s private API
331
+
332
+ 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
333
 
142
334
  ---
143
335
 
144
336
  ## 5. Gary Bernhardt Perspective
145
337
 
146
338
  ### 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
339
 
152
- ### Carried Forward Issues 🟡
340
+ - New dashboard modules continue to honor the imperative-shell / functional-core split:
341
+ - `Trust` does only reads + transformation (no writes)
342
+ - `Moments` does reads + transformation
343
+ - `Reuse` does reads + transformation
344
+ - `Efficacy::Reporter` is **pure** (no DB) — takes events, returns a hash — Bernhardt's dream
345
+ - `Knowledge#summary` returns shaped data; UI logic stays out of the model
346
+ - New value-object-y data: `KIND_TO_EVENT_TYPES`, `FEED_EVENT_TYPES` are frozen module constants
153
347
 
154
- | # | Issue | File:Line | Effort |
155
- |---|-------|-----------|--------|
156
- | 14 | **I/O mixed with logic in discover_other_projects** | `mcp/tools.rb:565-614` | 1 hour |
348
+ ### Boundaries
157
349
 
158
- SQL queries, filesystem checks, database connections in a loop, and error handling all mixed together.
350
+ ```
351
+ HTTP layer: Dashboard::Server (211 LOC, untested) ← imperative shell
352
+ JSON layer: Dashboard::API (807 LOC ⚠ growing) ← needs to shrink to routing
353
+ Subsystems: Conflicts, Moments, Trust, Knowledge, Reuse ← functional core (good)
354
+ Pure helpers: Efficacy::Reporter, ScopedFactResolver ← pure (excellent)
355
+ Query layer: Recall, store datasets ← impure but isolated
356
+ ```
159
357
 
160
- | # | Issue | File:Line | Effort |
161
- |---|-------|-----------|--------|
162
- | 15 | **Sweeper mutable state** | `sweep/sweeper.rb:16-17` | 20 min |
358
+ `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.
359
+
360
+ ### Test Speed Regression
361
+
362
+ 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.
363
+
364
+ ### Carried-Forward Issues 🟡
163
365
 
164
366
  | # | Issue | File:Line | Effort |
165
- |---|-------|-----------|--------|
166
- | 16 | **Dir.chdir in publish tests** | `spec/publish_spec.rb:14` | 15 min |
367
+ |---|---|---|---|
368
+ | 15 | Sweeper mutable state | `sweep/sweeper.rb:16-17` | 20 min |
369
+ | 16 | `Dir.chdir` in publish tests | `spec/publish_spec.rb:14` | 15 min |
167
370
 
168
371
  ---
169
372
 
170
373
  ## 6. General Ruby Idioms
171
374
 
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`
375
+ ### New Items
177
376
 
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
377
+ | # | Issue | File:Line | Severity | Effort |
378
+ |---|---|---|---|---|
379
+ | 31 | `Dashboard::API` 807 LOC, 11 methods >15 lines (regression of #28) | `dashboard/api.rb` | 🔴 High | 4–6 hours |
380
+ | 32 | `parse_timestamp(value)` duplicated verbatim in api.rb:565 and conflicts.rb:278 | both | 🟢 Low | 15 min |
381
+ | 33 | `stores_for` / `facts_stores_for` near-identical between Conflicts and API | `conflicts.rb:160`, `api.rb:589` | 🟢 Low | 30 min |
382
+ | 34 | `digest_command.rb:128` uses `.send(:utilization)` to call private | `digest_command.rb:128` | 🟡 Medium | 30 min |
383
+ | 35 | Sleep-based dashboard tests add 4.4s to suite | `dashboard/{moments,api}_spec.rb` | 🟡 Medium | 30 min |
384
+ | 36 | DedupeConflictsCommand and ReclassifyReferencesCommand untested | `commands/` | High | 60 min |
385
+ | 37 | `sweep/maintenance.rb` regrew to 456 LOC; 3 methods >50 lines | `sweep/maintenance.rb` | 🟡 Medium | 2 hours |
386
+ | 38 | `Moments#extracted_facts` per-moment join (potential N+1 at 50-row pages) | `moments.rb:231` | 🟡 Medium | 30 min |
387
+
388
+ ### Carried-Forward Items
181
389
 
182
390
  | # | 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 |
391
+ |---|---|---|---|---|
392
+ | 17 | ResponseFormatter duplication | `mcp/response_formatter.rb` | 🟡 Medium | 1 hour |
393
+ | 28 | ~~Dashboard::API method extraction~~ **escalated to #31** | | | |
394
+ | 8 | `upsert_content_item` 11 keyword params | `store/sqlite_store.rb:193` | 🟢 Low | 1 hour |
395
+ | 10 | Sleep-based ingester tests | `spec/ingest/ingester_spec.rb` | 🟢 Low | 1 hour |
396
+ | 11 | No shared test factory | `spec/spec_helper.rb` | 🟢 Low | 1 hour |
186
397
 
187
398
  ---
188
399
 
189
400
  ## 7. Positive Observations
190
401
 
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
402
+ - **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
403
+ - **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
404
+ - **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
405
+ - **`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
406
+ - **No raw SQL added; no N+1 in hot paths; transaction safety maintained** — across +2,011 LOC in 6 days
202
407
 
203
408
  ---
204
409
 
205
410
  ## 8. Priority Refactoring Recommendations
206
411
 
207
- ### High Priority (Next Week)
412
+ ### High Priority (This Week — pre-0.10.0 release)
208
413
 
209
- None remaining. All high-priority items are resolved.
414
+ | # | Item | File:Line | Effort | Impact |
415
+ |---|---|---|---|---|
416
+ | 31 | Extract `Dashboard::Timeline` / `Health` / `RecallQuery` / `RecallTriggerFinder` / `UserPromptExtractor` / `FactsQuery` from API | `dashboard/api.rb` | 4–6 hours | API drops 807→~250 LOC; reverses regression |
417
+ | 36 | Add `dedupe_conflicts_command_spec.rb` + `reclassify_references_command_spec.rb` | `spec/claude_memory/commands/` | 1 hour | CLI surface tested |
210
418
 
211
419
  ### 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 |
420
+
421
+ | # | Item | File:Line | Effort |
422
+ |---|---|---|---|
423
+ | 34 | Promote `Trust#utilization` to public OR extract `Dashboard::Utilization` | `dashboard/trust.rb`, `digest_command.rb:128` | 30 min |
424
+ | 35 | Replace sleep-based dashboard tests with explicit timestamps | `dashboard/{moments,api}_spec.rb` | 30 min |
425
+ | 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 |
426
+ | 38 | Batch `Moments#extracted_facts` to avoid 50-row N+1 | `moments.rb:231` | 30 min |
427
+ | 17 | ResponseFormatter consolidation (carried) | `mcp/response_formatter.rb` | 1 hour |
428
+ | 13 | Payload validator for hook events (carried) | `hook/handler.rb` | 30 min |
429
+ | E | `dashboard/server_spec.rb` (carried) | `spec/claude_memory/dashboard/` | 1.5 hours |
219
430
 
220
431
  ### 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 |
432
+
433
+ | # | Item | Effort |
434
+ |---|---|---|
435
+ | 32 | DRY `parse_timestamp` (`api.rb:565` ↔ `conflicts.rb:278`) | 15 min |
436
+ | 33 | DRY `stores_for` / `facts_stores_for` | 30 min |
437
+ | 8 | `ContentItemAttributes` value object | 1 hour |
438
+ | 10 | Replace sleep-based ingester tests | 1 hour |
439
+ | 11 | Shared test factory | 1 hour |
440
+ | 15 | Sweeper mutable state | 20 min |
441
+ | 16 | `Dir.chdir` in publish tests | 15 min |
442
+
443
+ ### Quick Wins (Today)
444
+
445
+ | # | Item | Effort |
446
+ |---|---|---|
447
+ | 32 | Extract `parse_timestamp` to `Core::RelativeTime` (it already lives there as a value module) | 15 min |
448
+ | 34 | Promote `Trust#utilization` to public | 5 min |
449
+ | 35 | Inject timestamps into dashboard spec inserts | 30 min |
236
450
 
237
451
  ---
238
452
 
239
453
  ## 9. Conclusion
240
454
 
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.
455
+ 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.
242
456
 
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).
457
+ **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.
458
+
459
+ **Recommended next-action set, in order:**
460
+
461
+ 1. **`/quality-update`** to apply #31 (Dashboard::API extraction) and #36 (missing command specs). Target: api.rb ≤ 300 LOC, all commands tested.
462
+ 2. Quick wins #32 + #34 + #35 in the same session (~75 min total).
463
+ 3. Schedule #37 and #38 for the next sprint — neither is urgent but both compound if left alone.
464
+ 4. After #31 lands, `/review-for-quality` again pre-0.10.0 release to confirm the regression closed.
465
+
466
+ 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
467
 
245
468
  ---
246
469
 
247
470
  ## Appendix A: Metrics Comparison
248
471
 
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** ✅ |
472
+ | Metric | Mar 9 | Mar 19 | Apr 22 (review) | Apr 22 (after update) | **Apr 28 (this review)** |
473
+ |---|---|---|---|---|---|
474
+ | Ruby files (lib) | 112 | 117 | 148 | 150 | **161** (+11 new modules) |
475
+ | LOC (lib) | 11,392 | 12,239 | 17,014 | 17,031 | **19,025** (+2,011) |
476
+ | LOC (spec) | 21,632 | 22,563 | 28,074 | 28,490 | **31,079** (+2,605) |
477
+ | Spec files | 128 | 122 | 154 | 156 | **188** (+32) |
478
+ | Test-to-code ratio | 1.90:1 | 1.84:1 | 1.65:1 | 1.67:1 | **1.63:1** ⬇️ |
479
+ | Files >500 lines | 3 | 0 | 2 | 1 | **2** ⬆️ (api.rb 807, sqlite_store.rb 584) |
480
+ | Files >300 lines | 9 | 9 | 10 | 8 | **8** (same count, different mix) |
481
+ | Bare rescues (justified) | 1 | 0 | 5 | 5 | **19** (14 new, all defensive) |
482
+ | Bare rescues (unsafe) | 0 | 0 | 0 | 0 | **0** ✅ |
483
+ | N+1 patterns (hot paths) | 0 | 0 | 0 | 0 | **0** ✅ |
484
+ | Pure logic classes | 20+ | 22+ | 25+ | 27+ | **32+** (+5 new dashboard modules) |
485
+ | Migration round-trip specs | 0 | 0 | 0 | 0 | **3** (v12→v17, v13→v17, v14→v17) ✅ |
486
+ | Per-migration specs | 0 | 0 | 0 | 0 | **13** (001–017 minus a few) ✅ |
487
+ | Sleep-based test cost | — | — | ~4s | ~4s | **~8.5s** ⬆️ |
488
+ | Untested new commands | — | — | 0 | 0 | **2** (dedupe-conflicts, reclassify-references) |
489
+ | Known correctness bugs | — | 0 | 0 | 0 | **0** ✅ |
263
490
 
264
491
  ## Appendix B: File Size Report
265
492
 
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 |
493
+ | File | Mar 19 | Apr 22 (review) | Apr 22 (after update) | **Apr 28 (this review)** | Trend |
494
+ |---|---|---|---|---|---|
495
+ | `dashboard/api.rb` | | 627 🆕 | 627 | **807** | ⬆️ +180 (+29%) **regression** |
496
+ | `store/sqlite_store.rb` | 386 | 683 | 544 | **584** | ⬆️ +40 (new tables) |
497
+ | `mcp/tool_definitions.rb` | 334 | 459 | 459 | **459** | |
498
+ | `sweep/maintenance.rb` | | 334 | 334 | **456** | ⬆️ +122 — new |
499
+ | `mcp/response_formatter.rb` | 396 | 397 | 397 | **397** | — |
500
+ | `commands/stats_command.rb` | 250 | 346 | 346 | **383** | ⬆️ +37 |
501
+ | `recall/query_core.rb` | 357 | 371 | 371 | **371** | — |
502
+ | `mcp/text_summary.rb` | 258 | 313 | 313 | **313** | — |
503
+ | `dashboard/conflicts.rb` | | 195 | 195 | **285** | ⬆️ +90 (dedup grouping logic) |
504
+ | `dashboard/trust.rb` | | | — | **284** | 🆕 new feed-first sidebar |
505
+ | `resolve/resolver.rb` | 195 | 254 | 254 | **268** | ⬆️ +14 (dedupe + scope_hint fix) |
506
+ | `mcp/tools.rb` | 104 | 249 | 249 | **264** | ⬆️ +15 |
507
+ | `commands/index_command.rb` | 272 | 259 | 259 | **259** | — |
508
+ | `commands/hook_command.rb` | 214 | 215 | 215 | **249** | ⬆️ +34 |
509
+ | `publish.rb` | 221 | 256 | 248 | **248** | — |
510
+ | `dashboard/moments.rb` | — | — | — | **244** | 🆕 feed primitive |
511
+ | `commands/uninstall_command.rb` | 226 | 226 | 226 | **226** | — |
512
+ | `hook/context_injector.rb` | — | 214 | 214 | **225** | ⬆️ +11 |
513
+ | `store/store_manager.rb` | — | 215 | 215 | **215** | — |
514
+ | `infrastructure/schema_validator.rb` | 215 | 215 | 215 | **215** | — |
515
+ | `commands/census_command.rb` | — | — | — | **210** | 🆕 predicate census |
516
+ | `mcp/handlers/setup_handlers.rb` | 211 | 211 | 211 | **211** | — |
517
+ | `dashboard/server.rb` | — | 189 | 189 | **211** | ⬆️ +22 (new endpoints) |
518
+ | `embeddings/model_registry.rb` | — | — | — | **210** | 🆕 |
519
+ | `mcp/server.rb` | — | 206 | 206 | **206** | — |
520
+ | `mcp/handlers/stats_handlers.rb` | — | 205 | 205 | **205** | — |
521
+ | `commands/initializers/hooks_configurator.rb` | — | — | — | **200** | — |
522
+ | `commands/embeddings_command.rb` | — | — | — | **198** | — |
523
+ | `ingest/ingester.rb` | — | — | — | **190** | — |
524
+ | `index/vector_index.rb` | 184 | 184 | 184 | **184** | — |
525
+ | `commands/digest_command.rb` | — | — | — | **181** | 🆕 weekly digest |
526
+ | `mcp/handlers/management_handlers.rb` | — | — | — | **177** | — |
527
+ | `ingest/observation_compressor.rb` | — | — | — | **177** | 🆕 tool-specific compression |
528
+ | `recall.rb` | 94 | 175 | 175 | **175** | — |
529
+ | `core/fact_query_builder.rb` | — | — | — | **174** | — |
530
+ | `mcp/error_classifier.rb` | — | — | — | **171** | — |
531
+ | `embeddings/generator.rb` | — | — | — | **165** | — |
532
+ | `index/lexical_fts.rb` | — | — | — | **153** | — |
533
+ | `dashboard/knowledge.rb` | — | — | — | **136** | 🆕 |
534
+ | `dashboard/efficacy.rb` | — | 127 | 127 | **127** | — |
535
+ | `dashboard/fact_presenter.rb` | — | 109 | 109 | **109** | — |
536
+ | `dashboard/reuse.rb` | — | — | — | **97** | 🆕 |
537
+ | `dashboard/scoped_fact_resolver.rb` | — | — | — | **95** | 🆕 |
538
+ | `commands/reclassify_references_command.rb` | — | — | — | **56** | 🆕 (untested) |
539
+ | `commands/dedupe_conflicts_command.rb` | — | — | — | **55** | 🆕 (untested) |
540
+
541
+ ## Appendix C: Methods >15 Lines in Watch-List Files
542
+
543
+ ### `dashboard/api.rb` (807 LOC, **42 methods**)
544
+
545
+ | Method | Line | Size | Action |
546
+ |---|---|---|---|
547
+ | `timeline` | 471 | 52 | Extract `Dashboard::Timeline` |
548
+ | `vec_health` | 759 | 46 | Extract into `Dashboard::Health` |
549
+ | `recall` | 315 | 41 | Extract `Dashboard::RecallQuery` |
550
+ | `facts` | 373 | 39 | Extract `Dashboard::FactsQuery` |
551
+ | `activity_detail` | 149 | 37 | Extract event-detail builder |
552
+ | `hooks_health` | 704 | 32 | Extract into `Dashboard::Health` |
553
+ | `find_recall_trigger` | 193 | 32 | Extract `Dashboard::RecallTriggerFinder` |
554
+ | `efficacy` | 439 | 31 | Move session-window logic into `Efficacy::Loader` |
555
+ | `extract_user_prompt` | 237 | 29 | Extract `Dashboard::UserPromptExtractor` |
556
+ | `session_summary` | 119 | 29 | Extract aggregator |
557
+ | `db_stats` | 647 | 28 | Extract into `Dashboard::Health` |
558
+ | `db_health` | 676 | 25 | Extract into `Dashboard::Health` |
559
+ | `load_content_item` | 603 | 21 | Could move into `FactPresenter` or its own loader |
560
+ | `activity` | 48 | 20 | Acceptable — thin wrapper |
561
+ | `facts_seen_in_recent_recalls` | 418 | 20 | Move into `Dashboard::FactsQuery` |
562
+ | `collect_configured_hook_types` | 739 | 19 | Move into `Dashboard::Health` |
563
+ | `serialize_recall_fact` | 545 | 19 | Move into `Dashboard::RecallQuery` |
564
+ | `health` | 14 | 18 | Becomes 3-liner after `Dashboard::Health` extraction |
565
+ | `reject_fact` | 294 | 16 | Acceptable — public surface |
566
+
567
+ ### `sweep/maintenance.rb` (456 LOC)
568
+
569
+ | Method | Line | Size | Action |
570
+ |---|---|---|---|
571
+ | `dedupe_open_conflicts` | 273 | 58 | Extract per-group `resolve_duplicate_group` helper |
572
+ | `restore_multi_value_supersessions` | 185 | 57 | Already documented; could extract `compute_restore_decisions` |
573
+ | `dedupe_multi_value_facts` | 58 | 34 | Acceptable — well-bounded transactional op |
574
+ | `reclassify_references` | 340 | 26 | Acceptable |
575
+ | `prune_old_content` | 130 | 16 | Acceptable |
576
+
577
+ ### `store/sqlite_store.rb` (584 LOC)
578
+
579
+ | Method | Line | Size | Notes |
580
+ |---|---|---|---|
581
+ | `upsert_content_item` | 193 | 27 | 11 kwargs (carried #8) |
582
+ | `reject_fact` | 410 | 25 | Conflict resolution in transaction |
583
+ | `insert_fact` | 332 | 22 | Many optional fields |
584
+ | `upsert_moment_feedback` | 123 | 21 | New — transaction with retry |
585
+ | `update_fact` | 373 | 19 | Generic update via allowed-keys |
586
+
587
+ ---
588
+
589
+ ## Historical Reviews
590
+
591
+ Earlier reviews (Jan 29, Feb 4, Mar 9, Mar 19) tracked the codebase from ~8,000 → 12,239 LOC. Their highlights, preserved here:
592
+
593
+ - **Jan 29 (initial)** — Identified Tools and Recall god-object risks; introduced first metrics baseline.
594
+ - **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.
595
+ - **Mar 9** — Three files >500 LOC; bare rescue counted; vector index work landed.
596
+ - **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.
597
+
598
+ 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
599
 
283
600
  ---
284
601
 
285
- **Next review:** After Recall strategy pattern refactoring or SQLiteStore extraction
602
+ **Next review:** After #31 (Dashboard::API extraction) lands, or pre-0.10.0 release tag.