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.
- checksums.yaml +4 -4
- data/.claude/.mind.mv2.aLCUZd +0 -0
- data/.claude/memory.sqlite3 +0 -0
- data/.claude/rules/claude_memory.generated.md +7 -1
- data/.claude/settings.json +0 -4
- data/.claude/settings.local.json +4 -1
- data/.claude-plugin/plugin.json +1 -1
- data/.claude.json +11 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +62 -11
- data/CLAUDE.md +87 -24
- data/README.md +76 -159
- data/docs/EXAMPLES.md +436 -0
- data/docs/RELEASE_NOTES_v0.2.0.md +179 -0
- data/docs/RUBY_COMMUNITY_POST_v0.2.0.md +582 -0
- data/docs/SOCIAL_MEDIA_v0.2.0.md +420 -0
- data/docs/architecture.md +360 -0
- data/docs/expert_review.md +1718 -0
- data/docs/feature_adoption_plan.md +1241 -0
- data/docs/feature_adoption_plan_revised.md +2374 -0
- data/docs/improvements.md +1325 -0
- data/docs/quality_review.md +1544 -0
- data/docs/review_summary.md +480 -0
- data/lefthook.yml +10 -0
- data/lib/claude_memory/cli.rb +16 -844
- data/lib/claude_memory/commands/base_command.rb +95 -0
- data/lib/claude_memory/commands/changes_command.rb +39 -0
- data/lib/claude_memory/commands/conflicts_command.rb +37 -0
- data/lib/claude_memory/commands/db_init_command.rb +40 -0
- data/lib/claude_memory/commands/doctor_command.rb +147 -0
- data/lib/claude_memory/commands/explain_command.rb +65 -0
- data/lib/claude_memory/commands/help_command.rb +37 -0
- data/lib/claude_memory/commands/hook_command.rb +106 -0
- data/lib/claude_memory/commands/ingest_command.rb +47 -0
- data/lib/claude_memory/commands/init_command.rb +218 -0
- data/lib/claude_memory/commands/promote_command.rb +30 -0
- data/lib/claude_memory/commands/publish_command.rb +36 -0
- data/lib/claude_memory/commands/recall_command.rb +61 -0
- data/lib/claude_memory/commands/registry.rb +55 -0
- data/lib/claude_memory/commands/search_command.rb +43 -0
- data/lib/claude_memory/commands/serve_mcp_command.rb +16 -0
- data/lib/claude_memory/commands/sweep_command.rb +36 -0
- data/lib/claude_memory/commands/version_command.rb +13 -0
- data/lib/claude_memory/configuration.rb +38 -0
- data/lib/claude_memory/core/fact_id.rb +41 -0
- data/lib/claude_memory/core/null_explanation.rb +47 -0
- data/lib/claude_memory/core/null_fact.rb +30 -0
- data/lib/claude_memory/core/result.rb +143 -0
- data/lib/claude_memory/core/session_id.rb +37 -0
- data/lib/claude_memory/core/token_estimator.rb +33 -0
- data/lib/claude_memory/core/transcript_path.rb +37 -0
- data/lib/claude_memory/domain/conflict.rb +51 -0
- data/lib/claude_memory/domain/entity.rb +51 -0
- data/lib/claude_memory/domain/fact.rb +70 -0
- data/lib/claude_memory/domain/provenance.rb +48 -0
- data/lib/claude_memory/hook/exit_codes.rb +18 -0
- data/lib/claude_memory/hook/handler.rb +7 -2
- data/lib/claude_memory/index/index_query.rb +89 -0
- data/lib/claude_memory/index/index_query_logic.rb +41 -0
- data/lib/claude_memory/index/query_options.rb +67 -0
- data/lib/claude_memory/infrastructure/file_system.rb +29 -0
- data/lib/claude_memory/infrastructure/in_memory_file_system.rb +32 -0
- data/lib/claude_memory/ingest/content_sanitizer.rb +42 -0
- data/lib/claude_memory/ingest/ingester.rb +3 -0
- data/lib/claude_memory/ingest/privacy_tag.rb +48 -0
- data/lib/claude_memory/mcp/tools.rb +174 -1
- data/lib/claude_memory/publish.rb +29 -20
- data/lib/claude_memory/recall.rb +164 -16
- data/lib/claude_memory/resolve/resolver.rb +41 -37
- data/lib/claude_memory/shortcuts.rb +56 -0
- data/lib/claude_memory/store/store_manager.rb +35 -32
- data/lib/claude_memory/templates/hooks.example.json +0 -4
- data/lib/claude_memory/version.rb +1 -1
- data/lib/claude_memory.rb +59 -21
- 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.
|