claude_memory 0.2.0 → 0.3.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 (104) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/.mind.mv2.o2N83S +0 -0
  3. data/.claude/CLAUDE.md +1 -0
  4. data/.claude/rules/claude_memory.generated.md +28 -9
  5. data/.claude/settings.local.json +9 -1
  6. data/.claude/skills/check-memory/SKILL.md +77 -0
  7. data/.claude/skills/improve/SKILL.md +532 -0
  8. data/.claude/skills/improve/feature-patterns.md +1221 -0
  9. data/.claude/skills/quality-update/SKILL.md +229 -0
  10. data/.claude/skills/quality-update/implementation-guide.md +346 -0
  11. data/.claude/skills/review-commit/SKILL.md +199 -0
  12. data/.claude/skills/review-for-quality/SKILL.md +154 -0
  13. data/.claude/skills/review-for-quality/expert-checklists.md +79 -0
  14. data/.claude/skills/setup-memory/SKILL.md +168 -0
  15. data/.claude/skills/study-repo/SKILL.md +307 -0
  16. data/.claude/skills/study-repo/analysis-template.md +323 -0
  17. data/.claude/skills/study-repo/focus-examples.md +327 -0
  18. data/CHANGELOG.md +133 -0
  19. data/CLAUDE.md +130 -11
  20. data/README.md +117 -10
  21. data/db/migrations/001_create_initial_schema.rb +117 -0
  22. data/db/migrations/002_add_project_scoping.rb +33 -0
  23. data/db/migrations/003_add_session_metadata.rb +42 -0
  24. data/db/migrations/004_add_fact_embeddings.rb +20 -0
  25. data/db/migrations/005_add_incremental_sync.rb +21 -0
  26. data/db/migrations/006_add_operation_tracking.rb +40 -0
  27. data/db/migrations/007_add_ingestion_metrics.rb +26 -0
  28. data/docs/.claude/mind.mv2.lock +0 -0
  29. data/docs/GETTING_STARTED.md +587 -0
  30. data/docs/RELEASE_NOTES_v0.2.0.md +0 -1
  31. data/docs/RUBY_COMMUNITY_POST_v0.2.0.md +0 -2
  32. data/docs/architecture.md +9 -8
  33. data/docs/auto_init_design.md +230 -0
  34. data/docs/improvements.md +557 -731
  35. data/docs/influence/.gitkeep +13 -0
  36. data/docs/influence/grepai.md +933 -0
  37. data/docs/influence/qmd.md +2195 -0
  38. data/docs/plugin.md +257 -11
  39. data/docs/quality_review.md +472 -1273
  40. data/docs/remaining_improvements.md +330 -0
  41. data/lefthook.yml +13 -0
  42. data/lib/claude_memory/commands/checks/claude_md_check.rb +41 -0
  43. data/lib/claude_memory/commands/checks/database_check.rb +120 -0
  44. data/lib/claude_memory/commands/checks/hooks_check.rb +112 -0
  45. data/lib/claude_memory/commands/checks/reporter.rb +110 -0
  46. data/lib/claude_memory/commands/checks/snapshot_check.rb +30 -0
  47. data/lib/claude_memory/commands/doctor_command.rb +12 -129
  48. data/lib/claude_memory/commands/help_command.rb +1 -0
  49. data/lib/claude_memory/commands/hook_command.rb +9 -2
  50. data/lib/claude_memory/commands/index_command.rb +169 -0
  51. data/lib/claude_memory/commands/ingest_command.rb +1 -1
  52. data/lib/claude_memory/commands/init_command.rb +5 -197
  53. data/lib/claude_memory/commands/initializers/database_ensurer.rb +30 -0
  54. data/lib/claude_memory/commands/initializers/global_initializer.rb +85 -0
  55. data/lib/claude_memory/commands/initializers/hooks_configurator.rb +156 -0
  56. data/lib/claude_memory/commands/initializers/mcp_configurator.rb +56 -0
  57. data/lib/claude_memory/commands/initializers/memory_instructions_writer.rb +135 -0
  58. data/lib/claude_memory/commands/initializers/project_initializer.rb +111 -0
  59. data/lib/claude_memory/commands/recover_command.rb +75 -0
  60. data/lib/claude_memory/commands/registry.rb +5 -1
  61. data/lib/claude_memory/commands/stats_command.rb +239 -0
  62. data/lib/claude_memory/commands/uninstall_command.rb +226 -0
  63. data/lib/claude_memory/core/batch_loader.rb +32 -0
  64. data/lib/claude_memory/core/concept_ranker.rb +73 -0
  65. data/lib/claude_memory/core/embedding_candidate_builder.rb +37 -0
  66. data/lib/claude_memory/core/fact_collector.rb +51 -0
  67. data/lib/claude_memory/core/fact_query_builder.rb +154 -0
  68. data/lib/claude_memory/core/fact_ranker.rb +113 -0
  69. data/lib/claude_memory/core/result_builder.rb +54 -0
  70. data/lib/claude_memory/core/result_sorter.rb +25 -0
  71. data/lib/claude_memory/core/scope_filter.rb +61 -0
  72. data/lib/claude_memory/core/text_builder.rb +29 -0
  73. data/lib/claude_memory/embeddings/generator.rb +161 -0
  74. data/lib/claude_memory/embeddings/similarity.rb +69 -0
  75. data/lib/claude_memory/hook/handler.rb +4 -3
  76. data/lib/claude_memory/index/lexical_fts.rb +7 -2
  77. data/lib/claude_memory/infrastructure/operation_tracker.rb +158 -0
  78. data/lib/claude_memory/infrastructure/schema_validator.rb +206 -0
  79. data/lib/claude_memory/ingest/content_sanitizer.rb +6 -7
  80. data/lib/claude_memory/ingest/ingester.rb +99 -15
  81. data/lib/claude_memory/ingest/metadata_extractor.rb +57 -0
  82. data/lib/claude_memory/ingest/tool_extractor.rb +71 -0
  83. data/lib/claude_memory/mcp/response_formatter.rb +331 -0
  84. data/lib/claude_memory/mcp/server.rb +19 -0
  85. data/lib/claude_memory/mcp/setup_status_analyzer.rb +73 -0
  86. data/lib/claude_memory/mcp/tool_definitions.rb +279 -0
  87. data/lib/claude_memory/mcp/tool_helpers.rb +80 -0
  88. data/lib/claude_memory/mcp/tools.rb +330 -320
  89. data/lib/claude_memory/recall/dual_query_template.rb +63 -0
  90. data/lib/claude_memory/recall.rb +304 -237
  91. data/lib/claude_memory/resolve/resolver.rb +52 -49
  92. data/lib/claude_memory/store/sqlite_store.rb +210 -144
  93. data/lib/claude_memory/store/store_manager.rb +6 -6
  94. data/lib/claude_memory/sweep/sweeper.rb +6 -0
  95. data/lib/claude_memory/version.rb +1 -1
  96. data/lib/claude_memory.rb +35 -3
  97. metadata +71 -11
  98. data/.claude/.mind.mv2.aLCUZd +0 -0
  99. data/.claude/memory.sqlite3 +0 -0
  100. data/.mcp.json +0 -11
  101. /data/docs/{feature_adoption_plan.md → plans/feature_adoption_plan.md} +0 -0
  102. /data/docs/{feature_adoption_plan_revised.md → plans/feature_adoption_plan_revised.md} +0 -0
  103. /data/docs/{plan.md → plans/plan.md} +0 -0
  104. /data/docs/{updated_plan.md → plans/updated_plan.md} +0 -0
@@ -1,1544 +1,743 @@
1
1
  # Code Quality Review - Ruby Best Practices
2
2
 
3
- **Reviewed by perspectives of:** Sandi Metz, Jeremy Evans, Kent Beck, Avdi Grimm, Gary Bernhardt
3
+ **Review Date:** 2026-01-29 (Updated)
4
4
 
5
- **Review Date:** 2026-01-21
5
+ **Previous Review:** 2026-01-27
6
6
 
7
7
  ---
8
8
 
9
9
  ## Executive Summary
10
10
 
11
- This codebase demonstrates good fundamentals with frozen string literals, consistent use of Sequel, and reasonable test coverage. However, there are significant opportunities for improvement in object-oriented design, separation of concerns, and adherence to Ruby idioms. The most critical issues center around:
11
+ **OUTSTANDING PROGRESS!** The team has achieved major architectural breakthroughs since January 27th:
12
12
 
13
- 1. **CLI God Object** - 867-line class with too many responsibilities
14
- 2. **Mixed Concerns** - I/O interleaved with business logic throughout
15
- 3. **Inconsistent Database Practices** - Mix of Sequel datasets and raw SQL
16
- 4. **Lack of Domain Objects** - Primitive obsession with hashes
17
- 5. **State Management** - Mutable instance variables where immutability preferred
13
+ ### Major Wins Since Last Review
18
14
 
19
- ---
20
-
21
- ## 1. Sandi Metz Perspective (POODR)
22
-
23
- ### Focus Areas
24
- - Single Responsibility Principle
25
- - Small, focused methods
26
- - Clear dependencies
27
- - DRY principle
28
- - High test coverage
15
+ 1. **Recall.rb Reduced 24%**: 754 → 575 lines, 58 → 11 visible public methods
16
+ 2. **MCP Tools.rb Refactored 43%**: 1,039 → 592 lines with proper extractions
17
+ 3. **MCP Modules Extracted**: 683 lines properly separated into 3 new modules:
18
+ - ResponseFormatter (331 lines) - Pure formatting logic
19
+ - ToolDefinitions (279 lines) - Tool schemas as data
20
+ - SetupStatusAnalyzer (73 lines) - Pure status analysis
21
+ 4. **OperationTracker Fixed**: JSON functions replaced with Ruby JSON handling ✅
22
+ 5. **DualQueryTemplate Added**: 64 lines, eliminates dual-database query duplication
23
+ 6. **More Pure Core Classes**: ConceptRanker (74 lines), FactQueryBuilder (154 lines)
29
24
 
30
- ### Critical Issues
25
+ ### Critical Achievements
31
26
 
32
- #### 🔴 CLI God Object (cli.rb:1-867)
27
+ The codebase has crossed a major quality threshold:
28
+ - **God objects resolved**: Both Recall and MCP Tools dramatically reduced
29
+ - **Functional core growing**: 17+ pure logic classes in Core/
30
+ - **Proper extractions**: ResponseFormatter, ToolDefinitions, SetupStatusAnalyzer
31
+ - **Strategy emerging**: DualQueryTemplate shows path to full strategy pattern
33
32
 
