claude_memory 0.6.0 → 0.7.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.
@@ -1,16 +1,18 @@
1
1
  # Code Quality Review - Ruby Best Practices
2
2
 
3
- **Review Date:** 2026-02-04
4
- **Previous Review:** 2026-01-29
3
+ **Review Date:** 2026-03-09
4
+ **Previous Review:** 2026-02-04
5
5
  **Last Quality Update:** 2026-02-04 (21/24 items completed)
6
6
 
7
7
  ---
8
8
 
9
9
  ## Executive Summary
10
10
 
11
- The codebase is in strong shape after a comprehensive quality pass on Feb 4. All critical and high-priority issues from the review have been resolved: N+1 queries eliminated, bare rescues replaced with specific exception types, mutation patterns fixed in functional core, and long methods decomposed into focused helpers.
11
+ The codebase has grown from 9,982 to 11,392 LOC since the Feb 4 review. Core architecture remains solid functional core, proper layering, zero bare rescues, zero N+1 queries in hot paths. The 3 files on the previous watch list have all grown: `tools.rb` (610→728), `recall.rb` (608→681), `sqlite_store.rb` (481→547). All three now exceed 500 lines.
12
12
 
13
- **Remaining work:** 9 items (1 medium, 8 low priority). No critical or high-priority issues remain.
13
+ **New issues found:** 18 items (2 critical, 4 high, 7 medium, 5 low)
14
+ **Carried forward:** 9 items from previous review (1 medium, 8 low)
15
+ **Total remaining:** 27 items
14
16
 
15
17
  ### Current Strengths
16
18
 
@@ -18,85 +20,371 @@ The codebase is in strong shape after a comprehensive quality pass on Feb 4. All
18
20
  - Domain objects: properly frozen and self-validating
19
21
  - Null object pattern: NullFact, NullExplanation
20
22
  - Result monad: Core::Result for Success/Failure
21
- - 100% frozen_string_literal compliance (104 files)
22
- - 1.77:1 test-to-code ratio (17,693 spec : 9,982 lib)
23
- - Zero bare rescues, zero N+1 queries
23
+ - 100% frozen_string_literal compliance (112 files)
24
+ - 1.90:1 test-to-code ratio (21,632 spec : 11,392 lib)
25
+ - Zero bare rescues in hot paths, zero N+1 in query paths
26
+ - Well-structured batch loading via FactQueryBuilder
24
27
 
25
28
  ---
26
29
 
27
- ## Remaining Items
30
+ ## 1. Sandi Metz Perspective
28
31
 
29
- ### Medium Priority
32
+ ### What's Been Fixed ✅
33
+ - N+1 queries eliminated in prior review
34
+ - Long methods decomposed (resolve_fact, detailed_stats, check_setup)
35
+ - Classes extracted (SchemaValidator, OperationTracker)
30
36
 
