claude_memory 0.1.0 → 0.2.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 (75) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/.mind.mv2.aLCUZd +0 -0
  3. data/.claude/memory.sqlite3 +0 -0
  4. data/.claude/rules/claude_memory.generated.md +7 -1
  5. data/.claude/settings.json +0 -4
  6. data/.claude/settings.local.json +4 -1
  7. data/.claude-plugin/plugin.json +1 -1
  8. data/.claude.json +11 -0
  9. data/.ruby-version +1 -0
  10. data/CHANGELOG.md +62 -11
  11. data/CLAUDE.md +87 -24
  12. data/README.md +76 -159
  13. data/docs/EXAMPLES.md +436 -0
  14. data/docs/RELEASE_NOTES_v0.2.0.md +179 -0
  15. data/docs/RUBY_COMMUNITY_POST_v0.2.0.md +582 -0
  16. data/docs/SOCIAL_MEDIA_v0.2.0.md +420 -0
  17. data/docs/architecture.md +360 -0
  18. data/docs/expert_review.md +1718 -0
  19. data/docs/feature_adoption_plan.md +1241 -0
  20. data/docs/feature_adoption_plan_revised.md +2374 -0
  21. data/docs/improvements.md +1325 -0
  22. data/docs/quality_review.md +1544 -0
  23. data/docs/review_summary.md +480 -0
  24. data/lefthook.yml +10 -0
  25. data/lib/claude_memory/cli.rb +16 -844
  26. data/lib/claude_memory/commands/base_command.rb +95 -0
  27. data/lib/claude_memory/commands/changes_command.rb +39 -0
  28. data/lib/claude_memory/commands/conflicts_command.rb +37 -0
  29. data/lib/claude_memory/commands/db_init_command.rb +40 -0
  30. data/lib/claude_memory/commands/doctor_command.rb +147 -0
  31. data/lib/claude_memory/commands/explain_command.rb +65 -0
  32. data/lib/claude_memory/commands/help_command.rb +37 -0
  33. data/lib/claude_memory/commands/hook_command.rb +106 -0
  34. data/lib/claude_memory/commands/ingest_command.rb +47 -0
  35. data/lib/claude_memory/commands/init_command.rb +218 -0
  36. data/lib/claude_memory/commands/promote_command.rb +30 -0
  37. data/lib/claude_memory/commands/publish_command.rb +36 -0
  38. data/lib/claude_memory/commands/recall_command.rb +61 -0
  39. data/lib/claude_memory/commands/registry.rb +55 -0
  40. data/lib/claude_memory/commands/search_command.rb +43 -0
  41. data/lib/claude_memory/commands/serve_mcp_command.rb +16 -0
  42. data/lib/claude_memory/commands/sweep_command.rb +36 -0
  43. data/lib/claude_memory/commands/version_command.rb +13 -0
  44. data/lib/claude_memory/configuration.rb +38 -0
  45. data/lib/claude_memory/core/fact_id.rb +41 -0
  46. data/lib/claude_memory/core/null_explanation.rb +47 -0
  47. data/lib/claude_memory/core/null_fact.rb +30 -0
  48. data/lib/claude_memory/core/result.rb +143 -0
  49. data/lib/claude_memory/core/session_id.rb +37 -0
  50. data/lib/claude_memory/core/token_estimator.rb +33 -0
  51. data/lib/claude_memory/core/transcript_path.rb +37 -0
  52. data/lib/claude_memory/domain/conflict.rb +51 -0
  53. data/lib/claude_memory/domain/entity.rb +51 -0
  54. data/lib/claude_memory/domain/fact.rb +70 -0
  55. data/lib/claude_memory/domain/provenance.rb +48 -0
  56. data/lib/claude_memory/hook/exit_codes.rb +18 -0
  57. data/lib/claude_memory/hook/handler.rb +7 -2
  58. data/lib/claude_memory/index/index_query.rb +89 -0
  59. data/lib/claude_memory/index/index_query_logic.rb +41 -0
  60. data/lib/claude_memory/index/query_options.rb +67 -0
  61. data/lib/claude_memory/infrastructure/file_system.rb +29 -0
  62. data/lib/claude_memory/infrastructure/in_memory_file_system.rb +32 -0
  63. data/lib/claude_memory/ingest/content_sanitizer.rb +42 -0
  64. data/lib/claude_memory/ingest/ingester.rb +3 -0
  65. data/lib/claude_memory/ingest/privacy_tag.rb +48 -0
  66. data/lib/claude_memory/mcp/tools.rb +174 -1
  67. data/lib/claude_memory/publish.rb +29 -20
  68. data/lib/claude_memory/recall.rb +164 -16
  69. data/lib/claude_memory/resolve/resolver.rb +41 -37
  70. data/lib/claude_memory/shortcuts.rb +56 -0
  71. data/lib/claude_memory/store/store_manager.rb +35 -32
  72. data/lib/claude_memory/templates/hooks.example.json +0 -4
  73. data/lib/claude_memory/version.rb +1 -1
  74. data/lib/claude_memory.rb +59 -21
  75. metadata +55 -1