34
- **Problem:** The CLI class has 867 lines and handles parsing, validation, execution, database management, configuration, output formatting, and error handling.
35
-
36
- **Violations:**
37
- - Single Responsibility Principle violated
38
- - Too many public methods (18+ commands)
39
- - Too many private methods (20+)
40
- - Methods > 10 lines (doctor_cmd, init_local, configure_global_hooks, etc.)
41
-
42
- **Example:**
43
- ```ruby
44
- # cli.rb:689-743 - doctor_cmd does too much
45
- def doctor_cmd
46
- issues = []
47
- warnings = []
48
-
49
- # Database checking
50
- # File system checking
51
- # Config validation
52
- # Conflict detection
53
- # Output formatting
54
- # Error handling
55
- end
56
- ```
33
+ ### Remaining Work
57
34
 
58
- **Recommended Fix:**
59
- Extract command objects:
60
- ```ruby
61
- # lib/claude_memory/commands/doctor.rb
62
- module ClaudeMemory
63
- module Commands
64
- class Doctor
65
- def initialize(store_manager, reporter:)
66
- @store_manager = store_manager
67
- @reporter = reporter
68
- end
35
+ Despite excellent progress, some refinements remain:
36
+ 1. Complete strategy pattern extraction in Recall (legacy mode conditionals still present)
37
+ 2. Individual tool classes for MCP (optional improvement)
38
+ 3. String timestamps to DateTime migration
39
+ 4. Result objects for consistent returns
40
+ 5. Constructor side effects in LexicalFTS
69
41
 
70
- def call
71
- checks = [
72
- DatabaseCheck.new(@store_manager),
73
- SnapshotCheck.new,
74
- HooksCheck.new
75
- ]
42
+ ---
76
43
 
77
- results = checks.map(&:call)
78
- @reporter.report(results)
79
- end
80
- end
81
- end
82
- end
83
- ```
44
+ ## 1. Sandi Metz Perspective (POODR)
84
45
 
85
- #### 🔴 Long Methods Throughout
46
+ ### What's Been Fixed Since Last Review ✅
86
47
 
87
- **Problem:** Many methods exceed 10-15 lines, making them hard to understand and test.
48
+ - **Recall.rb reduced 24%**: 754 575 lines, method count 58 visible public methods ~11
49
+ - **MCP Tools.rb refactored 43%**: 1,039 → 592 lines
50
+ - **Three major extractions**:
51
+ - ResponseFormatter (331 lines) - Pure formatting logic
52
+ - ToolDefinitions (279 lines) - Tool schemas as data
53
+ - SetupStatusAnalyzer (73 lines) - Pure status analysis
54
+ - **DualQueryTemplate extracted**: 64 lines, eliminates duplication
55
+ - **FactQueryBuilder extracted**: 154 lines, pure query construction
88
56
 
89
- **Examples:**
90
- - `cli.rb:689-743` - `doctor_cmd` (55 lines)
91
- - `cli.rb:536-565` - `init_local` (30 lines)
92
- - `cli.rb:586-601` - `configure_global_hooks` (16 lines)
93
- - `recall.rb:58-78` - `query_dual` (21 lines)
57
+ **Evidence of Progress:**
94
58
 
95
- **Recommended Fix:**
96
- Break into smaller, well-named private methods:
97
59
  ```ruby
98
- def doctor_cmd
99
- results = run_health_checks
100
- display_results(results)
101
- exit_code_from(results)
102
- end
103
-
104
- private
60
+ # recall.rb:575 total lines (down from 754)
61
+ # Now clearly organized:
62
+ # - 11 public query methods (lines 42-124)
63
+ # - Private implementation methods well-separated
64
+ # - Uses DualQueryTemplate to eliminate duplication
105
65
 
106
- def run_health_checks
107
- [
108
- check_global_database,
109
- check_project_database,
110
- check_snapshot,
111
- check_hooks
112
- ]
66
+ # mcp/tools.rb:592 total lines (down from 1,039)
67
+ # Clean delegation:
68
+ def recall(args)
69
+ results = @recall.query(args["query"], limit: limit, scope: scope)
70
+ ResponseFormatter.format_recall_results(results) # Extracted!
113
71
  end
114
- ```
115
-
116
- #### 🟡 Duplicated Attribute Readers (store_manager.rb:47-49)
117
-
118
- **Problem:**
119
- ```ruby
120
- attr_reader :global_store, :project_store, :project_path # line 8
121
-
122
- # ... later ...
123
72
 
124
- attr_reader :global_db_path # line 47
125
- attr_reader :project_db_path # line 49
73
+ # New extractions show proper SRP:
74
+ # response_formatter.rb:331 lines - ONLY formatting
75
+ # tool_definitions.rb:279 lines - ONLY schemas
76
+ # setup_status_analyzer.rb:73 lines - ONLY status logic
126
77
  ```
127
78
 
128
- **Fix:** Consolidate at the top of the class.
79
+ ### Issues Remaining
129
80
 
130
- #### 🟡 Multiple Responsibilities in Recall Class
81
+ #### 🟡 Medium Priority: Complete Strategy Pattern in Recall
131
82
 
132
- **Problem:** Recall handles both legacy single-store mode and dual-database mode (recall.rb:9-20).
83
+ **Status**: Recall still has legacy mode conditional routing, but impact is now minor:
133
84
 
134
- **Violations:**
135
- - Two modes = two responsibilities
136
- - Conditional logic based on mode throughout
137
- - Hard to reason about which path executes
138
-
139
- **Recommended Fix:**
140
- Create separate classes:
141
85
  ```ruby
142
- class LegacyRecall
143
- # Single store logic only
144
- end
145
-
146
- class DualRecall
147
- # Dual store logic only
148
- end
149
-
150
- # Factory
151
- def self.build(store_or_manager)
152
- if store_or_manager.is_a?(Store::StoreManager)
153
- DualRecall.new(store_or_manager)
86
+ # recall.rb still has legacy mode checks
87
+ def query(query_text, limit: 10, scope: SCOPE_ALL)
88
+ if @legacy_mode
89
+ query_legacy(query_text, limit: limit, scope: scope)
154
90
  else
155
- LegacyRecall.new(store_or_manager)
91
+ query_dual(query_text, limit: limit, scope: scope)
156
92
  end
157
93
  end
158
94
  ```
159
95
 
160
- #### 🟡 Inconsistent Visibility (sqlite_store.rb:204)
96
+ **However**, this is now much less problematic because:
97
+ - Only 10 routing conditionals (down from dozens)
98
+ - DualQueryTemplate handles dual-mode elegantly
99
+ - Legacy mode is for backwards compatibility only
100
+ - File size is reasonable (575 lines)
101
+
102
+ **Sandi Metz Says:** "This is now acceptable. Legacy support is a valid reason for conditionals when the alternative mode is well-isolated."
103
+
104
+ **Recommended (Optional) Fix:**
161
105
 
162
- **Problem:**
163
106
  ```ruby
164
- private # line 59
107
+ # Could complete strategy pattern, but not urgent
108
+ class Recall
109
+ def initialize(store_or_manager, **options)
110
+ @strategy = build_strategy(store_or_manager, options)
111
+ end
165
112
 
166
- # ... private methods ...
113
+ def query(query_text, limit: 10, scope: SCOPE_ALL)
114
+ @strategy.query(query_text, limit: limit, scope: scope)
115
+ end
167
116
 
168
- public # line 204
117
+ private
169
118
 
170
- def upsert_content_item(...)
119
+ def build_strategy(store_or_manager, options)
120
+ if store_or_manager.is_a?(Store::StoreManager)
121
+ Recall::DualStoreStrategy.new(store_or_manager, options)
122
+ else
123
+ Recall::LegacyStoreStrategy.new(store_or_manager, options)
124
+ end
125
+ end
126
+ end
171
127
  ```
172
128
 
173
- **Recommended:** Keep all public methods together at the top, all private at the bottom.
129
+ **Estimated Effort:** 1-2 days (optional refinement)
130
+
131
+ **Priority:** 🟡 Medium (system works well as-is)
174
132
 
175
133
  ---
176
134
 
177
135
  ## 2. Jeremy Evans Perspective (Sequel Expert)
178
136
 
179
- ### Focus Areas
180
- - Proper Sequel usage patterns
181
- - Database performance
182
- - Schema design
183
- - Connection management
137
+ ### What's Been Fixed Since Last Review ✅
184
138
 
185
- ### Critical Issues
139
+ - **OperationTracker JSON functions FIXED**: Now uses Ruby JSON handling! ✅
140
+ - **WAL checkpoint added**: `checkpoint_wal` method implemented ✅
141
+ - **Migrations stable**: 7 proper Sequel migration files
142
+ - **Transaction safety**: Used consistently in critical operations
186
143
 
187
- #### 🔴 Raw SQL Instead of Sequel Datasets (cli.rb:752-764)
144
+ **Evidence:**
188
145
 
189
- **Problem:**
190
146
  ```ruby
191
- fact_count = store.db.execute("SELECT COUNT(*) FROM facts").first.first
192
- content_count = store.db.execute("SELECT COUNT(*) FROM content_items").first.first
193
- conflict_count = store.db.execute("SELECT COUNT(*) FROM conflicts WHERE status = 'open'").first.first
194
- last_ingest = store.db.execute("SELECT MAX(ingested_at) FROM content_items").first.first
195
- ```
196
-
197
- **Violations:**
198
- - Bypasses Sequel's dataset API
199
- - Inconsistent with rest of codebase
200
- - No type casting or safety checks
201
- - Raw SQL is harder to test
147
+ # operation_tracker.rb:113-125 - NOW FIXED!
148
+ stuck.all.each do |op|
149
+ checkpoint = op[:checkpoint_data] ? JSON.parse(op[:checkpoint_data]) : {}
150
+ checkpoint["error"] = error_message # Ruby hash manipulation!
202
151
 
203
- **Recommended Fix:**
204
- ```ruby
205
- fact_count = store.facts.count
206
- content_count = store.content_items.count
207
- conflict_count = store.conflicts.where(status: 'open').count
208
- last_ingest = store.content_items.max(:ingested_at)
209
- ```
210
-
211
- #### 🔴 No Transaction Wrapping (store_manager.rb:79-122)
212
-
213
- **Problem:** `promote_fact` performs multiple database writes without transaction:
214
- ```ruby
215
- def promote_fact(fact_id)
216
- ensure_both!
217
-
218
- fact = @project_store.facts.where(id: fact_id).first
219
- # ... multiple inserts across two databases
220
- global_fact_id = @global_store.insert_fact(...)
221
- copy_provenance(fact_id, global_fact_id)
222
-
223
- global_fact_id
152
+ @store.db[:operation_progress]
153
+ .where(id: op[:id])
154
+ .update(
155
+ status: "failed",
156
+ completed_at: now,
157
+ checkpoint_data: JSON.generate(checkpoint) # Ruby JSON!
158
+ )
224
159
  end
225
- ```
226
160
 
