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.
- checksums.yaml +4 -4
- data/.claude/CLAUDE.md +1 -1
- data/.claude/memory.sqlite3 +0 -0
- data/.claude/memory.sqlite3-shm +0 -0
- data/.claude/memory.sqlite3-wal +0 -0
- data/.claude/settings.local.json +11 -1
- data/.claude-plugin/marketplace.json +1 -1
- data/.claude-plugin/plugin.json +1 -1
- data/.gitattributes +1 -0
- data/CHANGELOG.md +36 -0
- data/CLAUDE.md +1 -1
- data/README.md +1 -1
- data/docs/improvements.md +166 -22
- data/docs/influence/qmd.md +201 -130
- data/docs/quality_review.md +344 -56
- data/lib/claude_memory/commands/checks/database_check.rb +7 -0
- data/lib/claude_memory/commands/compact_command.rb +10 -0
- data/lib/claude_memory/commands/export_command.rb +14 -6
- data/lib/claude_memory/commands/git_lfs_command.rb +117 -0
- data/lib/claude_memory/commands/registry.rb +2 -1
- data/lib/claude_memory/commands/serve_mcp_command.rb +10 -1
- data/lib/claude_memory/commands/stats_command.rb +12 -1
- data/lib/claude_memory/configuration.rb +40 -1
- data/lib/claude_memory/core/snippet_extractor.rb +21 -19
- data/lib/claude_memory/index/lexical_fts.rb +88 -16
- data/lib/claude_memory/ingest/ingester.rb +1 -1
- data/lib/claude_memory/mcp/tool_definitions.rb +51 -21
- data/lib/claude_memory/mcp/tools.rb +13 -1
- data/lib/claude_memory/resolve/resolver.rb +22 -18
- data/lib/claude_memory/store/store_manager.rb +19 -24
- data/lib/claude_memory/sweep/sweeper.rb +11 -2
- data/lib/claude_memory/version.rb +1 -1
- data/lib/claude_memory.rb +7 -0
- metadata +6 -1
data/docs/quality_review.md
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
# Code Quality Review - Ruby Best Practices
|
|
2
2
|
|
|
3
|
-
**Review Date:** 2026-
|
|
4
|
-
**Previous Review:** 2026-
|
|
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
|
|
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
|
-
**
|
|
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 (
|
|
22
|
-
- 1.
|
|
23
|
-
- Zero bare rescues, zero N+1
|
|
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
|
-
##
|
|
30
|
+
## 1. Sandi Metz Perspective
|
|
28
31
|
|
|
29
|
-
###
|
|
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
|
-
|
|
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
|
-
|
|
|
182
|
+
| 10 | **Sleep-based tests add 4+ seconds** | `spec/ingest/ingester_spec.rb:43,65,81` | 1 hour |
|
|
34
183
|
|
|
35
|
-
|
|
184
|
+
Three `sleep 1.01` calls wait for filesystem mtime changes. `publish_spec.rb:189` has `sleep 1.1`.
|
|
36
185
|
|
|
37
|
-
|
|
186
|
+
**Fix:** Mock `File.mtime` or inject a time provider instead of real sleeps.
|
|
38
187
|
|
|
39
|
-
| # | Issue | File:Line |
|
|
188
|
+
| # | Issue | File:Line | Effort |
|
|
40
189
|
|---|-------|-----------|--------|
|
|
41
|
-
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
|
72
|
-
|
|
73
|
-
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
256
|
+
Tests use `Dir.chdir(test_dir)` which modifies global state. Fragile if tests ever run in parallel.
|
|
79
257
|
|
|
80
|
-
|
|
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
|
-
##
|
|
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
|
-
<
|
|
91
|
-
<summary>21 items completed in 7 atomic commits</summary>
|
|
365
|
+
These can be done immediately (< 30 min total):
|
|
92
366
|
|
|
93
|
-
**
|
|
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
|
-
|
|
371
|
+
## Appendix C: File Size Report
|
|
96
372
|
|
|
97
|
-
|
|
98
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
84
|
-
subject =
|
|
85
|
-
receipts =
|
|
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
|