31
- | # | Issue | File:Line | Expert |
37
+ ### Critical Issues 🔴
38
+
39
+ | # | Issue | File:Line | Effort |
40
+ |---|-------|-----------|--------|
41
+ | 1 | **Tools god object (728 lines, 48 methods)** | `mcp/tools.rb:1-728` | 2-3 days |
42
+
43
+ The `Tools` class handles all 21 MCP tool implementations in a single file. The `call` dispatcher (lines 29-76) is a 21-branch case statement. Each handler mixes parameter extraction, domain logic, and response formatting. Individual tool handlers like `store_extraction` (lines 221-262, 41 lines) and `discover_other_projects` (lines 565-614, 50 lines) violate the 15-line method limit.
44
+
45
+ **Fix:** Extract each tool handler into its own class (e.g., `RecallHandler`, `StoreExtractionHandler`) with a common interface, registered via a handler registry.
46
+
47
+ | # | Issue | File:Line | Effort |
48
+ |---|-------|-----------|--------|
49
+ | 2 | **Recall class too large (681 lines, 74 methods)** | `recall.rb:1-681` | 2 days |
50
+
51
+ Every public method branches on `@legacy_mode` with parallel `_legacy` / `_dual` implementations (lines 42-56, 58-66, etc.). ~150+ lines of duplicated branching logic. The class has 74 methods — far beyond single responsibility.
52
+
53
+ **Fix:** Extract `LegacyQueryEngine` and `DualQueryEngine` classes implementing a common `QueryEngine` interface. Inject the appropriate engine at initialization based on `store_or_manager` type.
54
+
55
+ ### High Priority Issues
56
+
57
+ | # | Issue | File:Line | Effort |
58
+ |---|-------|-----------|--------|
59
+ | 3 | **ExportCommand N+1 queries** | `commands/export_command.rb:83-85` | 30 min |
60
+
61
+ Inside the `collect_from_store` loop, each fact triggers 2 individual queries — one for the subject entity (line 84) and one for provenance records (line 85). With 100+ facts this becomes 200+ queries.
62
+
63
+ ```ruby
64
+ # Current (N+1):
65
+ facts_ds.each do |fact|
66
+ subject = store.entities.where(id: fact[:subject_entity_id]).first
67
+ receipts = store.provenance.where(fact_id: fact[:id]).all
68
+ end
69
+
70
+ # Fix (batch):
71
+ all_facts = facts_ds.all
72
+ entity_ids = all_facts.map { |f| f[:subject_entity_id] }.compact.uniq
73
+ entities_by_id = store.entities.where(id: entity_ids).all
74
+ .each_with_object({}) { |e, h| h[e[:id]] = e }
75
+ fact_ids = all_facts.map { |f| f[:id] }
76
+ provenance_by_fact = store.provenance.where(fact_id: fact_ids).all
77
+ .group_by { |p| p[:fact_id] }
78
+ ```
79
+
80
+ | # | Issue | File:Line | Effort |
81
+ |---|-------|-----------|--------|
82
+ | 4 | **SQLiteStore exceeds 500 lines (547)** | `store/sqlite_store.rb:1-547` | 1 day |
83
+
84
+ The file combines database connection, retry logic, schema management, migrations, and all CRUD operations. Schema migrations alone account for ~100 lines.
85
+
86
+ **Fix:** Extract `SchemaManager` module for migration methods, and consider a `RetryHandler` module for retry logic (lines 24-60).
87
+
88
+ ---
89
+
90
+ ## 2. Jeremy Evans Perspective
91
+
92
+ ### What's Been Fixed ✅
93
+ - Batch queries in Recall pipeline (FactQueryBuilder)
94
+ - Transaction wrapping in Resolver
95
+ - Proper Sequel DSL usage throughout
96
+
97
+ ### Critical Issues 🔴
98
+
99
+ | # | Issue | File:Line | Effort |
100
+ |---|-------|-----------|--------|
101
+ | 5 | **Bare rescue in discover_other_projects** | `mcp/tools.rb:607` | 10 min |
102
+
103
+ ```ruby
104
+ rescue => _e
105
+ entry[:error] = "Could not read database"
106
+ end
107
+ ```
108
+
109
+ This catches *all* exceptions including `NoMemoryError`, `SystemExit`, `Interrupt`. Should use specific exception types.
110
+
111
+ **Fix:** `rescue Sequel::DatabaseError, Extralite::Error, IOError => _e`
112
+
113
+ ### Medium Issues 🟡
114
+
115
+ | # | Issue | File:Line | Effort |
116
+ |---|-------|-----------|--------|
117
+ | 6 | **Transaction boundary mismatch in promote_fact** | `store/store_manager.rb:89-124` | 30 min |
118
+
119
+ The transaction wraps `@global_store.db.transaction` but `copy_provenance` (line 121) reads from `@project_store` inside the global transaction. If `@project_store` fails mid-read after global writes, the global transaction still commits (autocommit on the project side). Not a data loss risk but a consistency concern.
120
+
121
+ **Fix:** Read all provenance records from project store *before* the global transaction:
122
+
123
+ ```ruby
124
+ provenance_records = @project_store.provenance.where(fact_id: fact_id).all
125
+ @global_store.db.transaction do
126
+ # ... create entities and fact ...
127
+ provenance_records.each { |prov| @global_store.insert_provenance(...) }
128
+ end
129
+ ```
130
+
131
+ | # | Issue | File:Line | Effort |
132
+ |---|-------|-----------|--------|
133
+ | 7 | **Provenance insert with nil content_item_id** | `store/store_manager.rb:133` | 20 min |
134
+
135
+ `copy_provenance` passes `content_item_id: nil` when promoting facts. If `insert_provenance` in SQLiteStore validates this field, it will fail silently or raise. The Domain `Provenance` class validates non-nil `content_item_id`.
136
+
137
+ **Fix:** Use a sentinel value like `"promoted"` or make `content_item_id` nullable in the provenance domain model for promoted facts.
138
+
139
+ | # | Issue | File:Line | Effort |
140
+ |---|-------|-----------|--------|
141
+ | 8 | **upsert_content_item has 11 keyword parameters** | `store/sqlite_store.rb:158-184` | 1 hour |
142
+
143
+ Exceeds the 5-parameter guideline significantly. Suggests the method is doing too much.
144
+
145
+ **Fix:** Introduce a `ContentItemAttributes` value object:
146
+
147
+ ```ruby
148
+ attrs = ContentItemAttributes.new(source:, session_id:, text_hash:, byte_len:, ...)
149
+ store.upsert_content_item(attrs)
150
+ ```
151
+
152
+ ---
153
+
154
+ ## 3. Kent Beck Perspective
155
+
156
+ ### What's Been Fixed ✅
157
+ - Test-to-code ratio improved to 1.90:1
158
+ - Clear boundaries between layers
159
+
160
+ ### High Priority Issues
161
+
162
+ | # | Issue | File:Line | Effort |
163
+ |---|-------|-----------|--------|
164
+ | 9 | **16 lib files without tests** | Multiple | 2-3 days |
165
+
166
+ Critical untested files:
167
+ - `embeddings/generator.rb` (161 lines of math/algorithm code)
168
+ - `embeddings/similarity.rb`
169
+ - `embeddings/fastembed_adapter.rb`
170
+ - `commands/stats_command.rb` (239 lines)
171
+ - `commands/export_command.rb` (108 lines)
172
+ - `commands/recover_command.rb` (75 lines)
173
+ - `infrastructure/schema_validator.rb` (215 lines)
174
+ - `ingest/metadata_extractor.rb`, `ingest/tool_extractor.rb`
175
+
176
+ Database migrations 8-11 also lack migration-specific tests.
177
+
178
+ ### Medium Issues 🟡
179
+
180
+ | # | Issue | File:Line | Effort |
32
181
  |---|-------|-----------|--------|