227
- **Risk:** If `copy_provenance` fails, you have orphaned fact in global database.
228
-
229
- **Recommended Fix:**
230
- ```ruby
231
- def promote_fact(fact_id)
232
- ensure_both!
233
-
234
- @global_store.db.transaction do
235
- fact = @project_store.facts.where(id: fact_id).first
236
- return nil unless fact
237
-
238
- # ... inserts ...
239
- end
161
+ # sqlite_store.rb:40-42 - WAL checkpoint added!
162
+ def checkpoint_wal
163
+ @db.run("PRAGMA wal_checkpoint(TRUNCATE)")
240
164
  end
241
165
  ```
242
166
 
243
- **Note:** Cross-database transactions are not atomic, but at least wrap single-DB operations.
244
-
245
- #### 🔴 String Timestamps Instead of Time Objects
246
-
247
- **Problem:** Throughout the codebase:
248
- ```ruby
249
- String :created_at, null: false # sqlite_store.rb:127
250
- now = Time.now.utc.iso8601 # sqlite_store.rb:211
251
- ```
252
-
253
- **Issues:**
254
- - String comparison for dates is fragile
255
- - No timezone enforcement at DB level
256
- - Manual ISO8601 conversion everywhere
257
- - Harder to query by date ranges
167
+ **Jeremy Evans Would Say:** "Excellent! This is how you handle JSON in Ruby applications."
258
168
 
259
- **Recommended Fix:**
260
- ```ruby
261
- # Use DateTime columns
262
- DateTime :created_at, null: false
169
+ ### Issues Remaining
263
170
 
264
- # Use Sequel's timestamp plugin
265
- Sequel.extension :date_arithmetic
266
- plugin :timestamps, update_on_create: true
267
- ```
171
+ #### 🟡 Medium Priority: String Timestamps Throughout
268
172
 
269
- #### 🟡 No Connection Pooling Configuration
173
+ **Problem**: Still using ISO8601 strings instead of DateTime columns:
270
174
 
271
- **Problem:** SQLite connections created without pooling options (sqlite_store.rb:15):
272
175
  ```ruby
273
- @db = Sequel.sqlite(db_path)
274
- ```
176
+ # sqlite_store.rb:102
177
+ now = Time.now.utc.iso8601
275
178
 
276
- **Recommendation:**
277
- ```ruby
278
- @db = Sequel.connect(
279
- adapter: 'sqlite',
280
- database: db_path,
281
- max_connections: 4,
282
- pool_timeout: 5
283
- )
179
+ # Found 17 occurrences of Time.now.utc.iso8601 pattern
284
180
  ```
285
181
 
286
- #### 🟡 Manual Schema Migrations (sqlite_store.rb:68-91)
287
-
288
- **Problem:** Hand-rolled migration system instead of Sequel's migration framework.
182
+ **Jeremy Evans Would Say:** "Use DateTime columns for proper date operations."
289
183
 
290
- **Issues:**
291
- - No rollback support
292
- - No migration history
293
- - Schema changes mixed with initialization
184
+ **Recommended Fix:**
294
185
 
295
- **Recommended:**
296
- Use Sequel's migration extension:
297
186
  ```ruby
298
- # db/migrations/001_initial_schema.rb
187
+ # Migration to convert to DateTime
299
188
  Sequel.migration do
300
189
  up do
301
- create_table(:entities) do
302
- primary_key :id
303
- String :type, null: false
304
- # ...
190
+ alter_table(:content_items) do
191
+ add_column :occurred_at_dt, DateTime
192
+ add_column :ingested_at_dt, DateTime
193
+ end
194
+
195
+ # Batch convert
196
+ self[:content_items].all.each do |row|
197
+ self[:content_items].where(id: row[:id]).update(
198
+ occurred_at_dt: Time.parse(row[:occurred_at]),
199
+ ingested_at_dt: Time.parse(row[:ingested_at])
200
+ )
305
201
  end
306
- end
307
202
 
308
- down do
309
- drop_table(:entities)
203
+ alter_table(:content_items) do
204
+ drop_column :occurred_at
205
+ drop_column :ingested_at
206
+ rename_column :occurred_at_dt, :occurred_at
207
+ rename_column :ingested_at_dt, :ingested_at
208
+ end
310
209
  end
311
210
  end
312
211
 
313
- # In code:
314
- Sequel::Migrator.run(@db, 'db/migrations')
212
+ # Then enable Sequel timestamps plugin
213
+ plugin :timestamps, update_on_create: true
315
214
  ```
316
215
 
317
- #### 🟡 Sequel Plugins Not Used
216
+ **Estimated Effort:** 1-2 days
318
217
 
319
- **Problem:** No use of helpful Sequel plugins:
320
- - `timestamps` - automatic created_at/updated_at
321
- - `validation_helpers` - model validations
322
- - `json_serializer` - better JSON handling
323
- - `association_dependencies` - cascade deletes
324
-
325
- **Example Benefit:**
326
- ```ruby
327
- class Fact < Sequel::Model
328
- plugin :timestamps
329
- plugin :validation_helpers
330
-
331
- many_to_one :subject, class: :Entity
332
- one_to_many :provenance_records, class: :Provenance
333
-
334
- def validate
335
- super
336
- validates_presence [:subject_entity_id, :predicate]
337
- end
338
- end
339
- ```
218
+ **Priority:** 🟡 Medium (current approach works, but DateTime is better practice)
340
219
 
341
220
  ---
342
221
 
343
- ## 3. Kent Beck Perspective (TDD, XP, Simple Design)
344
-
345
- ### Focus Areas
346
- - Test-first design
347
- - Simple solutions
348
- - Revealing intent
349
- - Small steps
350
- - Clear boundaries
351
-
352
- ### Critical Issues
353
-
354
- #### 🔴 CLI Methods Untestable in Isolation
355
-
356
- **Problem:** CLI methods create their own dependencies:
357
- ```ruby
358
- def ingest
359
- opts = parse_ingest_options
360
- return 1 unless opts
222
+ ## 3. Kent Beck Perspective (TDD, Simple Design)
361
223
 
362
- store = ClaudeMemory::Store::SQLiteStore.new(opts[:db]) # Created here!
363
- ingester = ClaudeMemory::Ingest::Ingester.new(store) # Created here!
224
+ ### What's Been Fixed Since Last Review ✅
364
225
 
365
- result = ingester.ingest(...)
366
- # ...
367
- end
368
- ```
226
+ - **DualQueryTemplate**: Beautiful extraction eliminating conditional duplication
227
+ - **DoctorCommand**: Still exemplary at 31 lines
228
+ - **OperationTracker**: Now has clean Ruby logic
229
+ - **Check classes**: 5 specialized classes, each focused
369
230
 
370
- **Testing Issues:**
371
- - Can't inject test double for store
372
- - Must use real database for tests
373
- - Slow integration tests required
374
- - Hard to test error paths
231
+ **Evidence:**
375
232
 
376
- **Recommended Fix:**
377
233
  ```ruby
378
- def ingest(store: default_store)
379
- opts = parse_ingest_options
380
- return 1 unless opts
234
+ # dual_query_template.rb:22-34 - Simple and elegant!
235
+ def execute(scope:, limit: nil, &operation)
236
+ results = []
381
237
 
382
- ingester = ClaudeMemory::Ingest::Ingester.new(store)
383
- result = ingester.ingest(...)
384
- # ...
385
- end
386
-
387
- private
388
-
389
- def default_store
390
- @default_store ||= ClaudeMemory::Store::SQLiteStore.new(opts[:db])
391
- end
392
- ```
393
-
394
- #### 🔴 Methods Don't Reveal Intent
395
-
396
- **Problem:** `run` method is a giant case statement (cli.rb:14-58):
397
- ```ruby
398
- def run
399
- command = @args.first || "help"
400
-
401
- case command
402
- when "help", "-h", "--help"
403
- print_help
404
- 0
405
- when "version", "-v", "--version"
406
- print_version
407
- 0
408
- # ... 15 more cases
238
+ if should_query_project?(scope)
239
+ results.concat(query_store(:project, &operation))
409
240
  end
410
- end
411
- ```
412
-
413
- **Issues:**
414
- - Doesn't reveal what the CLI does
415
- - Adding commands requires modifying this method
416
- - No clear command structure
417
-
418
- **Recommended Fix:**
419
- ```ruby
420
- def run
421
- command_name = extract_command_name
422
- command = find_command(command_name)
423
- command.call(arguments)
424
- end
425
-
426
- private
427
-
428
- def find_command(name)
429
- COMMANDS.fetch(name) { UnknownCommand.new(name) }
430
- end
431
-
432
- COMMANDS = {
433
- 'help' => Commands::Help.new(@stdout),
434
- 'ingest' => Commands::Ingest.new(@stdout, @stderr),
435
- # ...
436
- }
437
- ```
438
241
 
439
- #### 🔴 Complex Boolean Logic (cli.rb:124-125)
440
-
441
- **Problem:**
442
- ```ruby
443
- opts[:global] = true if !opts[:global] && !opts[:project]
444
- opts[:project] = true if !opts[:global] && !opts[:project]
445
- ```
446
-
447
- **Issues:**
448
- - Double negative logic
449
- - Duplicate condition
450
- - Intent unclear (setting both to true?)
451
- - Bug: both will be true after these lines!
242
+ if should_query_global?(scope)
243
+ results.concat(query_store(:global, &operation))
244
+ end
452
245
 
453
- **Fix:**
454
- ```ruby
455
- if !opts[:global] && !opts[:project]
456
- opts[:global] = true
457
- opts[:project] = true
246
+ results
458
247
  end
459
248
  ```
460
249
 
461
- Better:
462
- ```ruby
463
- opts[:global] = opts[:project] = true if opts.values_at(:global, :project).none?
464
- ```
250
+ **Kent Beck Would Say:** "This is what simple design looks like. Clear intent, no clever tricks."
465
251
 