@@ -0,0 +1,1718 @@
1
+ # Expert Review: Feature Adoption Plan
2
+ ## Analysis Through the Lens of 5 Renowned Software Engineers
3
+
4
+ ---
5
+
6
+ ## Executive Summary
7
+
8
+ This document presents a comprehensive review of the Feature Adoption Plan by examining it through the perspectives of five influential software engineers. The review identifies strengths, weaknesses, and provides concrete recommendations for each phase.
9
+
10
+ **Reviewers:**
11
+ 1. Sandi Metz - Object-Oriented Design & Ruby
12
+ 2. Kent Beck - Test-Driven Development & Simple Design
13
+ 3. Jeremy Evans - Sequel & Database Performance
14
+ 4. Gary Bernhardt - Boundaries & Functional Architecture
15
+ 5. Martin Fowler - Refactoring & Evolutionary Design
16
+
17
+ **Overall Assessment:** ✅ Strong foundation with room for improvement
18
+
19
+ ---
20
+
21
+ ## 1. Sandi Metz Review
22
+ ### Practical Object-Oriented Design in Ruby (POODR)
23
+
24
+ #### Phase 1.1: Privacy Tag System - ContentSanitizer
25
+
26
+ **✅ Strengths:**
27
+ - Class methods are simple and focused (Single Responsibility)
28
+ - Clear naming (`strip_tags`, `validate_tag_count!`)
29
+ - Frozen constants prevent mutation
30
+
31
+ **⚠️ Concerns:**
32
+ ```ruby
33
+ # Current implementation (lines 45-55)
34
+ def self.strip_tags(text)
35
+ validate_tag_count!(text)
36
+
37
+ all_tags = SYSTEM_TAGS + USER_TAGS
38
+ all_tags.each do |tag|
39
+ text = text.gsub(/<#{Regexp.escape(tag)}>.*?<\/#{Regexp.escape(tag)}>/m, "")
40
+ end
41
+
42
+ text
43
+ end
44
+ ```
45
+
46
+ **Issues:**
47
+ 1. **Mutating argument** - The method modifies `text` via multiple `gsub` calls
48
+ 2. **Feature envy** - The method knows too much about tag structure
49
+ 3. **Primitive obsession** - Tags are just strings; no Tag object
50
+
51
+ **Sandi says:** *"If you have a muddled mass of code, the most productive thing you can do is to separate things from one another."*
52
+
53
+ **Recommendations:**
54
+
55
+ **Option A: Extract Tag Value Object (Preferred)**
56
+ ```ruby
57
+ # New file: lib/claude_memory/ingest/privacy_tag.rb
58
+ module ClaudeMemory
59
+ module Ingest
60
+ class PrivacyTag
61
+ attr_reader :name
62
+
63
+ def initialize(name)
64
+ @name = name
65
+ freeze
66
+ end
67
+
68
+ def pattern
69
+ /<#{Regexp.escape(name)}>.*?<\/#{Regexp.escape(name)}>/m
70
+ end
71
+
72
+ def strip_from(text)
73
+ text.gsub(pattern, "")
74
+ end
75
+ end
76
+ end
77
+ end
78
+
79
+ # Refactored ContentSanitizer
80
+ class ContentSanitizer
81
+ SYSTEM_TAGS = ["claude-memory-context"].map { |t| PrivacyTag.new(t) }.freeze
82
+ USER_TAGS = ["private", "no-memory", "secret"].map { |t| PrivacyTag.new(t) }.freeze
83
+ MAX_TAG_COUNT = 100
84
+
85
+ def self.strip_tags(text)
86
+ validate_tag_count!(text)
87
+
88
+ all_tags.reduce(text) { |result, tag| tag.strip_from(result) }
89
+ end
90
+
91
+ def self.all_tags
92
+ SYSTEM_TAGS + USER_TAGS
93
+ end
94
+
95
+ def self.validate_tag_count!(text)
96
+ pattern = /<(?:#{all_tags.map(&:name).join("|")})>/
97
+ count = text.scan(pattern).size
98
+
99
+ raise Error, "Too many privacy tags (#{count}), possible ReDoS attack" if count > MAX_TAG_COUNT
100
+ end
101
+ end
102
+ ```
103
+
104
+ **Benefits:**
105
+ - Tag becomes a first-class object
106
+ - `strip_from` method is clear about intent
107
+ - Easier to test individual tag behavior
108
+ - No mutation of method arguments
109
+
110
+ **Option B: Instance-based approach (If stateful behavior needed)**
111
+ ```ruby
112
+ class ContentSanitizer
113
+ def initialize(tags: default_tags)
114
+ @tags = tags
115
+ end
116
+
117
+ def strip(text)
118
+ validate_tag_count!(text)
119
+ @tags.reduce(text) { |result, tag| tag.strip_from(result) }
120
+ end
121
+
122
+ private
123
+
124
+ def default_tags
125
+ # ...
126
+ end
127
+ end
128
+
129
+ # Usage:
130
+ sanitizer = ContentSanitizer.new
131
+ sanitizer.strip(text)
132
+ ```
133
+
134
+ **Verdict:** ✅ Approved with refactoring to use Tag value object
135
+
136
+ ---
137
+
138
+ #### Phase 1.2: Progressive Disclosure - TokenEstimator
139
+
140
+ **✅ Strengths:**
141
+ - Simple, focused class
142
+ - Class methods appropriate for stateless utility
143
+ - Clear naming
144
+
145
+ **⚠️ Concerns:**
146
+ ```ruby
147
+ # Lines 259-268
148
+ def self.estimate(text)
149
+ return 0 if text.nil? || text.empty?
150
+
151
+ normalized = text.strip.gsub(/\s+/, " ")
152
+ chars = normalized.length
153
+
154
+ (chars / CHARS_PER_TOKEN).ceil
155
+ end
156
+ ```
157
+
158
+ **Issue:** Guard clause pattern is good, but could use Null Object
159
+
160
+ **Sandi says:** *"Raise your hand if you have ever written code to check if something is nil before calling a method on it."*
161
+
162
+ **Recommendation:**
163
+
164
+ ```ruby
165
+ # Better: Use null object or extract normalization
166
+ class TokenEstimator
167
+ NULL_TEXT = "".freeze
168
+
169
+ def self.estimate(text)
170
+ text = text || NULL_TEXT
171
+ normalized = normalize(text)
172
+ (normalized.length / CHARS_PER_TOKEN).ceil
173
+ end
174
+
175
+ def self.normalize(text)
176
+ text.strip.gsub(/\s+/, " ")
177
+ end
178
+
179
+ def self.estimate_fact(fact)
180
+ text = [
181
+ fact[:subject_name],
182
+ fact[:predicate],
183
+ fact[:object_literal]
184
+ ].compact.join(" ")
185
+
186
+ estimate(text)
187
+ end
188
+ end
189
+ ```
190
+
191
+ **Verdict:** ✅ Approved - Minor improvement suggested
192
+
193
+ ---
194
+
195
+ #### Phase 1.2: Progressive Disclosure - query_index
196
+
197
+ **❌ Major Concerns:**
198
+ ```ruby
199
+ # Lines 364-419 - query_index_single_store method
200
+ def query_index_single_store(store, query_text, limit:, source:)
201
+ fts = Index::LexicalFTS.new(store)
202
+ content_ids = fts.search(query_text, limit: limit * 3)
203
+ return [] if content_ids.empty?
204
+
205
+ seen_fact_ids = Set.new
206
+ ordered_fact_ids = []
207
+
208
+ # 17 lines of fact ID collection logic
209
+
210
+ # 16 lines of fact querying and mapping
211
+ end
212
+ ```
213
+
214
+ **Issues:**
215
+ 1. **Long method** - 55 lines violates "methods should be 5 lines or less"
216
+ 2. **Multiple responsibilities** - Collects IDs, queries facts, transforms results
217
+ 3. **Duplication** - Similar to `query_single_store` (already exists)
218
+ 4. **Tell, don't ask** - Too much asking of store object
219
+
220
+ **Sandi says:** *"The single biggest problem in communication is the illusion that it has taken place."* (about method length)
221
+
222
+ **Recommendations:**
223
+
224
+ **Extract Query Object:**
225
+ ```ruby
226
+ # New file: lib/claude_memory/recall/index_query.rb
227
+ module ClaudeMemory
228
+ module Recall
229
+ class IndexQuery
230
+ def initialize(store, query_text, limit:, source:)
231
+ @store = store
232
+ @query_text = query_text
233
+ @limit = limit
234
+ @source = source
235
+ end
236
+
237
+ def execute
238
+ return [] if content_ids.empty?
239
+
240
+ build_index_results
241
+ end
242
+
243
+ private
244
+
245
+ def content_ids
246
+ @content_ids ||= search_content
247
+ end
248
+
249
+ def search_content
250
+ fts = Index::LexicalFTS.new(@store)
251
+ fts.search(@query_text, limit: @limit * 3)
252
+ end
253
+
254
+ def fact_ids
255
+ @fact_ids ||= FactIdCollector.new(@store, content_ids, @limit).collect
256
+ end
257
+
258
+ def build_index_results
259
+ facts = batch_fetch_facts
260
+ facts.map { |fact| IndexResult.new(fact, @source).to_h }
261
+ end
262
+
263
+ def batch_fetch_facts
264
+ @store.facts
265
+ .left_join(:entities, id: :subject_entity_id)
266
+ .select(index_columns)
267
+ .where(Sequel[:facts][:id] => fact_ids)
268
+ .all
269
+ end
270
+
271
+ def index_columns
272
+ [
273
+ Sequel[:facts][:id],
274
+ Sequel[:facts][:predicate],
275
+ Sequel[:facts][:object_literal],
276
+ Sequel[:facts][:status],
277
+ Sequel[:entities][:canonical_name].as(:subject_name),
278
+ Sequel[:facts][:scope],
279
+ Sequel[:facts][:confidence]
280
+ ]
281
+ end
282
+ end
283
+
284
+ class FactIdCollector
285
+ def initialize(store, content_ids, limit)
286
+ @store = store
287
+ @content_ids = content_ids
288
+ @limit = limit
289
+ end
290
+
291
+ def collect
292
+ seen = Set.new
293
+ ordered = []
294
+
295
+ @content_ids.each do |content_id|
296
+ provenance_records = fetch_provenance(content_id)
297
+
298
+ provenance_records.each do |prov|
299
+ fact_id = prov[:fact_id]
300
+ next if seen.include?(fact_id)
301
+
302
+ seen.add(fact_id)
303
+ ordered << fact_id
304
+ break if ordered.size >= @limit
305
+ end
306
+ break if ordered.size >= @limit
307
+ end
308
+
309
+ ordered
310
+ end
311
+
312
+ private
313
+
314
+ def fetch_provenance(content_id)
315
+ @store.provenance
316
+ .select(:fact_id)
317
+ .where(content_item_id: content_id)
318
+ .all
319
+ end
320
+ end
321
+
322
+ class IndexResult
323
+ def initialize(fact, source)
324
+ @fact = fact
325
+ @source = source
326
+ end
327
+
328
+ def to_h
329
+ {
330
+ id: @fact[:id],
331
+ subject: @fact[:subject_name],
332
+ predicate: @fact[:predicate],
333
+ object_preview: truncate_object,
334
+ status: @fact[:status],
335
+ scope: @fact[:scope],
336
+ confidence: @fact[:confidence],
337
+ token_estimate: estimate_tokens,
338
+ source: @source
339
+ }
340
+ end
341
+
342
+ private
343
+
344
+ def truncate_object
345
+ @fact[:object_literal]&.slice(0, 50)
346
+ end
347
+
348
+ def estimate_tokens
349
+ Core::TokenEstimator.estimate_fact(@fact)
350
+ end
351
+ end
352
+ end
353
+ end
354
+
355
+ # Simplified query_index_single_store
356
+ def query_index_single_store(store, query_text, limit:, source:)
357
+ IndexQuery.new(store, query_text, limit: limit, source: source).execute
358
+ end
359
+ ```
360
+
361
+ **Benefits:**
362
+ - Each class has one job
363
+ - Methods are 5-10 lines
364
+ - Easy to test independently
365
+ - Clear dependencies
366
+ - Follows Tell, Don't Ask
367
+
368
+ **Verdict:** ⚠️ Conditional approval - Requires extraction of Query Object
369
+
370
+ ---
371
+
372
+ #### Phase 2.1: Semantic Shortcuts
373
+
374
+ **✅ Strengths:**
375
+ - Class methods appropriate for factory pattern
376
+ - Clear intent
377
+
378
+ **⚠️ Concerns:**
379
+ ```ruby
380
+ # Lines 744-770
381
+ class << self
382
+ def recent_decisions(manager, limit: 10)
383
+ recall = new(manager)
384
+ recall.query("decision constraint rule requirement", limit: limit, scope: SCOPE_ALL)
385
+ end
386
+ # ... repeated pattern
387
+ end
388
+ ```
389
+
390
+ **Issue:** Duplication - Every method follows same pattern
391
+
392
+ **Sandi says:** *"Duplication is far cheaper than the wrong abstraction."*
393
+
394
+ **Recommendation:**
395
+
396
+ **Extract Query Builder:**
397
+ ```ruby
398
+ class << self
399
+ def recent_decisions(manager, limit: 10)
400
+ query_shortcut(manager, "decision constraint rule requirement", limit: limit, scope: SCOPE_ALL)
401
+ end
402
+
403
+ def architecture_choices(manager, limit: 10)
404
+ query_shortcut(manager, "uses framework implements architecture pattern", limit: limit, scope: SCOPE_ALL)
405
+ end
406
+
407
+ def conventions(manager, limit: 20)
408
+ query_shortcut(manager, "convention style format pattern prefer", limit: limit, scope: SCOPE_GLOBAL)
409
+ end
410
+
411
+ private
412
+
413
+ def query_shortcut(manager, query_string, limit:, scope:)
414
+ recall = new(manager)
415
+ recall.query(query_string, limit: limit, scope: scope)
416
+ end
417
+ end
418
+ ```
419
+
420
+ **Even better - Query Object:**
421
+ ```ruby
422
+ # New file: lib/claude_memory/recall/shortcuts.rb
423
+ module ClaudeMemory
424
+ module Recall
425
+ class Shortcuts
426
+ QUERIES = {
427
+ decisions: {
428
+ query: "decision constraint rule requirement",
429
+ scope: :all,
430
+ limit: 10
431
+ },
432
+ architecture: {
433
+ query: "uses framework implements architecture pattern",
434
+ scope: :all,
435
+ limit: 10
436
+ },
437
+ conventions: {
438
+ query: "convention style format pattern prefer",
439
+ scope: :global,
440
+ limit: 20
441
+ }
442
+ }.freeze
443
+
444
+ def self.for(shortcut_name, manager, **overrides)
445
+ config = QUERIES.fetch(shortcut_name)
446
+ options = config.merge(overrides)
447
+
448
+ recall = ClaudeMemory::Recall.new(manager)
449
+ recall.query(options[:query], limit: options[:limit], scope: options[:scope])
450
+ end
451
+ end
452
+ end
453
+ end
454
+
455
+ # Usage:
456
+ Recall::Shortcuts.for(:decisions, manager)
457
+ Recall::Shortcuts.for(:conventions, manager, limit: 30)
458
+ ```
459
+
460
+ **Verdict:** ✅ Approved with extraction recommendation
461
+
462
+ ---
463
+
464
+ ## 2. Kent Beck Review
465
+ ### Test-Driven Development & Simple Design
466
+
467
+ #### Overall TDD Approach
468
+
469
+ **✅ Strengths:**
470
+ - Plan explicitly calls for test-first workflow
471
+ - Tests written before implementation
472
+ - Each feature has dedicated test coverage
473
+
474
+ **Kent says:** *"I'm not a great programmer; I'm just a good programmer with great habits."*
475
+
476
+ ---
477
+
478
+ #### Phase 1.1: Privacy Tag Tests
479
+
480
+ **✅ Strengths:**
481
+ ```ruby
482
+ it "strips <private> tags and content" do
483
+ text = "Public <private>Secret</private> Public"
484
+ expect(described_class.strip_tags(text)).to eq("Public Public")
485
+ end
486
+ ```
487
+ - Clear, focused tests
488
+ - Tests behavior, not implementation
489
+ - Good edge case coverage
490
+
491
+ **⚠️ Missing Tests:**
492
+
493
+ **Kent says:** *"Test everything that could possibly break."*
494
+
495
+ **Additional tests needed:**
496
+ ```ruby
497
+ # Edge cases
498
+ it "handles empty string" do
499
+ expect(described_class.strip_tags("")).to eq("")
500
+ end
501
+
502
+ it "handles text with only tags" do
503
+ expect(described_class.strip_tags("<private>secret</private>")).to eq("")
504
+ end
505
+
506
+ it "handles adjacent tags" do
507
+ text = "<private>a</private><private>b</private>"
508
+ expect(described_class.strip_tags(text)).to eq("")
509
+ end
510
+
511
+ it "handles tags with special regex characters" do
512
+ text = "<private>$100 [special]</private>"
513
+ expect(described_class.strip_tags(text)).to eq("")
514
+ end
515
+
516
+ # Security edge cases
517
+ it "handles malformed tags gracefully" do
518
+ text = "Public <private>Secret Public"
519
+ expect(described_class.strip_tags(text)).to eq("Public <private>Secret Public")
520
+ end
521
+
522
+ it "handles unclosed tags" do
523
+ text = "Public <private>Secret"
524
+ expect(described_class.strip_tags(text)).to eq("Public <private>Secret")
525
+ end
526
+
527
+ # Performance edge cases
528
+ it "handles very long content efficiently" do
529
+ long_text = "a" * 100_000
530
+ expect { described_class.strip_tags(long_text) }.to perform_under(100).ms
531
+ end
532
+ ```
533
+
534
+ **Verdict:** ✅ Approved - Add edge case tests
535
+
536
+ ---
537
+
538
+ #### Phase 1.2: Progressive Disclosure Tests
539
+
540
+ **⚠️ Concerns:**
541
+ ```ruby
542
+ # Line 426-447
543
+ it "returns lightweight index format" do
544
+ fact_id = create_fact("uses_database", "PostgreSQL with extensive configuration")
545
+ results = recall.query_index("database", limit: 10, scope: :all)
546
+
547
+ expect(results).not_to be_empty
548
+ result = results.first
549
+
550
+ # Has essential fields
551
+ expect(result[:id]).to eq(fact_id)
552
+ expect(result[:predicate]).to eq("uses_database")
553
+ # ... more assertions
554
+ end
555
+ ```
556
+
557
+ **Kent says:** *"One assertion per test."*
558
+
559
+ **Issue:** Test checks too many things
560
+
561
+ **Recommendation:**
562
+ ```ruby
563
+ # Split into focused tests
564
+ describe "#query_index" do
565
+ let(:fact_id) { create_fact("uses_database", "PostgreSQL with extensive configuration") }
566
+ let(:results) { recall.query_index("database", limit: 10, scope: :all) }
567
+ let(:result) { results.first }
568
+
569
+ it "returns results" do
570
+ expect(results).not_to be_empty
571
+ end
572
+
573
+ it "includes fact ID" do
574
+ expect(result[:id]).to eq(fact_id)
575
+ end
576
+
577
+ it "includes predicate" do
578
+ expect(result[:predicate]).to eq("uses_database")
579
+ end
580
+
581
+ it "includes truncated preview" do
582
+ expect(result[:object_preview].length).to be <= 50
583
+ end
584
+
585
+ it "includes token estimate" do
586
+ expect(result[:token_estimate]).to be > 0
587
+ end
588
+
589
+ it "excludes full provenance" do
590
+ expect(result).not_to have_key(:receipts)
591
+ end
592
+
593
+ it "excludes temporal data" do
594
+ expect(result).not_to have_key(:valid_from)
595
+ end
596
+ end
597
+ ```
598
+
599
+ **Verdict:** ⚠️ Conditional approval - Split tests
600
+
601
+ ---
602
+
603
+ #### Simple Design Rules
604
+
605
+ **Kent's 4 rules (in priority order):**
606
+ 1. Passes the tests
607
+ 2. Reveals intention
608
+ 3. No duplication
609
+ 4. Fewest elements
610
+
611
+ **Assessment:**
612
+
613
+ **Rule 1: Passes the tests** ✅
614
+ - All features have test coverage
615
+
616
+ **Rule 2: Reveals intention** ⚠️
617
+ - Some long methods hide intent (query_index_single_store)
618
+ - Solution: Extract methods with revealing names
619
+
620
+ **Rule 3: No duplication** ⚠️
621
+ - Semantic shortcuts duplicate pattern
622
+ - query_index duplicates query_single_store logic
623
+ - Solution: Extract common patterns
624
+
625
+ **Rule 4: Fewest elements** ⚠️
626
+ - Adding features without removing old ones
627
+ - Solution: Consider deprecating old patterns when new ones prove better
628
+
629
+ **Recommendations:**
630
+
631
+ ```ruby
632
+ # Example: Duplication in MCP tools
633
+ # Lines 525-550 and 552-593 follow same pattern
634
+
635
+ # Extract:
636
+ class MCP::Tools
637
+ def format_index_response(results, query, scope)
638
+ {
639
+ query: query,
640
+ scope: scope,
641
+ result_count: results.size,
642
+ total_estimated_tokens: results.sum { |r| r[:token_estimate] },
643
+ facts: results.map { |r| format_index_fact(r) }
644
+ }
645
+ end
646
+
647
+ def format_index_fact(result)
648
+ {
649
+ id: result[:id],
650
+ subject: result[:subject],
651
+ predicate: result[:predicate],
652
+ object_preview: result[:object_preview],
653
+ status: result[:status],
654
+ scope: result[:scope],
655
+ confidence: result[:confidence],
656
+ tokens: result[:token_estimate],
657
+ source: result[:source]
658
+ }
659
+ end
660
+ end
661
+ ```
662
+
663
+ **Verdict:** ⚠️ Address duplication before proceeding
664
+
665
+ ---
666
+
667
+ ## 3. Jeremy Evans Review
668
+ ### Sequel Author - Database Performance
669
+
670
+ #### Overall Database Strategy
671
+
672
+ **✅ Strengths:**
673
+ - Uses Sequel datasets (not raw SQL)
674
+ - Batch queries to avoid N+1
675
+ - Left joins for optional associations
676
+
677
+ **Jeremy says:** *"If you're not using datasets, you're not using Sequel."*
678
+
679
+ ---
680
+
681
+ #### Phase 1.2: query_index_single_store Performance
682
+
683
+ **⚠️ Major Performance Concerns:**
684
+
685
+ ```ruby
686
+ # Lines 373-388
687
+ content_ids.each do |content_id|
688
+ provenance_records = store.provenance
689
+ .select(:fact_id)
690
+ .where(content_item_id: content_id)
691
+ .all # ❌ N queries!
692
+
693
+ provenance_records.each do |prov|
694
+ # ...
695
+ end
696
+ end
697
+ ```
698
+
699
+ **Issue:** This is still N+1! For 30 content_ids, this makes 30 queries.
700
+
701
+ **Jeremy says:** *"The biggest performance problem in web applications is the N+1 query problem."*
702
+
703
+ **Recommendation:**
704
+
705
+ ```ruby
706
+ # Batch query ALL provenance at once
707
+ def collect_fact_ids(store, content_ids, limit)
708
+ # Single query with IN clause
709
+ provenance_by_content = store.provenance
710
+ .select(:fact_id, :content_item_id)
711
+ .where(content_item_id: content_ids)
712
+ .all
713
+ .group_by { |p| p[:content_item_id] }
714
+
715
+ seen_fact_ids = Set.new
716
+ ordered_fact_ids = []
717
+
718
+ # Now iterate through results (no queries)
719
+ content_ids.each do |content_id|
720
+ records = provenance_by_content[content_id] || []
721
+
722
+ records.each do |prov|
723
+ fact_id = prov[:fact_id]
724
+ next if seen_fact_ids.include?(fact_id)
725
+
726
+ seen_fact_ids.add(fact_id)
727
+ ordered_fact_ids << fact_id
728
+ break if ordered_fact_ids.size >= limit
729
+ end
730
+ break if ordered_fact_ids.size >= limit
731
+ end
732
+
733
+ ordered_fact_ids
734
+ end
735
+ ```
736
+
737
+ **Query Count:**
738
+ - Before: 1 (FTS) + N (provenance) + 1 (facts) = N+2 queries
739
+ - After: 1 (FTS) + 1 (provenance) + 1 (facts) = 3 queries
740
+
741
+ **Verdict:** ❌ Must fix N+1 before proceeding
742
+
743
+ ---
744
+
745
+ #### Database Connection Management
746
+
747
+ **⚠️ Concern:**
748
+
749
+ ```ruby
750
+ # Lines 342-362 - query_index_dual
751
+ def query_index_dual(query_text, limit:, scope:)
752
+ results = []
753
+
754
+ if scope == SCOPE_ALL || scope == SCOPE_PROJECT
755
+ @manager.ensure_project! if @manager.project_exists?
756
+ if @manager.project_store
757
+ project_results = query_index_single_store(@manager.project_store, ...)
758
+ results.concat(project_results)
759
+ end
760
+ end
761
+
762
+ if scope == SCOPE_ALL || scope == SCOPE_GLOBAL
763
+ @manager.ensure_global! if @manager.global_exists?
764
+ if @manager.global_store
765
+ global_results = query_index_single_store(@manager.global_store, ...)
766
+ results.concat(global_results)
767
+ end
768
+ end
769
+
770
+ dedupe_and_sort(results, limit)
771
+ end
772
+ ```
773
+
774
+ **Issue:** Multiple store connections, but no explicit transaction management
775
+
776
+ **Jeremy says:** *"Always be explicit about transaction boundaries."*
777
+
778
+ **Recommendation:**
779
+
780
+ ```ruby
781
+ def query_index_dual(query_text, limit:, scope:)
782
+ results = []
783
+
784
+ if should_query_project?(scope)
785
+ results.concat(query_project_index(query_text, limit))
786
+ end
787
+
788
+ if should_query_global?(scope)
789
+ results.concat(query_global_index(query_text, limit))
790
+ end
791
+
792
+ dedupe_and_sort(results, limit)
793
+ end
794
+
795
+ private
796
+
797
+ def should_query_project?(scope)
798
+ (scope == SCOPE_ALL || scope == SCOPE_PROJECT) &&
799
+ @manager.project_exists?
800
+ end
801
+
802
+ def query_project_index(query_text, limit)
803
+ return [] unless @manager.project_store
804
+
805
+ query_index_single_store(
806
+ @manager.project_store,
807
+ query_text,
808
+ limit: limit,
809
+ source: :project
810
+ )
811
+ end
812
+
813
+ # Similar for global
814
+ ```
815
+
816
+ **Verdict:** ⚠️ Improve connection handling
817
+
818
+ ---
819
+
820
+ #### Sequel Best Practices Violations
821
+
822
+ **Current code:**
823
+ ```ruby
824
+ # Line 404
825
+ .where(Sequel[:facts][:id] => ordered_fact_ids)
826
+ ```
827
+
828
+ **✅ This is correct!**
829
+
830
+ **But consider adding:**
831
+ ```ruby
832
+ # Add index if not present
833
+ def ensure_indexes!
834
+ db.add_index :facts, :id unless db.indexes(:facts).key?(:facts_id_index)
835
+ db.add_index :provenance, :content_item_id unless db.indexes(:provenance).key?(:provenance_content_item_id_index)
836
+ db.add_index :provenance, :fact_id unless db.indexes(:provenance).key?(:provenance_fact_id_index)
837
+ end
838
+ ```
839
+
840
+ **Verdict:** ✅ Sequel usage is good, add indexes
841
+
842
+ ---
843
+
844
+ ## 4. Gary Bernhardt Review
845
+ ### Boundaries - Functional Core, Imperative Shell
846
+
847
+ #### Overall Architecture
848
+
849
+ **Gary says:** *"Push I/O to the boundaries of your system."*
850
+
851
+ **Assessment:**
852
+
853
+ **Functional Core** (Pure functions, no I/O):
854
+ - ✅ TokenEstimator - Pure calculations
855
+ - ⚠️ ContentSanitizer - Could be purer
856
+ - ❌ query_index_single_store - Mixed I/O and logic
857
+
858
+ **Imperative Shell** (I/O, orchestration):
859
+ - ✅ Ingester - Orchestrates I/O
860
+ - ✅ Commands - Handle I/O
861
+ - ⚠️ Recall - Mixed responsibilities
862
+
863
+ ---
864
+
865
+ #### Phase 1.1: ContentSanitizer Purity
866
+
867
+ **Current:**
868
+ ```ruby
869
+ def self.strip_tags(text)
870
+ validate_tag_count!(text) # ❌ Raises exception (side effect)
871
+
872
+ all_tags = SYSTEM_TAGS + USER_TAGS
873
+ all_tags.each do |tag|
874
+ text = text.gsub(/<#{Regexp.escape(tag)}>.*?<\/#{Regexp.escape(tag)}>/m, "")
875
+ end
876
+
877
+ text
878
+ end
879
+ ```
880
+
881
+ **Gary says:** *"Values don't need tests, decisions don't need tests, but the integration of values and decisions absolutely needs tests."*
882
+
883
+ **Recommendation:**
884
+
885
+ ```ruby
886
+ # Functional core (pure)
887
+ module ContentSanitizer
888
+ module Pure
889
+ def self.strip_tags(text, tags)
890
+ tags.reduce(text) do |result, tag|
891
+ result.gsub(tag.pattern, "")
892
+ end
893
+ end
894
+
895
+ def self.count_tags(text, tags)
896
+ pattern = /<(?:#{tags.map(&:name).join("|")})>/
897
+ text.scan(pattern).size
898
+ end
899
+
900
+ def self.exceeds_limit?(count, limit)
901
+ count > limit
902
+ end
903
+ end
904
+ end
905
+
906
+ # Imperative shell (I/O and decisions)
907
+ class ContentSanitizer
908
+ SYSTEM_TAGS = ["claude-memory-context"].map { |t| PrivacyTag.new(t) }.freeze
909
+ USER_TAGS = ["private", "no-memory", "secret"].map { |t| PrivacyTag.new(t) }.freeze
910
+ MAX_TAG_COUNT = 100
911
+
912
+ def self.strip_tags(text)
913
+ all_tags = SYSTEM_TAGS + USER_TAGS
914
+ count = Pure.count_tags(text, all_tags)
915
+
916
+ if Pure.exceeds_limit?(count, MAX_TAG_COUNT)
917
+ raise Error, "Too many privacy tags (#{count}), possible ReDoS attack"
918
+ end
919
+
920
+ Pure.strip_tags(text, all_tags)
921
+ end
922
+ end
923
+ ```
924
+
925
+ **Benefits:**
926
+ - Pure functions easy to test (no mocking)
927
+ - Can test edge cases in isolation
928
+ - Clear separation of concerns
929
+ - Can reuse pure logic elsewhere
930
+
931
+ **Verdict:** ⚠️ Extract pure core
932
+
933
+ ---
934
+
935
+ #### Phase 1.2: query_index Boundaries
936
+
937
+ **Current Problem:**
938
+ ```ruby
939
+ def query_index_single_store(store, query_text, limit:, source:)
940
+ fts = Index::LexicalFTS.new(store) # I/O
941
+ content_ids = fts.search(query_text, limit: limit * 3) # I/O
942
+ return [] if content_ids.empty? # Decision
943
+
944
+ # Logic mixed with I/O
945
+ seen_fact_ids = Set.new
946
+ ordered_fact_ids = []
947
+
948
+ content_ids.each do |content_id|
949
+ provenance_records = store.provenance... # I/O
950
+ # Logic...
951
+ end
952
+
953
+ # More I/O
954
+ store.facts.left_join...
955
+ end
956
+ ```
957
+
958
+ **Gary says:** *"Dependencies are the problem. Your ability to test is inversely proportional to the number of dependencies."*
959
+
960
+ **Recommendation:**
961
+
962
+ ```ruby
963
+ # Functional core
964
+ module IndexQueryLogic
965
+ def self.collect_fact_ids(provenance_by_content, content_ids, limit)
966
+ seen = Set.new
967
+ ordered = []
968
+
969
+ content_ids.each do |content_id|
970
+ records = provenance_by_content[content_id] || []
971
+
972
+ records.each do |prov|
973
+ fact_id = prov[:fact_id]
974
+ next if seen.include?(fact_id)
975
+
976
+ seen.add(fact_id)
977
+ ordered << fact_id
978
+ break if ordered.size >= limit
979
+ end
980
+ break if ordered.size >= limit
981
+ end
982
+
983
+ ordered
984
+ end
985
+
986
+ def self.build_index_result(fact, source)
987
+ {
988
+ id: fact[:id],
989
+ subject: fact[:subject_name],
990
+ predicate: fact[:predicate],
991
+ object_preview: fact[:object_literal]&.slice(0, 50),
992
+ status: fact[:status],
993
+ scope: fact[:scope],
994
+ confidence: fact[:confidence],
995
+ token_estimate: TokenEstimator.estimate_fact(fact),
996
+ source: source
997
+ }
998
+ end
999
+ end
1000
+
1001
+ # Imperative shell
1002
+ class IndexQuery
1003
+ def initialize(store, query_text, limit:, source:)
1004
+ @store = store
1005
+ @query_text = query_text
1006
+ @limit = limit
1007
+ @source = source
1008
+ end
1009
+
1010
+ def execute
1011
+ content_ids = search_content
1012
+ return [] if content_ids.empty?
1013
+
1014
+ provenance_by_content = fetch_provenance(content_ids)
1015
+ fact_ids = IndexQueryLogic.collect_fact_ids(provenance_by_content, content_ids, @limit)
1016
+ return [] if fact_ids.empty?
1017
+
1018
+ facts = fetch_facts(fact_ids)
1019
+ facts.map { |f| IndexQueryLogic.build_index_result(f, @source) }
1020
+ end
1021
+
1022
+ private
1023
+
1024
+ def search_content
1025
+ Index::LexicalFTS.new(@store).search(@query_text, limit: @limit * 3)
1026
+ end
1027
+
1028
+ def fetch_provenance(content_ids)
1029
+ @store.provenance
1030
+ .select(:fact_id, :content_item_id)
1031
+ .where(content_item_id: content_ids)
1032
+ .all
1033
+ .group_by { |p| p[:content_item_id] }
1034
+ end
1035
+
1036
+ def fetch_facts(fact_ids)
1037
+ @store.facts
1038
+ .left_join(:entities, id: :subject_entity_id)
1039
+ .select(index_columns)
1040
+ .where(Sequel[:facts][:id] => fact_ids)
1041
+ .all
1042
+ end
1043
+
1044
+ def index_columns
1045
+ [
1046
+ Sequel[:facts][:id],
1047
+ Sequel[:facts][:predicate],
1048
+ Sequel[:facts][:object_literal],
1049
+ Sequel[:facts][:status],
1050
+ Sequel[:entities][:canonical_name].as(:subject_name),
1051
+ Sequel[:facts][:scope],
1052
+ Sequel[:facts][:confidence]
1053
+ ]
1054
+ end
1055
+ end
1056
+ ```
1057
+
1058
+ **Benefits:**
1059
+ - Pure logic testable without database
1060
+ - Easy to test edge cases (empty arrays, duplicates, etc.)
1061
+ - Clear boundaries between I/O and logic
1062
+ - Can mock just the I/O parts
1063
+
1064
+ **Test Example:**
1065
+ ```ruby
1066
+ # Fast unit tests (no database)
1067
+ describe IndexQueryLogic do
1068
+ describe ".collect_fact_ids" do
1069
+ it "collects unique fact IDs in order" do
1070
+ provenance = {
1071
+ 1 => [{fact_id: 10}, {fact_id: 11}],
1072
+ 2 => [{fact_id: 11}, {fact_id: 12}]
1073
+ }
1074
+
1075
+ result = described_class.collect_fact_ids(provenance, [1, 2], 10)
1076
+
1077
+ expect(result).to eq([10, 11, 12])
1078
+ end
1079
+
1080
+ it "limits results" do
1081
+ provenance = {
1082
+ 1 => [{fact_id: 10}, {fact_id: 11}],
1083
+ 2 => [{fact_id: 12}, {fact_id: 13}]
1084
+ }
1085
+
1086
+ result = described_class.collect_fact_ids(provenance, [1, 2], 2)
1087
+
1088
+ expect(result).to eq([10, 11])
1089
+ end
1090
+ end
1091
+ end
1092
+ ```
1093
+
1094
+ **Verdict:** ⚠️ Separate functional core from I/O
1095
+
1096
+ ---
1097
+
1098
+ #### Immutability
1099
+
1100
+ **Gary says:** *"Mutation is the root of all evil... well, most evil anyway."*
1101
+
1102
+ **✅ Good:**
1103
+ - Domain models are frozen
1104
+ - Value objects are frozen
1105
+ - Constants are frozen
1106
+
1107
+ **⚠️ Concern:**
1108
+ ```ruby
1109
+ # Line 54 - Mutating text variable
1110
+ text = text.gsub(/<#{Regexp.escape(tag)}>.*?<\/#{Regexp.escape(tag)}>/m, "")
1111
+ ```
1112
+
1113
+ **Already addressed in Sandi's review - use `reduce` pattern**
1114
+
1115
+ **Verdict:** ✅ Good practices, maintain them
1116
+
1117
+ ---
1118
+
1119
+ ## 5. Martin Fowler Review
1120
+ ### Refactoring & Evolutionary Design
1121
+
1122
+ #### Overall Refactoring Strategy
1123
+
1124
+ **Martin says:** *"Any fool can write code that a computer can understand. Good programmers write code that humans can understand."*
1125
+
1126
+ **✅ Strengths:**
1127
+ - Incremental approach (phases)
1128
+ - Test coverage maintained
1129
+ - Backward compatibility preserved
1130
+ - Clear milestones
1131
+
1132
+ ---
1133
+
1134
+ #### Refactoring Catalog Applied
1135
+
1136
+ **Current Plan uses:**
1137
+ 1. ✅ Extract Method (implicit in recommendations)
1138
+ 2. ✅ Extract Class (Query objects suggested)
1139
+ 3. ✅ Replace Magic Number with Symbolic Constant (MAX_TAG_COUNT)
1140
+ 4. ⚠️ Replace Conditional with Polymorphism (not used, could help)
1141
+ 5. ⚠️ Introduce Parameter Object (could reduce parameter lists)
1142
+
1143
+ ---
1144
+
1145
+ #### Phase 1.1: Refactoring Opportunities
1146
+
1147
+ **Extract Class:**
1148
+ ```ruby
1149
+ # Before: Mixed responsibilities
1150
+ class ContentSanitizer
1151
+ def self.strip_tags(text)
1152
+ validate_tag_count!(text)
1153
+ # stripping logic
1154
+ end
1155
+
1156
+ def self.validate_tag_count!(text)
1157
+ # validation logic
1158
+ end
1159
+ end
1160
+
1161
+ # After: Separate concerns
1162
+ class TagValidator
1163
+ def initialize(tags, max_count: 100)
1164
+ @tags = tags
1165
+ @max_count = max_count
1166
+ end
1167
+
1168
+ def validate!(text)
1169
+ count = count_tags(text)
1170
+ raise_if_exceeds_limit(count)
1171
+ end
1172
+
1173
+ private
1174
+
1175
+ def count_tags(text)
1176
+ pattern = /<(?:#{@tags.map(&:name).join("|")})>/
1177
+ text.scan(pattern).size
1178
+ end
1179
+
1180
+ def raise_if_exceeds_limit(count)
1181
+ return if count <= @max_count
1182
+ raise Error, "Too many privacy tags (#{count}), possible ReDoS attack"
1183
+ end
1184
+ end
1185
+
1186
+ class TagStripper
1187
+ def initialize(tags)
1188
+ @tags = tags
1189
+ end
1190
+
1191
+ def strip(text)
1192
+ @tags.reduce(text) { |result, tag| tag.strip_from(result) }
1193
+ end
1194
+ end
1195
+
1196
+ class ContentSanitizer
1197
+ def self.strip_tags(text)
1198
+ validator = TagValidator.new(all_tags)
1199
+ validator.validate!(text)
1200
+
1201
+ stripper = TagStripper.new(all_tags)
1202
+ stripper.strip(text)
1203
+ end
1204
+
1205
+ def self.all_tags
1206
+ SYSTEM_TAGS + USER_TAGS
1207
+ end
1208
+ end
1209
+ ```
1210
+
1211
+ **Verdict:** ✅ Good candidate for refactoring
1212
+
1213
+ ---
1214
+
1215
+ #### Introduce Parameter Object
1216
+
1217
+ **Current:**
1218
+ ```ruby
1219
+ # Lines 348-351
1220
+ project_results = query_index_single_store(@manager.project_store, query_text, limit: limit, source: :project)
1221
+ ```
1222
+
1223
+ **Martin says:** *"When you see long parameter lists, think about grouping data."*
1224
+
1225
+ **Recommendation:**
1226
+
1227
+ ```ruby
1228
+ # New: QueryOptions parameter object
1229
+ class QueryOptions
1230
+ attr_reader :query_text, :limit, :scope, :source
1231
+
1232
+ def initialize(query_text:, limit: 20, scope: SCOPE_ALL, source: nil)
1233
+ @query_text = query_text
1234
+ @limit = limit
1235
+ @scope = scope
1236
+ @source = source
1237
+ freeze
1238
+ end
1239
+
1240
+ def for_project
1241
+ self.class.new(
1242
+ query_text: query_text,
1243
+ limit: limit,
1244
+ scope: scope,
1245
+ source: :project
1246
+ )
1247
+ end
1248
+
1249
+ def for_global
1250
+ self.class.new(
1251
+ query_text: query_text,
1252
+ limit: limit,
1253
+ scope: scope,
1254
+ source: :global
1255
+ )
1256
+ end
1257
+ end
1258
+
1259
+ # Refactored method
1260
+ def query_index_dual(query_text, limit:, scope:)
1261
+ options = QueryOptions.new(query_text: query_text, limit: limit, scope: scope)
1262
+ results = []
1263
+
1264
+ if should_query_project?(options.scope)
1265
+ results.concat(query_index_single_store(@manager.project_store, options.for_project))
1266
+ end
1267
+
1268
+ if should_query_global?(options.scope)
1269
+ results.concat(query_index_single_store(@manager.global_store, options.for_global))
1270
+ end
1271
+
1272
+ dedupe_and_sort(results, options.limit)
1273
+ end
1274
+
1275
+ def query_index_single_store(store, options)
1276
+ IndexQuery.new(store, options).execute
1277
+ end
1278
+ ```
1279
+
1280
+ **Verdict:** ✅ Good refactoring opportunity
1281
+
1282
+ ---
1283
+
1284
+ #### Evolutionary Design - Feature Flags
1285
+
1286
+ **Martin says:** *"The key to evolutionary design is to make small changes and to have good tests."*
1287
+
1288
+ **Recommendation for Progressive Disclosure:**
1289
+
1290
+ ```ruby
1291
+ # Add feature flag support
1292
+ module ClaudeMemory
1293
+ class Configuration
1294
+ def progressive_disclosure_enabled?
1295
+ env.fetch("CLAUDE_MEMORY_PROGRESSIVE_DISCLOSURE", "false") == "true"
1296
+ end
1297
+ end
1298
+ end
1299
+
1300
+ # Gradual rollout
1301
+ class Recall
1302
+ def query_index(query_text, limit: 20, scope: SCOPE_ALL)
1303
+ if config.progressive_disclosure_enabled?
1304
+ query_index_v2(query_text, limit: limit, scope: scope)
1305
+ else
1306
+ # Fallback to full query (existing behavior)
1307
+ query(query_text, limit: limit, scope: scope)
1308
+ end
1309
+ end
1310
+ end
1311
+ ```
1312
+
1313
+ **Benefits:**
1314
+ - Can test in production with subset of users
1315
+ - Easy rollback if issues found
1316
+ - Gradual migration path
1317
+ - Data to prove performance improvement
1318
+
1319
+ **Verdict:** ✅ Strongly recommend feature flags
1320
+
1321
+ ---
1322
+
1323
+ #### Technical Debt Management
1324
+
1325
+ **Martin says:** *"Technical debt is a useful metaphor, but like all metaphors, it shouldn't be taken too literally."*
1326
+
1327
+ **Debt Introduced by Plan:**
1328
+
1329
+ 1. **Duplication debt** - query_index duplicates query logic
1330
+ - **Interest:** Hard to maintain consistency
1331
+ - **Payoff:** Extract shared logic
1332
+
1333
+ 2. **Long method debt** - query_index_single_store is 55 lines
1334
+ - **Interest:** Hard to understand and test
1335
+ - **Payoff:** Extract query objects
1336
+
1337
+ 3. **N+1 debt** - Still present in provenance queries
1338
+ - **Interest:** Performance degrades with scale
1339
+ - **Payoff:** Batch queries
1340
+
1341
+ **Recommendation:**
1342
+ Address high-interest debt (N+1, duplication) before adding new features.
1343
+
1344
+ **Verdict:** ⚠️ Pay off high-interest debt first
1345
+
1346
+ ---
1347
+
1348
+ ## Consensus Recommendations
1349
+
1350
+ ### Critical Changes (All Experts Agree)
1351
+
1352
+ #### 1. Fix N+1 Query in query_index_single_store
1353
+ **Priority:** CRITICAL
1354
+ **Experts:** Jeremy Evans, Gary Bernhardt, Martin Fowler
1355
+
1356
+ ```ruby
1357
+ # Replace lines 373-388 with batch query
1358
+ def collect_fact_ids(store, content_ids, limit)
1359
+ # Batch query - single query instead of N
1360
+ provenance_by_content = store.provenance
1361
+ .select(:fact_id, :content_item_id)
1362
+ .where(content_item_id: content_ids)
1363
+ .all
1364
+ .group_by { |p| p[:content_item_id] }
1365
+
1366
+ # Rest of logic operates on in-memory data
1367
+ # ...
1368
+ end
1369
+ ```
1370
+
1371
+ #### 2. Extract Query Object for Index Search
1372
+ **Priority:** HIGH
1373
+ **Experts:** Sandi Metz, Gary Bernhardt, Martin Fowler
1374
+
1375
+ ```ruby
1376
+ # Create lib/claude_memory/recall/index_query.rb
1377
+ class IndexQuery
1378
+ def initialize(store, options)
1379
+ @store = store
1380
+ @options = options
1381
+ end
1382
+
1383
+ def execute
1384
+ # Orchestrate query with clear steps
1385
+ end
1386
+ end
1387
+ ```
1388
+
1389
+ #### 3. Separate Pure Logic from I/O
1390
+ **Priority:** HIGH
1391
+ **Experts:** Gary Bernhardt, Kent Beck
1392
+
1393
+ ```ruby
1394
+ # Create lib/claude_memory/ingest/content_sanitizer/pure.rb
1395
+ module ContentSanitizer::Pure
1396
+ def self.strip_tags(text, tags)
1397
+ # Pure function - no I/O, no exceptions
1398
+ end
1399
+ end
1400
+ ```
1401
+
1402
+ #### 4. Extract Tag Value Object
1403
+ **Priority:** MEDIUM-HIGH
1404
+ **Experts:** Sandi Metz, Martin Fowler
1405
+
1406
+ ```ruby
1407
+ # Create lib/claude_memory/ingest/privacy_tag.rb
1408
+ class PrivacyTag
1409
+ def initialize(name)
1410
+ @name = name
1411
+ freeze
1412
+ end
1413
+
1414
+ def pattern
1415
+ /<#{Regexp.escape(@name)}>.*?<\/#{Regexp.escape(@name)}>/m
1416
+ end
1417
+
1418
+ def strip_from(text)
1419
+ text.gsub(pattern, "")
1420
+ end
1421
+ end
1422
+ ```
1423
+
1424
+ ### Important Changes (4/5 Experts Agree)
1425
+
1426
+ #### 5. Add Missing Edge Case Tests
1427
+ **Priority:** MEDIUM-HIGH
1428
+ **Experts:** Kent Beck, Gary Bernhardt, Jeremy Evans, Martin Fowler
1429
+
1430
+ ```ruby
1431
+ # Add to spec/claude_memory/ingest/content_sanitizer_spec.rb
1432
+ it "handles empty string"
1433
+ it "handles text with only tags"
1434
+ it "handles adjacent tags"
1435
+ it "handles malformed tags gracefully"
1436
+ it "handles very long content efficiently"
1437
+ ```
1438
+
1439
+ #### 6. Extract Shortcut Query Builder
1440
+ **Priority:** MEDIUM
1441
+ **Experts:** Sandi Metz, Kent Beck, Martin Fowler, Gary Bernhardt
1442
+
1443
+ ```ruby
1444
+ # Create lib/claude_memory/recall/shortcuts.rb
1445
+ class Recall::Shortcuts
1446
+ QUERIES = {
1447
+ decisions: {query: "...", scope: :all, limit: 10}
1448
+ }.freeze
1449
+
1450
+ def self.for(name, manager, **overrides)
1451
+ # Query builder pattern
1452
+ end
1453
+ end
1454
+ ```
1455
+
1456
+ #### 7. Introduce Parameter Object
1457
+ **Priority:** MEDIUM
1458
+ **Experts:** Martin Fowler, Sandi Metz, Gary Bernhardt
1459
+
1460
+ ```ruby
1461
+ # Create lib/claude_memory/recall/query_options.rb
1462
+ class QueryOptions
1463
+ attr_reader :query_text, :limit, :scope, :source
1464
+
1465
+ def initialize(query_text:, limit: 20, scope: SCOPE_ALL, source: nil)
1466
+ # ...
1467
+ end
1468
+ end
1469
+ ```
1470
+
1471
+ ### Optional Enhancements (2-3 Experts)
1472
+
1473
+ #### 8. Feature Flags for Gradual Rollout
1474
+ **Priority:** LOW-MEDIUM
1475
+ **Experts:** Martin Fowler, Kent Beck
1476
+
1477
+ ```ruby
1478
+ def query_index(query_text, limit: 20, scope: SCOPE_ALL)
1479
+ if config.progressive_disclosure_enabled?
1480
+ query_index_v2(query_text, limit: limit, scope: scope)
1481
+ else
1482
+ query(query_text, limit: limit, scope: scope)
1483
+ end
1484
+ end
1485
+ ```
1486
+
1487
+ #### 9. Split Large Test Cases
1488
+ **Priority:** LOW-MEDIUM
1489
+ **Experts:** Kent Beck, Gary Bernhardt
1490
+
1491
+ One assertion per test for better failure messages.
1492
+
1493
+ ---
1494
+
1495
+ ## Revised Implementation Plan
1496
+
1497
+ ### Phase 1: Privacy & Token Economics (Revised)
1498
+
1499
+ #### 1.1 Privacy Tag System (Days 1-4, +1 day)
1500
+
1501
+ **Day 1: Extract Tag Value Object**
1502
+ ```ruby
1503
+ # NEW: Create PrivacyTag first
1504
+ lib/claude_memory/ingest/privacy_tag.rb
1505
+ spec/claude_memory/ingest/privacy_tag_spec.rb
1506
+ ```
1507
+
1508
+ **Day 2: Extract Pure Logic**
1509
+ ```ruby
1510
+ # NEW: Separate pure from impure
1511
+ lib/claude_memory/ingest/content_sanitizer/pure.rb
1512
+ spec/claude_memory/ingest/content_sanitizer/pure_spec.rb
1513
+ ```
1514
+
1515
+ **Day 3: Create ContentSanitizer with extracted components**
1516
+ ```ruby
1517
+ lib/claude_memory/ingest/content_sanitizer.rb
1518
+ spec/claude_memory/ingest/content_sanitizer_spec.rb
1519
+ # Add all edge case tests
1520
+ ```
1521
+
1522
+ **Day 4: Integrate and Document**
1523
+ ```ruby
1524
+ # Integrate into Ingester
1525
+ # Update documentation
1526
+ ```
1527
+
1528
+ #### 1.2 Progressive Disclosure (Days 5-9, +2 days)
1529
+
1530
+ **Day 5: Create QueryOptions Parameter Object**
1531
+ ```ruby
1532
+ # NEW: Parameter object first
1533
+ lib/claude_memory/recall/query_options.rb
1534
+ spec/claude_memory/recall/query_options_spec.rb
1535
+ ```
1536
+
1537
+ **Day 6: Extract Pure Query Logic**
1538
+ ```ruby
1539
+ # NEW: Pure fact collection logic
1540
+ lib/claude_memory/recall/index_query_logic.rb
1541
+ spec/claude_memory/recall/index_query_logic_spec.rb
1542
+ ```
1543
+
1544
+ **Day 7: Create IndexQuery Object**
1545
+ ```ruby
1546
+ # NEW: Query object with fixed N+1
1547
+ lib/claude_memory/recall/index_query.rb
1548
+ spec/claude_memory/recall/index_query_spec.rb
1549
+ ```
1550
+
1551
+ **Day 8: Integrate query_index into Recall**
1552
+ ```ruby
1553
+ # Add query_index method using IndexQuery
1554
+ lib/claude_memory/recall.rb
1555
+ spec/claude_memory/recall_spec.rb
1556
+ ```
1557
+
1558
+ **Day 9: Add MCP Tools and Documentation**
1559
+ ```ruby
1560
+ # MCP tools + docs
1561
+ lib/claude_memory/mcp/tools.rb
1562
+ README.md, CLAUDE.md
1563
+ ```
1564
+
1565
+ ### Phase 2: Semantic Enhancements (Revised)
1566
+
1567
+ #### 2.1 Semantic Shortcuts (Days 10-12, +1 day)
1568
+
1569
+ **Day 10: Create Shortcuts Query Builder**
1570
+ ```ruby
1571
+ # NEW: Centralized shortcuts
1572
+ lib/claude_memory/recall/shortcuts.rb
1573
+ spec/claude_memory/recall/shortcuts_spec.rb
1574
+ ```
1575
+
1576
+ **Day 11: Add Shortcut MCP Tools**
1577
+ ```ruby
1578
+ lib/claude_memory/mcp/tools.rb
1579
+ spec/claude_memory/mcp/tools_spec.rb
1580
+ ```
1581
+
1582
+ **Day 12: Documentation**
1583
+ ```ruby
1584
+ README.md
1585
+ ```
1586
+
1587
+ #### 2.2 Exit Code Strategy (Day 13, unchanged)
1588
+
1589
+ **Day 13: Exit Codes**
1590
+ ```ruby
1591
+ lib/claude_memory/hook/exit_codes.rb
1592
+ lib/claude_memory/hook/handler.rb
1593
+ lib/claude_memory/commands/hook_command.rb
1594
+ spec/claude_memory/commands/hook_command_spec.rb
1595
+ ```
1596
+
1597
+ ### Revised Timeline
1598
+ - **Phase 1:** 9 days (was 7)
1599
+ - **Phase 2:** 4 days (was 3)
1600
+ - **Total:** 13 days vs 11 days (+2 days for better design)
1601
+
1602
+ ---
1603
+
1604
+ ## Updated Testing Strategy
1605
+
1606
+ ### Unit Test Layers
1607
+
1608
+ **Pure Functions (Fast, No Mocking)**
1609
+ ```ruby
1610
+ # ContentSanitizer::Pure
1611
+ # IndexQueryLogic
1612
+ # TokenEstimator
1613
+ ```
1614
+
1615
+ **Value Objects (Fast, Simple)**
1616
+ ```ruby
1617
+ # PrivacyTag
1618
+ # QueryOptions
1619
+ # Domain models (already tested)
1620
+ ```
1621
+
1622
+ **Integration (Medium Speed, Database)**
1623
+ ```ruby
1624
+ # IndexQuery with real store
1625
+ # Recall with real StoreManager
1626
+ ```
1627
+
1628
+ **End-to-End (Slower, Full Stack)**
1629
+ ```ruby
1630
+ # MCP tools with full pipeline
1631
+ # Commands with full pipeline
1632
+ ```
1633
+
1634
+ ### Coverage Goals
1635
+ - **Pure functions:** 100% (easy to achieve)
1636
+ - **Value objects:** 100% (simple)
1637
+ - **Integration:** >90%
1638
+ - **E2E:** >80%
1639
+ - **Overall:** Maintain >80%
1640
+
1641
+ ---
1642
+
1643
+ ## Key Metrics
1644
+
1645
+ ### Code Quality Improvements
1646
+
1647
+ **Before Plan:**
1648
+ - Long methods: 3 (>50 lines)
1649
+ - N+1 queries: 2 active, 1 fixed
1650
+ - Class methods with duplication: 5
1651
+ - Classes with multiple responsibilities: 4
1652
+
1653
+ **After Expert Review:**
1654
+ - Long methods: 0 (all extracted)
1655
+ - N+1 queries: 0 (all fixed with batch queries)
1656
+ - Duplication: Eliminated via Parameter Objects and Query Builders
1657
+ - Single Responsibility: All classes focused
1658
+
1659
+ ### Performance Improvements
1660
+
1661
+ **Progressive Disclosure:**
1662
+ - Query count: 2N+2 → 3 queries
1663
+ - For 30 content_ids: 62 queries → 3 queries (95% reduction)
1664
+
1665
+ **Token Savings:**
1666
+ - Initial search: ~500 tokens → ~50 tokens (90% reduction)
1667
+ - Progressive disclosure workflow: 10x token reduction
1668
+
1669
+ ---
1670
+
1671
+ ## Final Recommendations
1672
+
1673
+ ### Must Do (Before Implementation)
1674
+ 1. ✅ Fix N+1 query in index search (Jeremy Evans)
1675
+ 2. ✅ Extract Query Objects (Sandi Metz, Gary Bernhardt, Martin Fowler)
1676
+ 3. ✅ Separate pure logic from I/O (Gary Bernhardt)
1677
+ 4. ✅ Add comprehensive edge case tests (Kent Beck)
1678
+
1679
+ ### Should Do (During Implementation)
1680
+ 5. ✅ Extract Tag value object (Sandi Metz, Martin Fowler)
1681
+ 6. ✅ Introduce Parameter Objects (Martin Fowler, Sandi Metz)
1682
+ 7. ✅ Extract Shortcut Query Builder (All experts)
1683
+ 8. ✅ Split large test cases (Kent Beck, Gary Bernhardt)
1684
+
1685
+ ### Could Do (Optional Enhancements)
1686
+ 9. ⚠️ Add feature flags for gradual rollout (Martin Fowler)
1687
+ 10. ⚠️ Add database indexes (Jeremy Evans)
1688
+ 11. ⚠️ Extract Validator/Stripper classes (Martin Fowler)
1689
+
1690
+ ---
1691
+
1692
+ ## Expert Quotes Summary
1693
+
1694
+ > **Sandi Metz:** "Change is easy when you have the right abstraction. The wrong abstraction is worse than duplication."
1695
+
1696
+ > **Kent Beck:** "Make it work, make it right, make it fast - in that order."
1697
+
1698
+ > **Jeremy Evans:** "Performance problems are almost always caused by N+1 queries."
1699
+
1700
+ > **Gary Bernhardt:** "Put I/O at the edges of your system and keep your core pure."
1701
+
1702
+ > **Martin Fowler:** "Good design is easier to change than bad design."
1703
+
1704
+ ---
1705
+
1706
+ ## Conclusion
1707
+
1708
+ **Overall Assessment:** The plan is **solid with important revisions needed**.
1709
+
1710
+ **Consensus:**
1711
+ - ✅ Privacy tag system is sound with suggested refactorings
1712
+ - ⚠️ Progressive disclosure has N+1 issue that MUST be fixed
1713
+ - ✅ Semantic shortcuts need consolidation but are good
1714
+ - ✅ Exit code strategy is appropriate
1715
+
1716
+ **Verdict:** **CONDITIONALLY APPROVED** - Implement recommended changes before proceeding.
1717
+
1718
+ The revised plan adds 2 days but results in significantly better code quality, performance, and maintainability. All experts agree this investment is worthwhile.