33
- | 16 | Resolver mutable state after init | `resolve/resolver.rb:10-13` | Gary Bernhardt |
182
+ | 10 | **Sleep-based tests add 4+ seconds** | `spec/ingest/ingester_spec.rb:43,65,81` | 1 hour |
34
183
 
35
- `@current_project_path` and `@current_scope` are set in `apply()` rather than threaded as parameters. Should pass through method chain instead of mutable instance state.
184
+ Three `sleep 1.01` calls wait for filesystem mtime changes. `publish_spec.rb:189` has `sleep 1.1`.
36
185
 
37
- ### Low Priority
186
+ **Fix:** Mock `File.mtime` or inject a time provider instead of real sleeps.
38
187
 
39
- | # | Issue | File:Line | Expert |
188
+ | # | Issue | File:Line | Effort |
40
189
  |---|-------|-----------|--------|
41
- | 17 | DateTime migration (string timestamps) | Multiple files | Jeremy Evans |
42
- | 18 | Strategy pattern in Recall (608 lines) | `recall.rb` | Sandi Metz |
43
- | 19 | Command manager helper (`with_manager`) | `commands/*.rb` | Kent Beck |
44
- | 20 | release_connections polymorphism | `mcp/server.rb:148-156` | Gary Bernhardt |
45
- | 21 | Sweeper mutable state | `sweep/sweeper.rb:16-17` | Gary Bernhardt |
46
- | 22 | Provenance batch insert (`multi_insert`) | `store/store_manager.rb:129-139` | Jeremy Evans |
47
- | 23 | Individual MCP tool classes | `mcp/tools.rb` | Sandi Metz |
48
- | 24 | Result objects for all queries | Multiple files | Avdi Grimm |
190
+ | 11 | **No shared test factory** | `spec/spec_helper.rb` | 1 hour |
191
+
192
+ `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.
193
+
194
+ **Fix:** Create `spec/support/database_factory.rb` with shared helpers, require from spec_helper.
49
195
 
50
196
  ---
51
197
 
52
- ## Risk Assessment
198
+ ## 4. Avdi Grimm Perspective
199
+
200
+ ### What's Been Fixed ✅
201
+ - Null objects used properly (NullFact, NullExplanation)
202
+ - Result monad for Success/Failure
203
+ - Domain objects frozen and self-validating
204
+
205
+ ### Medium Issues 🟡
206
+
207
+ | # | Issue | File:Line | Effort |
208
+ |---|-------|-----------|--------|
209
+ | 12 | **Resolver mutable state after init** | `resolve/resolver.rb:12-13` | 30 min |
210
+
211
+ `@current_project_path` and `@current_scope` are set in `apply()` (line 12-13) rather than threaded as parameters through the method chain. This makes the Resolver stateful between calls.
212
+
213
+ **Fix:** Pass `project_path` and `scope` as explicit parameters to `resolve_fact`, `create_conflict`, etc.
214
+
215
+ *(Carried forward from previous review as #16)*
216
+
217
+ | # | Issue | File:Line | Effort |
218
+ |---|-------|-----------|--------|
219
+ | 13 | **Inconsistent payload validation in hooks** | `hook/handler.rb:17-53` | 30 min |
220
+
221
+ `ingest` uses `.fetch("field")` with fallback, `sweep` uses `.fetch("budget", default)`, `publish` uses `.fetch("mode", "shared")`. No consistent schema validation pattern.
53
222
 
54
- | Area | Risk Level | Notes |
55
- |------|-----------|-------|
56
- | **Performance** | ✅ Low | N+1 queries fixed |
57
- | **Maintainability** | ✅ Low | Long methods decomposed |
58
- | **Correctness** | ✅ Low | databases_exist? fixed, ResultSorter non-mutating |
59
- | **Error Handling** | ✅ Low | All bare rescues replaced with specific types |
60
- | **Architecture** | ✅ Low | Strong functional core, proper layering |
61
- | **Testing** | ✅ Low | 1.77:1 ratio, 98 spec files |
223
+ **Fix:** Create a `PayloadValidator` or use a simple schema hash to validate required/optional fields uniformly.
62
224
 
63
225
  ---
64
226
 
65
- ## Metrics
227
+ ## 5. Gary Bernhardt Perspective
228
+
229
+ ### What's Been Fixed ✅
230
+ - Strong functional core / imperative shell separation
231
+ - Value objects (FactId, SessionId, TranscriptPath) are immutable
232
+ - Pure logic in FactRanker, ConceptRanker, SnippetExtractor
233
+
234
+ ### High Priority Issues
235
+
236
+ | # | Issue | File:Line | Effort |
237
+ |---|-------|-----------|--------|
238
+ | 14 | **I/O mixed with logic in discover_other_projects** | `mcp/tools.rb:565-614` | 1 hour |
239
+
240
+ This method performs: SQL queries (lines 572-590), filesystem checks (line 598: `File.exist?`), database connections in a loop (lines 602-606), and error handling. Pure imperative shell with no separation.
66
241
 
67
- | Metric | Jan 29 | Feb 4 |
68
- |--------|--------|-------|
69
- | Ruby files (lib) | ~85 | 104 |
70
- | LOC (lib) | ~8,000 | 9,982 |
71
- | Pure logic classes | 17+ | 20+ |
72
- | Test files | 74+ | 98 |
73
- | Test-to-code ratio | ~1.5:1 | 1.77:1 |
74
- | Files >500 lines | 0 | 2 (tools, recall) 🟡 |
75
- | Bare rescues | 0 | 0 ✅ |
76
- | N+1 patterns | 0 | 0 ✅ |
242
+ **Fix:** Extract database discovery to a pure function that returns paths, and filesystem/DB checks to an imperative wrapper.
243
+
244
+ ### Medium Issues 🟡
245
+
246
+ | # | Issue | File:Line | Effort |
247
+ |---|-------|-----------|--------|
248
+ | 15 | **Sweeper mutable state** | `sweep/sweeper.rb:16-17` | 20 min |
249
+
250
+ *(Carried forward from previous review as #21)*
251
+
252
+ | # | Issue | File:Line | Effort |
253
+ |---|-------|-----------|--------|
254
+ | 16 | **Dir.chdir in publish tests** | `spec/publish_spec.rb:14` | 15 min |
77
255
 
78
- ## File Size Watch List
256
+ Tests use `Dir.chdir(test_dir)` which modifies global state. Fragile if tests ever run in parallel.
79
257
 
80
- | File | Lines | Concern |
81
- |------|-------|---------|
82
- | `mcp/tools.rb` | ~610 | Consider individual tool classes (#23) |
83
- | `recall.rb` | ~608 | Consider strategy pattern extraction (#18) |
84
- | `store/sqlite_store.rb` | 481 | Trending up — watch for 500 |
258
+ **Fix:** Use `Dir.chdir(test_dir) { ... }` block form or inject working directory.
85
259
 
86
260
  ---
87
261
 
88
- ## Completed (Feb 4, 2026)
262
+ ## 6. General Ruby Idioms
263
+
264
+ | # | Issue | File:Line | Severity | Effort |
265
+ |---|-------|-----------|----------|--------|
266
+ | 17 | **ResponseFormatter duplication** | `mcp/response_formatter.rb:27-280` | 🟡 Medium | 1 hour |
267
+ | 18 | **Publish section generator repetition** | `publish.rb:100-154` | Low | 30 min |
268
+ | 19 | **SnippetExtractor validation duplication** | `core/snippet_extractor.rb:18-31` | Low | 10 min |
269
+
270
+ `ResponseFormatter` has 4 nearly identical `format_*_fact` methods (`format_recall_fact`, `format_semantic_fact`, `format_concept_fact`, etc.) sharing ~80% of code. Extract a base `format_fact` method with field selection.
271
+
272
+ `Publish` has 4 similar section generators (decisions, conventions, constraints, conflicts) each filtering facts by predicate and building markdown. Extract a `SectionBuilder`.
273
+
274
+ ---
275
+
276
+ ## 7. Positive Observations
277
+
278
+ - **Batch loading architecture**: `FactQueryBuilder` and `BatchLoader` eliminate N+1 patterns in all hot query paths
279
+ - **Consistent dependency injection**: All commands accept `stdout`, `stderr`, `stdin` for testability
280
+ - **Clean module boundaries**: Each module has clear responsibilities with minimal cross-coupling
281
+ - **Proper Sequel usage**: Datasets used consistently, raw SQL avoided almost entirely
282
+ - **Excellent domain modeling**: Fact, Entity, Provenance are immutable value objects with validation
283
+ - **Good file organization**: ~1 class per file, consistent naming, clear module nesting
284
+ - **Strong test culture**: 1.90:1 test-to-code ratio, behavior-focused tests
285
+ - **Infrastructure abstractions**: `FileSystem`, `InMemoryFileSystem` enable fast tests
286
+ - **Core::Result monad**: Consistent Success/Failure pattern throughout
287
+
288
+ ---
289
+
290
+ ## 8. Priority Refactoring Recommendations
291
+
292
+ ### Critical (This Week)
293
+ | # | Item | Effort | Impact |
294
+ |---|------|--------|--------|
295
+ | 5 | Fix bare rescue in `discover_other_projects` | 10 min | Correctness |
296
+ | 3 | Fix ExportCommand N+1 queries | 30 min | Performance |
297
+
298
+ ### High Priority (Next Week)
299
+ | # | Item | Effort | Impact |
300
+ |---|------|--------|--------|
301
+ | 1 | Extract Tools into handler classes | 2-3 days | Maintainability |
302
+ | 2 | Extract Recall legacy/dual into strategy | 2 days | Maintainability |
303
+ | 9 | Add tests for untested critical files | 2-3 days | Coverage |
304
+ | 4 | Extract SQLiteStore schema/retry modules | 1 day | Maintainability |
305
+
306
+ ### Medium Priority (Next Sprint)
307
+ | # | Item | Effort | Impact |
308
+ |---|------|--------|--------|
309
+ | 6 | Fix promote_fact transaction boundary | 30 min | Consistency |
310
+ | 7 | Fix provenance nil content_item_id | 20 min | Correctness |
311
+ | 8 | ContentItemAttributes value object | 1 hour | Readability |
312
+ | 10 | Replace sleep-based tests with mocks | 1 hour | Test speed |
313
+ | 11 | Shared test factory | 1 hour | DRY |
314
+ | 12 | Thread Resolver state as params | 30 min | Immutability |
315
+ | 17 | ResponseFormatter base method | 1 hour | DRY |
316
+ | 14 | Separate I/O in discover_other_projects | 1 hour | Boundaries |
317
+
318
+ ### Low Priority (Later)
319
+ | # | Item | Effort | Impact |
320
+ |---|------|--------|--------|
321
+ | 13 | Payload validator for hooks | 30 min | Consistency |
322
+ | 15 | Sweeper mutable state | 20 min | Immutability |
323
+ | 16 | Dir.chdir in tests | 15 min | Test isolation |
324
+ | 18 | Publish section builder | 30 min | DRY |
325
+ | 19 | SnippetExtractor validation DRY | 10 min | DRY |
326
+
327
+ ### Carried Forward (Low Priority from Feb 4)
328
+ | # | Item | Original # |
329
+ |---|------|-----------|
330
+ | 20 | DateTime migration (string timestamps) | #17 |
331
+ | 21 | Command manager helper (`with_manager`) | #19 |
332
+ | 22 | release_connections polymorphism | #20 |
333
+ | 23 | Provenance batch insert (`multi_insert`) | #22 |
334
+ | 24 | Individual MCP tool classes | #23 (subsumed by #1) |
335
+ | 25 | Result objects for all queries | #24 |
336
+
337
+ ---
338
+
339
+ ## 9. Conclusion
340
+
341
+ The codebase maintains its strong architectural foundation but the three largest files have continued growing and now all exceed 500 lines. The most impactful improvements are: (1) fixing the bare rescue and N+1 in export (quick wins), (2) splitting `Tools` and `Recall` into focused classes (structural), and (3) adding tests for the 16 untested files (coverage).
342
+
343
+ No correctness regressions found. The batch loading patterns, domain modeling, and test culture remain excellent. The main risk is the growing complexity of `tools.rb` and `recall.rb` making future changes harder.
344
+
345
+ ---
346
+
347
+ ## Appendix A: Metrics Comparison
348
+
349
+ | Metric | Jan 29 | Feb 4 | Mar 9 |
350
+ |--------|--------|-------|-------|
351
+ | Ruby files (lib) | ~85 | 104 | 112 |
352
+ | LOC (lib) | ~8,000 | 9,982 | 11,392 |
353
+ | LOC (spec) | — | 17,693 | 21,632 |
354
+ | Pure logic classes | 17+ | 20+ | 20+ |
355
+ | Test files | 74+ | 98 | 128 |
356
+ | Test-to-code ratio | ~1.5:1 | 1.77:1 | 1.90:1 |
357
+ | Files >500 lines | 0 | 2 | **3** 🔴 |
358
+ | Bare rescues | 0 | 0 | **1** 🔴 |
359
+ | N+1 patterns (hot paths) | 0 | 0 | 0 ✅ |
360
+ | N+1 patterns (cold paths) | — | — | **1** 🟡 |
361
+ | Untested lib files | — | — | **16** 🟡 |
362
+
363
+ ## Appendix B: Quick Wins
89
364
 
90
- <details>
91
- <summary>21 items completed in 7 atomic commits</summary>
365
+ These can be done immediately (< 30 min total):
92
366
 
93
- **Quick Wins (6):** bare rescue in server.rb, tool_extractor.rb, stats_command.rb; ResultSorter mutation; RRFusion mutation; databases_exist? logic
367
+ 1. **Fix bare rescue** (`mcp/tools.rb:607`): Change `rescue => _e` to `rescue Sequel::DatabaseError, Extralite::Error, IOError => _e`
368
+ 2. **SnippetExtractor DRY** (`core/snippet_extractor.rb:18-31`): Extract shared validation to private method
369
+ 3. **Dir.chdir block form** (`spec/publish_spec.rb:14`): Use `Dir.chdir(dir) { ... }` instead of global chdir
94
370
 
95
- **High Priority (8):** N+1 provenance query; N+1 legacy query; check_setup extraction; detailed_stats extraction; resolve_fact decomposition; ingester transaction body extraction
371
+ ## Appendix C: File Size Report
96
372
 
97
- **Medium Priority (7):** RRFusion mutation; OperationTracker DRY; ToolExtractor bare rescue; databases_exist?; stats_command bare rescue; SchemaValidator.validate extraction; FactGraph.build decomposition
98
- </details>
373
+ | File | Feb 4 | Mar 9 | Trend |
374
+ |------|-------|-------|-------|
375
+ | `mcp/tools.rb` | ~610 | 728 | ⬆️ +118 |
376
+ | `recall.rb` | ~608 | 681 | ⬆️ +73 |
377
+ | `store/sqlite_store.rb` | 481 | 547 | ⬆️ +66 |
378
+ | `mcp/response_formatter.rb` | — | 394 | new to watch |
379
+ | `mcp/tool_definitions.rb` | — | 303 | new to watch |
380
+ | `mcp/text_summary.rb` | — | 257 | new to watch |
381
+ | `commands/stats_command.rb` | — | 239 | — |
382
+ | `commands/uninstall_command.rb` | — | 226 | — |
383
+ | `commands/index_command.rb` | — | 224 | — |
384
+ | `publish.rb` | — | 221 | — |
385
+ | `infrastructure/schema_validator.rb` | — | 215 | — |
386
+ | `commands/hook_command.rb` | — | 214 | — |
99
387
 
100
388
  ---
101
389
 
102
- **Next review:** After recall.rb strategy pattern or sqlite_store.rb extraction
390
+ **Next review:** After Tools extraction or Recall strategy pattern refactoring
@@ -81,6 +81,13 @@ module ClaudeMemory
81
81
  end
82
82
  details[:stuck_operations] = stuck_ops.size
83
83
 
84
+ # Check FTS table format
85
+ fts_sql = store.db.fetch("SELECT sql FROM sqlite_master WHERE name = 'content_fts' AND type = 'table'").first
86
+ if fts_sql && !fts_sql[:sql].to_s.include?("content=''")
87
+ details[:fts_legacy] = true
88
+ warnings << "FTS index uses legacy format (stores duplicate text). Run 'claude-memory compact' to save ~40% disk space."
89
+ end
90
+
84
91
  # Run schema validation
85
92
  validator = ClaudeMemory::Infrastructure::SchemaValidator.new(store)
86
93
  validation = validator.validate
@@ -50,6 +50,9 @@ module ClaudeMemory
50
50
  stdout.puts "#{label}: integrity check passed"
51
51
  end
52
52
 
53
+ stdout.puts "#{label}: rebuilding FTS index..."
54
+ rebuild_fts(db_path)
55
+
53
56
  stdout.puts "#{label}: compacting..."
54
57
  run_vacuum(db_path)
55
58
 
@@ -59,6 +62,13 @@ module ClaudeMemory
59
62
  stdout.puts "#{label}: #{format_size(size_before)} -> #{format_size(size_after)} (#{format_saved(saved)})"
60
63
  end
61
64
 
65
+ def rebuild_fts(db_path)
66
+ store = ClaudeMemory::Store::SQLiteStore.new(db_path)
67
+ fts = ClaudeMemory::Index::LexicalFTS.new(store)
68
+ fts.rebuild!
69
+ store.close
70
+ end
71
+
62
72
  def run_vacuum(db_path)
63
73
  store = ClaudeMemory::Store::SQLiteStore.new(db_path)
64
74
  store.db.run("VACUUM")
@@ -66,8 +66,11 @@ module ClaudeMemory
66
66
  end
67
67
 
68
68
  def collect_from_store(store, source_label, status_filter, export)
69
- # Collect entities
70
- store.entities.each do |entity|
69
+ # Collect entities (batch load all for lookup)
70
+ all_entities = store.entities.all
71
+ entities_by_id = all_entities.each_with_object({}) { |e, h| h[e[:id]] = e }
72
+
73
+ all_entities.each do |entity|
71
74
  export[:entities] << {
72
75
  id: entity[:id],
73
76
  type: entity[:type],
@@ -76,13 +79,18 @@ module ClaudeMemory
76
79
  }
77
80
  end
78
81
 
79
- # Collect facts with provenance
82
+ # Collect facts with provenance (batch load to avoid N+1)
80
83
  facts_ds = store.facts
81
84
  facts_ds = facts_ds.where(status: "active") if status_filter == "active"
85
+ all_facts = facts_ds.all
86
+
87
+ fact_ids = all_facts.map { |f| f[:id] }
88
+ provenance_by_fact = store.provenance.where(fact_id: fact_ids).all
89
+ .group_by { |p| p[:fact_id] }
82
90
 
83
- facts_ds.each do |fact|
84
- subject = store.entities.where(id: fact[:subject_entity_id]).first
85
- receipts = store.provenance.where(fact_id: fact[:id]).all
91
+ all_facts.each do |fact|
92
+ subject = entities_by_id[fact[:subject_entity_id]]
93
+ receipts = provenance_by_fact[fact[:id]] || []
86
94
 
87
95
  export[:facts] << {
88
96
  id: fact[:id],
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClaudeMemory
4
+ module Commands
5
+ # Sets up git-lfs tracking for the project memory database.
6
+ # This allows committing .claude/memory.sqlite3 to a git repository
7
+ # without bloating the repo, using Git Large File Storage.
8
+ class GitLfsCommand < BaseCommand
9
+ TRACKED_PATTERN = ".claude/memory.sqlite3"
10
+
11
+ def call(args)
12
+ opts = parse_options(args, {compact: true}) do |o|
13
+ OptionParser.new do |parser|
14
+ parser.banner = "Usage: claude-memory git-lfs [options]"
15
+ parser.on("--no-compact", "Skip compacting before setup") { o[:compact] = false }
16
+ end
17
+ end
18
+ return 1 if opts.nil?
19
+
20
+ return failure("Not a git repository. Run this from a project root.") unless git_repo?
21
+ return failure("git-lfs is not installed. Install it first: https://git-lfs.com") unless git_lfs_installed?
22
+
23
+ if already_tracked?
24
+ stdout.puts "git-lfs is already tracking #{TRACKED_PATTERN}"
25
+ return 0
26
+ end
27
+
28
+ if opts[:compact]
29
+ stdout.puts "Compacting project database before setup..."
30
+ compact_project_db
31
+ end
32
+
33
+ setup_git_lfs
34
+ 0
35
+ end
36
+
37
+ private
38
+
39
+ def git_repo?
40
+ system("git", "rev-parse", "--git-dir", out: File::NULL, err: File::NULL)
41
+ end
42
+
43
+ def git_lfs_installed?
44
+ system("git", "lfs", "version", out: File::NULL, err: File::NULL)
45
+ end
46
+
47
+ def already_tracked?
48
+ return false unless File.exist?(".gitattributes")
49
+
50
+ File.read(".gitattributes").include?(TRACKED_PATTERN)
51
+ end
52
+
53
+ def compact_project_db
54
+ db_path = ClaudeMemory::Store::StoreManager.new.project_db_path
55
+ if File.exist?(db_path)
56
+ CompactCommand.new(stdout: stdout, stderr: stderr).call(["--scope", "project"])
57
+ else
58
+ stdout.puts "No project database found, skipping compact."
59
+ end
60
+ end
61
+
62
+ def setup_git_lfs
63
+ # Initialize git-lfs in the repo
64
+ run_cmd("git", "lfs", "install", "--local")
65
+
66
+ # Track the sqlite3 file (adds to .gitattributes)
67
+ run_cmd("git", "lfs", "track", TRACKED_PATTERN)
68
+
69
+ # Also track WAL/SHM files in case they exist at commit time
70
+ run_cmd("git", "lfs", "track", "#{TRACKED_PATTERN}-shm")
71
+ run_cmd("git", "lfs", "track", "#{TRACKED_PATTERN}-wal")
72
+
73
+ # Update .gitignore: remove the project memory.sqlite3 entries
74
+ update_gitignore
75
+
76
+ stdout.puts ""
77
+ stdout.puts "git-lfs setup complete!"
78
+ stdout.puts ""
79
+ stdout.puts "Files tracked via LFS:"
80
+ stdout.puts " #{TRACKED_PATTERN}"
81
+ stdout.puts " #{TRACKED_PATTERN}-shm"
82
+ stdout.puts " #{TRACKED_PATTERN}-wal"
83
+ stdout.puts ""
84
+ stdout.puts "Next steps:"
85
+ stdout.puts " 1. git add .gitattributes .gitignore"
86
+ stdout.puts " 2. git add .claude/memory.sqlite3"
87
+ stdout.puts " 3. git commit -m 'Add project memory via git-lfs'"
88
+ end
89
+
90
+ def update_gitignore
91
+ gitignore_path = ".gitignore"
92
+ return unless File.exist?(gitignore_path)
93
+
94
+ lines = File.readlines(gitignore_path)
95
+ # Remove lines that ignore the project memory sqlite3 files
96
+ patterns_to_remove = [
97
+ ".claude/memory.sqlite3\n",
98
+ ".claude/memory.sqlite3-shm\n",
99
+ ".claude/memory.sqlite3-wal\n"
100
+ ]
101
+
102
+ new_lines = lines.reject { |line| patterns_to_remove.include?(line) }
103
+
104
+ if new_lines.length < lines.length
105
+ File.write(gitignore_path, new_lines.join)
106
+ stdout.puts "Updated .gitignore: removed project memory exclusions"
107
+ end
108
+ end
109
+
110
+ def run_cmd(*cmd)
111
+ unless system(*cmd, out: File::NULL, err: File::NULL)
112
+ stderr.puts "Command failed: #{cmd.join(" ")}"
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
@@ -29,7 +29,8 @@ module ClaudeMemory
29
29
  "index" => "IndexCommand",
30
30
  "recover" => "RecoverCommand",
31
31
  "compact" => "CompactCommand",
32
- "export" => "ExportCommand"
32
+ "export" => "ExportCommand",
33
+ "git-lfs" => "GitLfsCommand"
33
34
  }.freeze
34
35
 
35
36
  # Find a command class by name