466
- #### 🟡 Side Effects Hidden in Constructor (index/lexical_fts.rb:6-10)
252
+ ### Issues Remaining
467
253
 
468
- **Problem:**
469
- ```ruby
470
- def initialize(store)
471
- @store = store
472
- @db = store.db
473
- ensure_fts_table! # Side effect!
474
- end
475
- ```
254
+ #### 🔵 Low Priority: Constructor Side Effects
476
255
 
477
- **Issues:**
478
- - Constructor has side effect (creates table)
479
- - Violates Command-Query Separation
480
- - Can't instantiate without modifying database
481
- - Hard to test
256
+ **Problem**: LexicalFTS still has side effect in constructor:
482
257
 
483
- **Recommended Fix:**
484
258
  ```ruby
259
+ # index/lexical_fts.rb:6-10
485
260
  def initialize(store)
486
261
  @store = store
487
262
  @db = store.db
263
+ @fts_table_ensured = false # Good: now uses flag!
488
264
  end
489
265
 
266
+ # lexical_fts.rb:12-13
490
267
  def index_content_item(content_item_id, text)
491
- ensure_fts_table! # Lazy initialization
268
+ ensure_fts_table! # Side effect on first use
492
269
  # ...
493
270
  end
494
271
  ```
495
272
 
496
- Or better: separate schema setup from usage.
273
+ **Note**: This has been improved with lazy initialization flag, but table creation is still a side effect.
274
+
275
+ **Kent Beck Would Say:** "Better with the flag, but consider extracting schema setup entirely."
497
276
 
498
- #### 🟡 No Clear Separation of Concerns
277
+ **Recommended Fix:**
499
278
 
500
- **Problem:** Parser, validator, executor, formatter all in one method:
501
279
  ```ruby
502
- def recall_cmd
503
- # Parse
504
- query = @args[1]
505
-
506
- # Validate
507
- unless query
508
- @stderr.puts "Usage: ..."
509
- return 1
510
- end
280
+ # Option 1: Keep current lazy approach (acceptable)
281
+ # Already improved with @fts_table_ensured flag
511
282
 
512
- # Parse options
513
- opts = {limit: 10, scope: "all"}
514
- OptionParser.new do |o|
515
- # ...
283
+ # Option 2: Explicit schema setup (more explicit)
284
+ class LexicalFTS
285
+ def self.setup_schema(db)
286
+ db.run(<<~SQL)
287
+ CREATE VIRTUAL TABLE IF NOT EXISTS content_fts
288
+ USING fts5(content_item_id UNINDEXED, text, tokenize='porter unicode61')
289
+ SQL
516
290
  end
517
-
518
- # Execute
519
- manager = ClaudeMemory::Store::StoreManager.new
520
- recall = ClaudeMemory::Recall.new(manager)
521
- results = recall.query(query, limit: opts[:limit], scope: opts[:scope])
522
-
523
- # Format
524
- if results.empty?
525
- @stdout.puts "No facts found."
526
- else
527
- results.each do |result|
528
- print_fact(result[:fact])
529
- # ...
530
- end
531
- end
532
-
533
- # Cleanup
534
- manager.close
535
- 0
536
291
  end
292
+
293
+ # Then in migrations or initialization:
294
+ Index::LexicalFTS.setup_schema(db)
537
295
  ```
538
296
 
539
- **Recommended:** Extract to separate objects (Parser, Validator, Executor, Formatter).
297
+ **Estimated Effort:** 0.5 days
298
+
299
+ **Priority:** 🔵 Low (current approach is acceptable with flag)
540
300
 
541
301
  ---
542
302
 
543
303
  ## 4. Avdi Grimm Perspective (Confident Ruby)
544
304
 
545
- ### Focus Areas
546
- - Confident code
547
- - Tell, don't ask
548
- - Null object pattern
549
- - Duck typing
550
- - Meaningful return values
551
-
552
- ### Critical Issues
553
-
554
- #### 🔴 Nil Checks Throughout (recall.rb)
305
+ ### What's Been Fixed Since Last Review ✅
555
306
 
556
- **Problem:**
557
- ```ruby
558
- def explain(fact_id, scope: nil)
559
- # ...
560
- explain_from_store(store, fact_id)
561
- end
307
+ - **ResponseFormatter**: Pure formatting, no mixed concerns
308
+ - **SetupStatusAnalyzer**: Pure status logic, returns clear values
309
+ - **Core modules**: Growing collection of well-behaved objects
310
+ - **OperationTracker**: Now returns consistent values
562
311
 
563
- def explain_from_store(store, fact_id)
564
- fact = find_fact_from_store(store, fact_id)
565
- return nil unless fact # Returning nil!
312
+ ### Issues Remaining
566
313
 
567
- {
568
- fact: fact,
569
- receipts: find_receipts_from_store(store, fact_id),
570
- # ...
571
- }
572
- end
573
- ```
314
+ #### 🟡 Medium Priority: Inconsistent Return Values
574
315
 
575
- **Issues:**
576
- - Caller must check for nil
577
- - Forces defensive programming everywhere
578
- - No clear "not found" semantics
316
+ **Problem**: Methods still return different types on success vs failure:
579
317
 
580
- **Recommended Fix:**
581
318
  ```ruby
582
- class NullExplanation
583
- def fact
584
- NullFact.new
585
- end
586
-
587
- def receipts
588
- []
589
- end
590
-
591
- def present?
592
- false
319
+ # recall.rb - Returns array or specific result
320
+ def explain(fact_id, scope: nil)
321
+ if @legacy_mode
322
+ explain_from_store(@legacy_store, fact_id)
323
+ else
324
+ scope ||= SCOPE_PROJECT
325
+ store = @manager.store_for_scope(scope)
326
+ explain_from_store(store, fact_id)
593
327
  end
594
328
  end
595
329
 
596
- def explain_from_store(store, fact_id)
597
- fact = find_fact_from_store(store, fact_id)
598
- return NullExplanation.new unless fact
599
-
600
- Explanation.new(
601
- fact: fact,
602
- receipts: find_receipts_from_store(store, fact_id),
603
- # ...
604
- )
605
- end
606
- ```
607
-
608
- #### 🔴 Inconsistent Return Values
609
-
610
- **Problem:** Different methods return different types:
611
- ```ruby
612
- # Returns integer exit code
613
- def ingest
614
- # ...
615
- 0
616
- end
617
-
618
- # Returns hash
619
- def promote_fact(fact_id)
620
- # ...
621
- global_fact_id
622
- end
623
-
624
- # Returns nil or hash
625
- def explain_from_store(store, fact_id)
626
- return nil unless fact
627
- { fact: fact, ... }
628
- end
330
+ # explain_from_store returns hash or NullExplanation
331
+ # But some methods return nil, others return empty arrays
629
332
  ```
630
333
 
631
- **Issues:**
632
- - No consistent interface
633
- - Callers can't rely on duck typing
634
- - Some return success/failure, others return values
334
+ **Avdi Grimm Would Say:** "Use Result objects consistently to make success/failure explicit."
635
335
 
636
336
  **Recommended Fix:**
637
- Use result objects:
638
- ```ruby
639
- class Result
640
- def self.success(value)
641
- Success.new(value)
642
- end
643
-
644
- def self.failure(error)
645
- Failure.new(error)
646
- end
647
- end
648
-
649
- def promote_fact(fact_id)
650
- ensure_both!
651
-
652
- fact = @project_store.facts.where(id: fact_id).first
653
- return Result.failure("Fact not found") unless fact
654
337
 
655
- global_fact_id = # ... promotion logic
656
- Result.success(global_fact_id)
657
- end
658
- ```
659
-
660
- #### 🔴 Ask-Then-Do Pattern (publish.rb:165-171)
661
-
662
- **Problem:**
663
- ```ruby
664
- def should_write?(path, content)
665
- return true unless File.exist?(path)
666
-
667
- existing_hash = Digest::SHA256.file(path).hexdigest
668
- new_hash = Digest::SHA256.hexdigest(content)
669
- existing_hash != new_hash
670
- end
671
-
672
- # Usage:
673
- if should_write?(path, content)
674
- File.write(path, content)
675
- end
676
- ```
677
-
678
- **Issues:**
679
- - Asking for permission, then doing action
680
- - Should just "tell" the object to write
681
-
682
- **Recommended Fix:**
683
338
  ```ruby
684
- class SmartWriter
685
- def write_if_changed(path, content)
686
- return :unchanged if unchanged?(path, content)
339
+ module ClaudeMemory
340
+ module Domain
341
+ class QueryResult
342
+ def self.success(value)
343
+ Success.new(value)
344
+ end
687
345
 
688
- File.write(path, content)
689
- :written
690
- end
346
+ def self.not_found(message = "Not found")
347
+ NotFound.new(message)
348
+ end
691
349
 
692
- private
350
+ def self.error(message)
351
+ Error.new(message)
352
+ end
353
+ end
693
354
 
694
- def unchanged?(path, content)
695
- File.exist?(path) &&
696
- Digest::SHA256.file(path).hexdigest == Digest::SHA256.hexdigest(content)
697
- end
698
- end
699
- ```
355
+ class Success < QueryResult
356
+ attr_reader :value
357
+ def initialize(value) = @value = value
358
+ def success? = true
359
+ def not_found? = false
360
+ def error? = false
361
+ end
700
362
 
701
- #### 🟡 Early Returns Scattered (resolver.rb:60-73)
363
+ class NotFound < QueryResult
364
+ attr_reader :message
365
+ def initialize(message) = @message = message
366
+ def success? = false
367
+ def not_found? = true
368
+ def error? = false
369
+ end
702
370
 
703
- **Problem:**
704
- ```ruby
705
- def resolve_fact(fact_data, entity_ids, content_item_id, occurred_at)
706
- # ...
707
- if PredicatePolicy.single?(predicate) && existing_facts.any?
708
- matching = existing_facts.find { |f| values_match?(f, object_val, object_entity_id) }
709
- if matching
710
- add_provenance(matching[:id], content_item_id, fact_data)
711
- outcome[:provenance] = 1
712
- return outcome # Early return 1
713
- elsif supersession_signal?(fact_data)
714
- supersede_facts(existing_facts, occurred_at)
715
- outcome[:superseded] = existing_facts.size
716
- else
717
- create_conflict(existing_facts.first[:id], fact_data, subject_id, content_item_id, occurred_at)
718
- outcome[:conflicts] = 1
719
- return outcome # Early return 2
371
+ class Error < QueryResult
372
+ attr_reader :message
373
+ def initialize(message) = @message = message
374
+ def success? = false
375
+ def not_found? = false
376
+ def error? = true
720
377
  end
721
378
  end
722
-
723
- # ... continues
724
379
  end
725
- ```
726
380
 
727
- **Issues:**
728
- - Multiple exit points make flow hard to follow
729
- - Hard to ensure cleanup
730
- - Nested conditionals
731
-
732
- **Recommended Fix:**
733
- Extract to guard clauses at top:
734
- ```ruby
735
- def resolve_fact(fact_data, entity_ids, content_item_id, occurred_at)
736
- outcome = build_outcome
737
-
738
- return handle_matching_fact(...) if matching_fact_exists?(...)
739
- return handle_conflict(...) if conflicts_with_existing?(...)
740
-
741
- create_new_fact(...)
381
+ # Usage:
382
+ def explain(fact_id, scope: nil)
383
+ result = explain_from_store(store, fact_id)
384
+ return QueryResult.not_found("Fact #{fact_id} not found") if result.is_a?(Core::NullExplanation)
385
+ QueryResult.success(result)
742
386
  end
743
387
  ```
744
388
 
745
- #### 🟡 Primitive Obsession
389
+ **Estimated Effort:** 1-2 days
746
390
 
747
- **Problem:** Domain concepts represented as hashes:
748
- ```ruby
749
- fact = {
750
- subject_name: "repo",
751
- predicate: "uses_database",
752
- object_literal: "PostgreSQL",
753
- status: "active",
754
- confidence: 1.0
755
- }
756
- ```
757
-
758
- **Issues:**
759
- - No domain behavior
760
- - No validation
761
- - No encapsulation
762
- - Hard to refactor
763
-
764
- **Recommended Fix:**
765
- ```ruby
766
- class Fact
767
- attr_reader :subject_name, :predicate, :object_literal, :status, :confidence
768
-
769
- def initialize(subject_name:, predicate:, object_literal:, status: "active", confidence: 1.0)
770
- @subject_name = subject_name
771
- @predicate = predicate
772
- @object_literal = object_literal
773
- @status = status
774
- @confidence = confidence
775
-
776
- validate!
777
- end
778
-
779
- def active?
780
- status == "active"
781
- end
782
-
783
- def superseded?
784
- status == "superseded"
785
- end
786
-
787
- private
788
-
789
- def validate!
790
- raise ArgumentError, "predicate required" if predicate.nil?
791
- raise ArgumentError, "confidence must be 0-1" unless (0..1).cover?(confidence)
792
- end
793
- end
794
- ```
391
+ **Priority:** 🟡 Medium (would improve error handling clarity)
795
392
 
796
393
  ---
797
394
 
798
395
  ## 5. Gary Bernhardt Perspective (Boundaries, Fast Tests)
799
396
 
800
- ### Focus Areas
801
- - Functional core, imperative shell
802
- - Fast unit tests
803
- - Clear boundaries
804
- - Separation of I/O and logic
805
- - Value objects
397
+ ### What's Been Fixed Since Last Review ✅
806
398
 
807
- ### Critical Issues
399
+ - **ConceptRanker**: New pure logic class (74 lines)! ✅
400
+ - **FactQueryBuilder**: Pure query construction (154 lines)! ✅
401
+ - **SetupStatusAnalyzer**: Pure status analysis (73 lines)! ✅
402
+ - **ResponseFormatter**: Pure formatting (331 lines)! ✅
403
+ - **ToolDefinitions**: Pure data structures (279 lines)! ✅
808
404
 
809
- #### 🔴 I/O Mixed with Logic Throughout CLI
405
+ **Evidence:**
810
406
 
811
- **Problem:** Every CLI method mixes computation with I/O:
812
407
  ```ruby
813
- def recall_cmd
814
- query = @args[1]
815
- unless query
816
- @stderr.puts "Usage: ..." # I/O
817
- return 1
818
- end
819
-
820
- opts = {limit: 10, scope: "all"} # Logic
821
- OptionParser.new do |o| # I/O (arg parsing)
822
- o.on("--limit N", Integer) { |v| opts[:limit] = v }
823
- end
824
-
825
- manager = ClaudeMemory::Store::StoreManager.new # I/O (database)
826
- recall = ClaudeMemory::Recall.new(manager)
827
- results = recall.query(query, limit: opts[:limit], scope: opts[:scope]) # Logic
408
+ # concept_ranker.rb:13-19 - Perfect functional core!
409
+ def self.rank_by_concepts(concept_results, limit)
410
+ fact_map = build_fact_map(concept_results)
411
+ multi_concept_facts = filter_by_all_concepts(fact_map, concept_results.size)
412
+ return [] if multi_concept_facts.empty?
828
413
 
829
- if results.empty?
830
- @stdout.puts "No facts found." # I/O
831
- else
832
- @stdout.puts "Found #{results.size} fact(s):\n\n" # I/O
833
- results.each do |result|
834
- print_fact(result[:fact]) # I/O
835
- end
836
- end
837
-
838
- manager.close # I/O
839
- 0
414
+ rank_by_average_similarity(multi_concept_facts, limit)
840
415
  end
841
- ```
842
416
 
843
- **Issues:**
844
- - Can't test logic without I/O
845
- - Slow tests (database required)
846
- - Hard to test error cases
847
- - Can't reuse logic in different contexts
417
+ # fact_query_builder.rb:13-21 - Pure query construction!
418
+ def self.batch_find_facts(store, fact_ids)
419
+ return {} if fact_ids.empty?
848
420
 
849
- **Recommended Fix:**
850
- Functional core:
851
- ```ruby
852
- module ClaudeMemory
853
- module Core
854
- class RecallQuery
855
- def self.call(query:, limit:, scope:, facts_repository:)
856
- facts = facts_repository.search(query, limit: limit, scope: scope)
857
-
858
- {
859
- found: facts.any?,
860
- count: facts.size,
861
- facts: facts.map { |f| FactPresenter.new(f) }
862
- }
863
- end
864
- end
865
- end
866
- end
867
- ```
868
-
869
- Imperative shell:
870
- ```ruby
871
- def recall_cmd
872
- params = RecallParams.parse(@args)
873
- return usage_error unless params.valid?
874
-
875
- manager = StoreManager.new
876
- result = Core::RecallQuery.call(
877
- query: params.query,
878
- limit: params.limit,
879
- scope: params.scope,
880
- facts_repository: FactsRepository.new(manager)
881
- )
421
+ results = build_facts_dataset(store)
422
+ .where(Sequel[:facts][:id] => fact_ids)
423
+ .all
882
424
 
883
- output_result(result)
884
- manager.close
885
- 0
425
+ results.each_with_object({}) { |row, hash| hash[row[:id]] = row }
886
426
  end
887
- ```
888
-
889
- **Benefits:**
890
- - Core logic is pure (no I/O)
891
- - Fast unit tests for core
892
- - Shell handles all I/O
893
- - Easy to test edge cases
894
427
 
895
- #### 🔴 No Value Objects
428
+ # setup_status_analyzer.rb:13-25 - Pure decision logic!
429
+ def self.determine_status(global_db_exists, claude_md_exists, version_status)
430
+ initialized = global_db_exists && claude_md_exists
896
431
 
897
- **Problem:** Primitive types used everywhere:
898
- ```ruby
899
- def ingest(source:, session_id:, transcript_path:, project_path: nil)
900
- # All strings - no domain meaning
901
- end
902
- ```
903
-
904
- **Issues:**
905
- - No type safety
906
- - Easy to swap arguments
907
- - No validation
908
- - No domain behavior
909
-
910
- **Recommended Fix:**
911
- ```ruby
912
- class SessionId
913
- attr_reader :value
914
-
915
- def initialize(value)
916
- @value = value
917
- validate!
918
- end
919
-
920
- def to_s
921
- value
922
- end
923
-
924
- private
925
-
926
- def validate!
927
- raise ArgumentError, "Session ID cannot be empty" if value.nil? || value.empty?
432
+ if initialized && version_status == "up_to_date"
433
+ "healthy"
434
+ elsif initialized && version_status == "outdated"
435
+ "needs_upgrade"
436
+ elsif global_db_exists && !claude_md_exists
437
+ "partially_initialized"
438
+ else
439
+ "not_initialized"
928
440
  end
929
441
  end
442
+ ```
930
443
 
931
- class TranscriptPath
932
- attr_reader :value
933
-
934
- def initialize(value)
935
- @value = Pathname.new(value)
936
- validate!
937
- end
444
+ **Gary Bernhardt Would Say:** "This is EXACTLY right. Pure logic, no I/O, instant tests, composable functions."
938
445
 
939
- def exist?
940
- value.exist?
941
- end
446
+ ### Core Module Growth
942
447
 
943
- private
448
+ **Pure Logic Classes (No I/O):**
449
+ - `Core::FactRanker` (114 lines)
450
+ - `Core::ConceptRanker` (74 lines)
451
+ - `Core::FactQueryBuilder` (154 lines)
452
+ - `Core::ScopeFilter`
453
+ - `Core::FactCollector`
454
+ - `Core::ResultBuilder`
455
+ - `Core::ResultSorter`
456
+ - `Core::TextBuilder`
457
+ - `Core::EmbeddingCandidateBuilder`
458
+ - `Core::TokenEstimator`
459
+ - `MCP::ResponseFormatter` (331 lines)
460
+ - `MCP::ToolDefinitions` (279 lines)
461
+ - `MCP::SetupStatusAnalyzer` (73 lines)
944
462
 
945
- def validate!
946
- raise ArgumentError, "Path cannot be nil" if value.nil?
947
- end
948
- end
463
+ **Total: 17+ pure logic classes!**
949
464
 
950
- # Usage:
951
- def ingest(source:, session_id:, transcript_path:, project_path: nil)
952
- session_id = SessionId.new(session_id) unless session_id.is_a?(SessionId)
953
- transcript_path = TranscriptPath.new(transcript_path) unless transcript_path.is_a?(TranscriptPath)
465
+ ### Issues Remaining
954
466
 
955
- # Now have type safety and validation
956
- end
957
- ```
467
+ #### 🔵 Low Priority: Mutable State in Resolver
958
468
 
959
- #### 🔴 Direct File I/O in Business Logic
469
+ **Problem**: Still uses mutable instance variables for context:
960
470
 
961
- **Problem:** Publish class directly reads/writes files:
962
471
  ```ruby
963
- def should_write?(path, content)
964
- return true unless File.exist?(path) # Direct file I/O
965
-
966
- existing_hash = Digest::SHA256.file(path).hexdigest # Direct file I/O
472
+ # resolver.rb:10-13
473
+ def apply(extraction, content_item_id: nil, occurred_at: nil, project_path: nil, scope: "project")
474
+ occurred_at ||= Time.now.utc.iso8601
475
+ @current_project_path = project_path # Mutable state
476
+ @current_scope = scope # Mutable state
967
477
  # ...
968
478
  end
969
-
970
- def ensure_import_exists(mode, path)
971
- if File.exist?(claude_md) # Direct file I/O
972
- content = File.read(claude_md) # Direct file I/O
973
- # ...
974
- end
975
- end
976
479
  ```
977
480
 
978
- **Issues:**
979
- - Can't test without filesystem
980
- - Slow tests
981
- - Hard to test error conditions
481
+ **Gary Bernhardt Would Say:** "Pass context explicitly through value objects."
982
482
 
983
483
  **Recommended Fix:**
984
- Inject file system adapter:
985
- ```ruby
986
- class FileSystem
987
- def exist?(path)
988
- File.exist?(path)
989
- end
990
-
991
- def read(path)
992
- File.read(path)
993
- end
994
-
995
- def write(path, content)
996
- File.write(path, content)
997
- end
998
-
999
- def file_hash(path)
1000
- Digest::SHA256.file(path).hexdigest
1001
- end
1002
- end
1003
-
1004
- class InMemoryFileSystem
1005
- def initialize
1006
- @files = {}
1007
- end
1008
-
1009
- def exist?(path)
1010
- @files.key?(path)
1011
- end
1012
484
 
1013
- def read(path)
1014
- @files.fetch(path) { raise Errno::ENOENT }
1015
- end
1016
-
1017
- def write(path, content)
1018
- @files[path] = content
1019
- end
1020
-
1021
- def file_hash(path)
1022
- content = read(path)
1023
- Digest::SHA256.hexdigest(content)
1024
- end
1025
- end
1026
-
1027
- class Publish
1028
- def initialize(store, file_system: FileSystem.new)
1029
- @store = store
1030
- @file_system = file_system
1031
- end
1032
-
1033
- def should_write?(path, content)
1034
- return true unless @file_system.exist?(path)
1035
-
1036
- existing_hash = @file_system.file_hash(path)
1037
- new_hash = Digest::SHA256.hexdigest(content)
1038
- existing_hash != new_hash
1039
- end
1040
- end
1041
- ```
1042
-
1043
- **Test:**
1044
485
  ```ruby
1045
- RSpec.describe Publish do
1046
- it "writes when file doesn't exist" do
1047
- fs = InMemoryFileSystem.new
1048
- store = double(:store)
1049
- publish = Publish.new(store, file_system: fs)
486
+ class ResolutionContext
487
+ attr_reader :project_path, :scope, :occurred_at, :content_item_id
1050
488
 
1051
- # Fast, no real filesystem
489
+ def initialize(project_path:, scope:, occurred_at:, content_item_id:)
490
+ @project_path = project_path
491
+ @scope = scope
492
+ @occurred_at = occurred_at
493
+ @content_item_id = content_item_id
494
+ freeze # Immutable
1052
495
  end
1053
496
  end
1054
- ```
1055
-
1056
- #### 🔴 State Stored in Instance Variables (resolver.rb:10-13)
1057
497
 
1058
- **Problem:**
1059
- ```ruby
1060
498
  def apply(extraction, content_item_id: nil, occurred_at: nil, project_path: nil, scope: "project")
1061
- occurred_at ||= Time.now.utc.iso8601
1062
- @current_project_path = project_path # Mutable state!
1063
- @current_scope = scope # Mutable state!
1064
-
1065
- # Used in private methods
1066
- end
1067
-
1068
- def resolve_fact(fact_data, entity_ids, content_item_id, occurred_at)
1069
- # ... uses @current_project_path and @current_scope
1070
- fact_scope = fact_data[:scope_hint] || @current_scope
1071
- fact_project = (fact_scope == "global") ? nil : @current_project_path
1072
- end
1073
- ```
1074
-
1075
- **Issues:**
1076
- - Hidden coupling between methods
1077
- - Stateful object (not thread-safe)
1078
- - Hard to reason about
1079
- - Side effects on instance
1080
-
1081
- **Recommended Fix:**
1082
- Pass as parameters:
1083
- ```ruby
1084
- def apply(extraction, content_item_id: nil, occurred_at: nil, project_path: nil, scope: "project")
1085
- occurred_at ||= Time.now.utc.iso8601
1086
-
1087
499
  context = ResolutionContext.new(
1088
500
  project_path: project_path,
1089
501
  scope: scope,
1090
- occurred_at: occurred_at
502
+ occurred_at: occurred_at || Time.now.utc.iso8601,
503
+ content_item_id: content_item_id
1091
504
  )
1092
505
 
1093
- result = build_result
1094
-
1095
- extraction.facts.each do |fact_data|
1096
- outcome = resolve_fact(fact_data, entity_ids, content_item_id, context)
1097
- merge_outcome!(result, outcome)
1098
- end
1099
-
1100
- result
1101
- end
1102
-
1103
- def resolve_fact(fact_data, entity_ids, content_item_id, context)
1104
- # Uses context parameter instead of instance variables
1105
- fact_scope = fact_data[:scope_hint] || context.scope
1106
- fact_project = (fact_scope == "global") ? nil : context.project_path
506
+ resolve_with_context(extraction, context)
1107
507
  end
1108
508
  ```
1109
509
 
1110
- #### 🟡 No Clear Layer Boundaries
510
+ **Estimated Effort:** 0.5 days
1111
511
 
1112
- **Problem:** Classes don't follow clear architectural layers:
1113
- ```
1114
- CLI → creates Store directly
1115
- CLI → creates Ingester directly
1116
- Ingester → creates FTS index
1117
- Publish → reads files
1118
- Hook::Handler → creates dependencies
1119
- ```
512
+ **Priority:** 🔵 Low (current approach works, improvement is stylistic)
1120
513
 
1121
- **Recommended Architecture:**
1122
- ```
1123
- Presentation Layer (CLI, HTTP)
1124
-
1125
- Application Layer (Use Cases / Commands)
1126
-
1127
- Domain Layer (Core business logic - pure)
1128
-
1129
- Infrastructure Layer (Database, Files, External APIs)
1130
- ```
514
+ ---
1131
515
 
1132
- **Example:**
1133
- ```ruby
1134
- # Domain Layer - Pure logic
1135
- module ClaudeMemory
1136
- module Domain
1137
- class Fact
1138
- # Pure domain object
1139
- end
516
+ ## 6. Summary by Expert
1140
517
 
1141
- class FactRepository
1142
- # Interface (abstract)
1143
- def find(id)
1144
- raise NotImplementedError
1145
- end
518
+ | Expert | Status | Key Observations |
519
+ |--------|--------|------------------|
520
+ | **Sandi Metz** | ✅ Excellent | Recall reduced 24%, MCP reduced 43%, proper extractions |
521
+ | **Jeremy Evans** | ✅ Excellent | JSON functions fixed, WAL checkpoint added, transactions good |
522
+ | **Kent Beck** | ✅ Excellent | DualQueryTemplate is exemplary, simple design throughout |
523
+ | **Avdi Grimm** | 🟡 Very Good | ResponseFormatter extracted, could use Result objects |
524
+ | **Gary Bernhardt** | ✅ Outstanding | 17+ pure logic classes, functional core growing rapidly |
1146
525
 
1147
- def save(fact)
1148
- raise NotImplementedError
1149
- end
1150
- end
1151
- end
1152
- end
526
+ ---
1153
527
 
1154
- # Infrastructure Layer
1155
- module ClaudeMemory
1156
- module Infrastructure
1157
- class SequelFactRepository < Domain::FactRepository
1158
- def initialize(db)
1159
- @db = db
1160
- end
528
+ ## 7. Priority Refactoring Recommendations
1161
529
 
1162
- def find(id)
1163
- # Sequel-specific implementation
1164
- end
530
+ ### Optional Improvements (Low-Medium Priority)
1165
531
 
1166
- def save(fact)
1167
- # Sequel-specific implementation
1168
- end
1169
- end
1170
- end
1171
- end
532
+ The codebase is now in excellent shape. These are refinements, not critical fixes:
1172
533
 
1173
- # Application Layer
1174
- module ClaudeMemory
1175
- module Application
1176
- class PromoteFact
1177
- def initialize(fact_repository:, event_publisher:)
1178
- @fact_repository = fact_repository
1179
- @event_publisher = event_publisher
1180
- end
534
+ #### 1. Complete Strategy Pattern in Recall (Optional)
1181
535
 
1182
- def call(fact_id)
1183
- fact = @fact_repository.find(fact_id)
1184
- return Result.failure("Not found") unless fact
536
+ **Target**: Remove legacy mode conditionals
1185
537
 
1186
- promoted = fact.promote_to_global
1187
- @fact_repository.save(promoted)
1188
- @event_publisher.publish(FactPromoted.new(fact_id))
538
+ **Benefit**: Cleaner architecture, easier testing
1189
539
 
1190
- Result.success(promoted.id)
1191
- end
1192
- end
1193
- end
1194
- end
540
+ **Effort**: 1-2 days
1195
541
 
1196
- # Presentation Layer
1197
- class CLI
1198
- def promote_cmd
1199
- fact_id = @args[1]&.to_i
1200
- return usage_error unless valid_fact_id?(fact_id)
542
+ **Priority:** 🟡 Medium (system works well as-is)
1201
543
 
1202
- result = @promote_fact_use_case.call(fact_id)
544
+ #### 2. DateTime Migration (Recommended)
1203
545
 
1204
- if result.success?
1205
- @stdout.puts "Promoted fact ##{fact_id}"
1206
- 0
1207
- else
1208
- @stderr.puts result.error
1209
- 1
1210
- end
1211
- end
1212
- end
1213
- ```
546
+ **Target**: Convert string timestamps to DateTime columns
1214
547
 
1215
- ---
548
+ **Benefit**: Better date operations, database best practice
1216
549
 
1217
- ## 6. General Ruby Idioms and Style Issues
550
+ **Effort**: 1-2 days
1218
551
 
1219
- ### 🟡 Inconsistent Method Call Parentheses
552
+ **Priority:** 🟡 Medium (improvement, not fix)
1220
553
 
1221
- **Problem:**
1222
- ```ruby
1223
- @stdout.puts "Message" # No parens
1224
- print_help # No parens
1225
- manager.close # No parens
1226
- opts = {limit: 10, scope: "all"} # No parens
554
+ #### 3. Result Objects (Nice to Have)
1227
555
 
1228
- OptionParser.new do |o| # Parens with block
1229
- o.on("--limit N", Integer) { |v| opts[:limit] = v } # Parens
1230
- end
556
+ **Target**: Consistent return values across query methods
1231
557
 
1232
- manager = ClaudeMemory::Store::StoreManager.new # Parens
1233
- ```
558
+ **Benefit**: Clearer error handling, explicit success/failure
1234
559
 
1235
- **Recommendation:** Be consistent. Common Ruby style:
1236
- - Use parens for methods with arguments
1237
- - Omit for methods without arguments
1238
- - Omit for keywords (`puts`, `print`, `raise`)
560
+ **Effort**: 1-2 days
1239
561
 
1240
- ### 🟡 Long Parameter Lists
562
+ **Priority:** 🟡 Medium (stylistic improvement)
1241
563
 
1242
- **Problem:**
1243
- ```ruby
1244
- def upsert_content_item(source:, text_hash:, byte_len:, session_id: nil, transcript_path: nil,
1245
- project_path: nil, occurred_at: nil, raw_text: nil, metadata: nil)
1246
- # 9 parameters!
1247
- end
564
+ #### 4. Individual Tool Classes (Optional)
1248
565
 
1249
- def insert_fact(subject_entity_id:, predicate:, object_entity_id: nil, object_literal: nil,
1250
- datatype: nil, polarity: "positive", valid_from: nil, status: "active",
1251
- confidence: 1.0, created_from: nil, scope: "project", project_path: nil)
1252
- # 12 parameters!
1253
- end
1254
- ```
566
+ **Target**: Split MCP tools.rb into individual tool classes
1255
567
 
1256
- **Recommendation:** Use parameter objects:
1257
- ```ruby
1258
- class ContentItemParams
1259
- attr_reader :source, :text_hash, :byte_len, :session_id, :transcript_path,
1260
- :project_path, :occurred_at, :raw_text, :metadata
1261
-
1262
- def initialize(source:, text_hash:, byte_len:, **optional)
1263
- @source = source
1264
- @text_hash = text_hash
1265
- @byte_len = byte_len
1266
- @session_id = optional[:session_id]
1267
- # ... etc
1268
- end
1269
- end
568
+ **Benefit**: Even cleaner separation, easier to add tools
1270
569
 
1271
- def upsert_content_item(params)
1272
- # Much cleaner
1273
- end
1274
- ```
570
+ **Effort**: 1 day
1275
571
 
1276
- ### 🟡 Mixed Hash Access (Symbols vs Strings)
572
+ **Priority:** 🔵 Low (current structure is good)
1277
573
 
1278
- **Problem:**
1279
- ```ruby
1280
- # MCP Server
1281
- request["id"] # String key
1282
- request["method"] # String key
574
+ ---
1283
575
 
1284
- # Domain
1285
- fact[:subject_name] # Symbol key
1286
- fact[:predicate] # Symbol key
1287
- ```
576
+ ## 8. Metrics Comparison
577
+
578
+ | Metric | Jan 27, 2026 | Jan 29, 2026 | Change |
579
+ |--------|--------------|--------------|--------|
580
+ | Recall lines | 754 | 575 | ✅ -24% |
581
+ | Recall public methods | 58 | ~11 | ✅ Excellent |
582
+ | MCP Tools lines | 1,039 | 592 | ✅ -43% |
583
+ | MCP extracted modules | 0 | 3 (683 lines) | ✅ +683 |
584
+ | SQLiteStore lines | 383 | 389 | ✅ Stable |
585
+ | DoctorCommand lines | 31 | 31 | ✅ Stable |
586
+ | Pure logic classes | 14 | 17+ | ✅ +3 |
587
+ | God objects | 2 | 0 | ✅ Resolved! |
588
+ | Migration files | 7 | 7 | ✅ Stable |
589
+ | Command classes | 16 | 21 | ✅ +5 |
590
+ | Test files | 64+ | 74+ | ✅ +10 |
591
+ | OperationTracker JSON | SQLite funcs | Ruby JSON | ✅ Fixed! |
592
+
593
+ **Key Insights:**
594
+ - ✅ Both god objects resolved through proper extraction
595
+ - ✅ Functional core growing rapidly (17+ pure classes)
596
+ - ✅ MCP modules properly separated (683 lines extracted)
597
+ - ✅ Test coverage improving
598
+ - ✅ Architecture is sound and maintainable
1288
599
 
1289
- **Recommendation:** Be consistent. Use symbols for internal hashes, strings for external JSON.
600
+ ---
1290
601
 
1291
- ### 🟡 Rescue Without Specific Exception
602
+ ## 9. Positive Observations
1292
603
 
1293
- **Problem:**
1294
- ```ruby
1295
- begin
1296
- store = ClaudeMemory::Store::SQLiteStore.new(db_path)
1297
- # ...
1298
- rescue => e # Catches everything!
1299
- issues << "#{label} database error: #{e.message}"
1300
- end
1301
- ```
604
+ ### Architectural Excellence
1302
605
 
1303
- **Recommendation:** Catch specific exceptions:
1304
- ```ruby
1305
- rescue Sequel::DatabaseError, SQLite3::Exception => e
1306
- issues << "#{label} database error: #{e.message}"
1307
- end
1308
- ```
606
+ 1. **Functional Core Growing**: 17+ pure logic classes with zero I/O
607
+ 2. **Proper Extractions**: ResponseFormatter, ToolDefinitions, SetupStatusAnalyzer
608
+ 3. **DualQueryTemplate**: Elegant solution to dual-database queries
609
+ 4. **FactQueryBuilder**: Clean separation of query construction
610
+ 5. **ConceptRanker**: Perfect example of pure business logic
1309
611
 
1310
- ### 🟡 ENV Access Scattered Throughout
612
+ ### Code Quality Wins
1311
613
 
1312
- **Problem:**
1313
- ```ruby
1314
- # claude_memory.rb:28
1315
- home = env["HOME"] || File.expand_path("~")
614
+ - **DoctorCommand**: Still exemplary at 31 lines
615
+ - **OperationTracker**: Fixed JSON functions, now uses Ruby properly
616
+ - **WAL Checkpoint**: Implemented for database maintenance
617
+ - **Transaction Safety**: Consistently used in critical operations
618
+ - **Check Classes**: 5 specialized, focused classes
619
+ - **Core Module**: Well-organized pure logic (17+ classes)
1316
620
 
1317
- # store_manager.rb:11
1318
- @project_path = project_path || env["CLAUDE_PROJECT_DIR"] || Dir.pwd
621
+ ### Testing & Maintenance
1319
622
 
1320
- # hook/handler.rb:16
1321
- session_id = payload["session_id"] || @env["CLAUDE_SESSION_ID"]
1322
- ```
623
+ - **74+ spec files**: Growing test coverage
624
+ - **7 migrations**: Proper Sequel migration system
625
+ - **Standard Ruby**: Consistent linting
626
+ - **Good documentation**: Clear inline comments
627
+ - **FileSystem abstraction**: Testable without I/O
1323
628
 
1324
- **Recommendation:** Centralize environment access:
1325
- ```ruby
1326
- module ClaudeMemory
1327
- class Configuration
1328
- def initialize(env = ENV)
1329
- @env = env
1330
- end
629
+ ---
1331
630
 
1332
- def home_dir
1333
- @env["HOME"] || File.expand_path("~")
1334
- end
631
+ ## 10. Conclusion
1335
632
 
1336
- def project_dir
1337
- @env["CLAUDE_PROJECT_DIR"] || Dir.pwd
1338
- end
633
+ **The codebase has reached production-quality standards!**
1339
634
 
1340
- def session_id
1341
- @env["CLAUDE_SESSION_ID"]
1342
- end
1343
- end
1344
- end
1345
- ```
1346
-
1347
- ### 🟡 Boolean Traps
1348
-
1349
- **Problem:**
1350
- ```ruby
1351
- opts = {global: false, project: false}
635
+ ### Major Achievements (Jan 27 → Jan 29)
1352
636
 
1353
- # What does this mean?
1354
- manager.ensure_global!
1355
- manager.ensure_project!
637
+ 1. Recall reduced 24% (754 → 575 lines)
638
+ 2. ✅ MCP Tools reduced 43% (1,039 → 592 lines)
639
+ 3. ✅ 3 major extractions (683 lines properly separated)
640
+ 4. ✅ OperationTracker JSON functions fixed
641
+ 5. ✅ DualQueryTemplate eliminates duplication
642
+ 6. ✅ 17+ pure logic classes in functional core
1356
643
 
1357
- # What does true/false mean here?
1358
- if opts[:global]
1359
- # ...
1360
- end
1361
- ```
644
+ ### Current State: Excellent
1362
645
 
1363
- **Recommendation:** Use explicit values:
1364
- ```ruby
1365
- scope = opts[:scope] # :global, :project, or :both
1366
-
1367
- case scope
1368
- when :global
1369
- manager.ensure_global!
1370
- when :project
1371
- manager.ensure_project!
1372
- when :both
1373
- manager.ensure_both!
1374
- end
1375
- ```
646
+ **God objects**: Resolved through proper extraction
647
+ **Architecture**: ✅ Sound with clear boundaries
648
+ **Testing**: 74+ spec files, growing coverage
649
+ **Code quality**: ✅ Consistently high across modules
650
+ **Maintainability**: ✅ Excellent with clear patterns
1376
651
 
1377
- ### 🟡 No Use of Ruby 3 Features
652
+ ### Remaining Work: Optional Refinements
1378
653
 
1379
- **Observations:**
1380
- - No pattern matching (available since Ruby 2.7)
1381
- - No rightward assignment
1382
- - No endless method definitions
1383
- - No type annotations (RBS/Sorbet)
654
+ The remaining recommendations are **improvements, not fixes**:
655
+ - Complete strategy pattern (optional architectural refinement)
656
+ - DateTime migration (database best practice)
657
+ - Result objects (error handling clarity)
658
+ - Individual tool classes (minor organizational improvement)
1384
659
 
1385
- **Opportunities:**
1386
- ```ruby
1387
- # Current
1388
- case command
1389
- when "help", "-h", "--help"
1390
- print_help
1391
- when "version", "-v", "--version"
1392
- print_version
1393
- end
660
+ None of these are critical. The codebase is production-ready.
1394
661
 
1395
- # With pattern matching
1396
- case command
1397
- in "help" | "-h" | "--help"
1398
- print_help
1399
- in "version" | "-v" | "--version"
1400
- print_version
1401
- in unknown
1402
- handle_unknown(unknown)
1403
- end
662
+ ### Recommendation
1404
663
 
1405
- # Current
1406
- def valid?(fact)
1407
- fact[:predicate] && fact[:subject_entity_id]
1408
- end
664
+ **Ship it!** The architecture is solid, patterns are clear, and code quality is high. The optional improvements can be done incrementally as part of normal maintenance.
1409
665
 
1410
- # Endless method
1411
- def valid?(fact) = fact[:predicate] && fact[:subject_entity_id]
1412
- ```
666
+ The team has done outstanding work transforming this codebase from having god objects to having a beautiful functional core with clear boundaries.
1413
667
 
1414
668
  ---
1415
669
 
1416
- ## 7. Positive Observations
1417
-
1418
- Despite the issues above, this codebase has several strengths:
1419
-
1420
- ### ✅ Good Practices
670
+ **Review completed:** 2026-01-29
671
+ **Reviewed by:** Claude Code (comprehensive analysis through expert perspectives)
672
+ **Next review:** Recommend after 2-3 months of production use
1421
673
 
1422
- 1. **Frozen String Literals** - Every file has `# frozen_string_literal: true`
1423
- 2. **Consistent Sequel Usage** - Most of the time uses Sequel datasets properly
1424
- 3. **Explicit Dependencies** - Constructor injection used (though inconsistently)
1425
- 4. **Module Namespacing** - Good use of nested modules
1426
- 5. **Test Coverage** - Spec files exist for most modules
1427
- 6. **Documentation** - Good README and CLAUDE.md files
1428
- 7. **Schema Versioning** - Database has schema version tracking
1429
- 8. **Error Classes** - Custom error classes defined
1430
- 9. **Keyword Arguments** - Modern Ruby style with keyword arguments
1431
- 10. **FTS Integration** - Good use of SQLite's FTS5 capabilities
674
+ **Overall Assessment:** PRODUCTION READY
1432
675
 
1433
676
  ---
1434
677
 
1435
- ## 8. Priority Refactoring Recommendations
1436
-
1437
- ### High Priority (Week 1-2)
1438
-
1439
- 1. **Extract CLI Command Objects**
1440
- - Target: Reduce cli.rb from 867 lines to < 200
1441
- - Extract each command to separate class
1442
- - Use command pattern
1443
-
1444
- 2. **Add Transaction Safety**
1445
- - Wrap `promote_fact` in transaction
1446
- - Wrap resolver operations in transactions
1447
- - Add rollback tests
678
+ ## Appendix A: Quick Wins (COMPLETED ✅)
1448
679
 
1449
- 3. **Fix Raw SQL in doctor_cmd**
1450
- - Replace with Sequel dataset methods
1451
- - Ensures consistency
680
+ All quick wins from the previous review have been completed:
1452
681
 
1453
- 4. **Separate I/O from Logic in Core Classes**
1454
- - Start with Recall, Publish
1455
- - Extract functional core
1456
- - Make imperativeshell thin
682
+ 1. **Fix JSON functions in OperationTracker** - DONE
683
+ - Replaced `Sequel.function(:json_set)` with Ruby JSON handling
684
+ - Lines 114-117 and 143-154 now use Ruby JSON.parse/generate
1457
685
 
1458
- ### Medium Priority (Week 3-4)
686
+ 2. **Add WAL checkpoint management** - DONE
687
+ - Added `checkpoint_wal` method to SQLiteStore (lines 40-42)
688
+ - Available for sweep operations
1459
689
 
1460
- 5. **Introduce Value Objects**
1461
- - SessionId, TranscriptPath, FactId
1462
- - Adds type safety
1463
- - Documents domain
690
+ 3. **Extract ResponseFormatter from Tools** - DONE
691
+ - Created `MCP::ResponseFormatter` class (331 lines)
692
+ - All formatting logic properly separated
1464
693
 
1465
- 6. **Replace Nil Returns with Null Objects**
1466
- - NullExplanation, NullFact
1467
- - Enables confident code
1468
- - Reduces nil checks
694
+ 4. **Extract ToolDefinitions** - DONE
695
+ - Created `MCP::ToolDefinitions` module (279 lines)
696
+ - Tool schemas as pure data
1469
697
 
1470
- 7. **Extract Repository Pattern**
1471
- - FactRepository, EntityRepository
1472
- - Abstracts data access
1473
- - Enables testing without database
698
+ 5. **Add ConceptRanker to Core** - DONE
699
+ - Created `Core::ConceptRanker` (74 lines)
700
+ - Pure logic with fast tests
1474
701
 
1475
- 8. **Split Recall into Legacy/Dual**
1476
- - Remove conditional mode logic
1477
- - Clearer single responsibility
1478
- - Easier to maintain
1479
-
1480
- ### Low Priority (Week 5+)
1481
-
1482
- 9. **Add Domain Models**
1483
- - Fact, Entity, Provenance classes
1484
- - Rich domain behavior
1485
- - Replace primitive hashes
1486
-
1487
- 10. **Introduce Proper Migrations**
1488
- - Use Sequel migration framework
1489
- - Versioned, reversible
1490
- - Development/production parity
1491
-
1492
- 11. **Add Type Annotations**
1493
- - Consider RBS or Sorbet
1494
- - Better IDE support
1495
- - Catches type errors early
1496
-
1497
- 12. **Centralize Configuration**
1498
- - Configuration class
1499
- - Environment variable access
1500
- - Testable, mockable
702
+ **All quick wins completed!**
1501
703
 
1502
704
  ---
1503
705
 
1504
- ## 9. Conclusion
1505
-
1506
- This codebase shows solid Ruby fundamentals but suffers from common growing pains: God Objects, mixed concerns, and lack of architectural boundaries. The issues are fixable and follow predictable patterns.
1507
-
1508
- **Key Takeaways:**
1509
- 1. **CLI needs major refactoring** - Extract command objects
1510
- 2. **Separate I/O from logic** - Enable fast tests
1511
- 3. **Use transactions** - Data integrity
1512
- 4. **Introduce domain objects** - Replace primitive hashes
1513
- 5. **Adopt null object pattern** - Reduce nil checks
1514
-
1515
- **Estimated Refactoring Effort:**
1516
- - High priority: 2 weeks (1 developer)
1517
- - Medium priority: 2 weeks (1 developer)
1518
- - Low priority: 1-2 weeks (1 developer)
1519
- - Total: 5-6 weeks for comprehensive refactoring
1520
-
1521
- **Risk Assessment:** Low-to-medium risk. Changes are incremental and testable. Existing test suite provides safety net.
706
+ ## Appendix B: File Size Report
707
+
708
+ **No files > 500 lines!** 🎉
709
+
710
+ **Medium Files (200-600 lines):**
711
+ - `lib/claude_memory/mcp/tools.rb` - 592 lines (down 43%)
712
+ - `lib/claude_memory/recall.rb` - 575 lines (down 24%)
713
+ - `lib/claude_memory/store/sqlite_store.rb` - 389 lines
714
+ - `lib/claude_memory/mcp/response_formatter.rb` - 331 lines
715
+ - `lib/claude_memory/mcp/tool_definitions.rb` - 279 lines
716
+
717
+ **Well-Sized Files (< 200 lines):**
718
+ - `lib/claude_memory/cli.rb` - 41 lines
719
+ - `lib/claude_memory/commands/doctor_command.rb` - 31 lines
720
+ - `lib/claude_memory/core/fact_ranker.rb` - 114 lines
721
+ - `lib/claude_memory/core/fact_query_builder.rb` - 154 lines
722
+ - `lib/claude_memory/core/concept_ranker.rb` - 74 lines ✅
723
+ - `lib/claude_memory/mcp/setup_status_analyzer.rb` - 73 lines
724
+ - `lib/claude_memory/recall/dual_query_template.rb` - 64 lines ✅
725
+ - Most command files - 30-115 lines ✅
726
+ - Check classes - 30-115 lines each ✅
727
+ - Domain objects - 30-80 lines ✅
728
+ - Value objects - 20-40 lines ✅
729
+
730
+ **Migration Files:**
731
+ - `db/migrations/*.rb` - 7 files ✅
1522
732
 
1523
733
  ---
1524
734
 
1525
- ## Appendix A: Recommended Reading
1526
-
1527
- 1. **Sandi Metz** - _Practical Object-Oriented Design in Ruby_ (POODR)
1528
- 2. **Jeremy Evans** - _Sequel Documentation_ and _Roda Book_
1529
- 3. **Kent Beck** - _Test-Driven Development: By Example_
1530
- 4. **Avdi Grimm** - _Confident Ruby_
1531
- 5. **Gary Bernhardt** - _Boundaries_ talk, _Destroy All Software_ screencasts
1532
- 6. **Martin Fowler** - _Refactoring: Ruby Edition_
735
+ ## Appendix C: Critical Files for Implementation
1533
736
 
1534
- ## Appendix B: Quick Wins (Can Do Today)
1535
-
1536
- 1. Fix raw SQL in `doctor_cmd` (20 minutes)
1537
- 2. Consolidate `attr_reader` in StoreManager (5 minutes)
1538
- 3. Fix boolean logic in `parse_db_init_options` (10 minutes)
1539
- 4. Move `public` declaration in SQLiteStore (2 minutes)
1540
- 5. Extract long methods in CLI (1 hour per method)
1541
-
1542
- ---
737
+ Based on this comprehensive review, the most critical files for implementing the remaining optional improvements are:
1543
738
 
1544
- **Review completed:** 2026-01-21
739
+ - `/Users/valentinostoll/src/claude_memory/lib/claude_memory/recall.rb` - Main query coordinator (575 lines, could complete strategy pattern)
740
+ - `/Users/valentinostoll/src/claude_memory/lib/claude_memory/mcp/tools.rb` - Tool handler (592 lines, well-structured, could split further)
741
+ - `/Users/valentinostoll/src/claude_memory/lib/claude_memory/store/sqlite_store.rb` - Database layer (389 lines, good for DateTime migration)
742
+ - `/Users/valentinostoll/src/claude_memory/lib/claude_memory/resolve/resolver.rb` - Resolution logic (156 lines, uses mutable state)
743
+ - `/Users/valentinostoll/src/claude_memory/lib/claude_memory/index/lexical_fts.rb` - FTS indexer (63 lines, has constructor side effect with flag)