claude_memory 0.7.1 → 0.9.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 (107) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/memory.sqlite3 +0 -0
  3. data/.claude/rules/claude_memory.generated.md +32 -2
  4. data/.claude/settings.json +65 -15
  5. data/.claude/settings.local.json +5 -2
  6. data/.claude/skills/improve/SKILL.md +113 -25
  7. data/.claude/skills/upgrade-dependencies/SKILL.md +154 -0
  8. data/.claude-plugin/commands/distill-transcripts.md +98 -0
  9. data/.claude-plugin/commands/memory-recall.md +67 -0
  10. data/.claude-plugin/marketplace.json +2 -2
  11. data/.claude-plugin/plugin.json +3 -3
  12. data/.claude-plugin/scripts/hook-runner.sh +14 -0
  13. data/.claude-plugin/scripts/serve-mcp.sh +14 -0
  14. data/.ruby-version +1 -1
  15. data/CHANGELOG.md +90 -1
  16. data/CLAUDE.md +56 -18
  17. data/README.md +35 -0
  18. data/db/migrations/013_add_mcp_tool_calls.rb +26 -0
  19. data/db/migrations/014_canonicalize_predicates.rb +30 -0
  20. data/docs/improvements.md +74 -74
  21. data/docs/influence/claude-mem.md +1 -0
  22. data/docs/influence/claude-supermemory.md +1 -0
  23. data/docs/influence/episodic-memory.md +1 -0
  24. data/docs/influence/grepai.md +1 -0
  25. data/docs/influence/kbs.md +1 -0
  26. data/docs/influence/lossless-claw.md +1 -0
  27. data/docs/influence/qmd.md +1 -0
  28. data/docs/quality_review.md +119 -224
  29. data/hooks/hooks.json +39 -7
  30. data/lib/claude_memory/commands/checks/distill_check.rb +61 -0
  31. data/lib/claude_memory/commands/checks/hooks_check.rb +2 -2
  32. data/lib/claude_memory/commands/checks/vec_check.rb +2 -1
  33. data/lib/claude_memory/commands/completion_command.rb +149 -0
  34. data/lib/claude_memory/commands/doctor_command.rb +2 -0
  35. data/lib/claude_memory/commands/embeddings_command.rb +198 -0
  36. data/lib/claude_memory/commands/help_command.rb +12 -1
  37. data/lib/claude_memory/commands/hook_command.rb +2 -1
  38. data/lib/claude_memory/commands/index_command.rb +85 -78
  39. data/lib/claude_memory/commands/initializers/database_ensurer.rb +16 -0
  40. data/lib/claude_memory/commands/initializers/global_initializer.rb +2 -1
  41. data/lib/claude_memory/commands/initializers/hooks_configurator.rb +55 -11
  42. data/lib/claude_memory/commands/initializers/project_initializer.rb +2 -1
  43. data/lib/claude_memory/commands/install_skill_command.rb +78 -0
  44. data/lib/claude_memory/commands/registry.rb +47 -32
  45. data/lib/claude_memory/commands/reject_command.rb +62 -0
  46. data/lib/claude_memory/commands/restore_command.rb +77 -0
  47. data/lib/claude_memory/commands/skills/distill-transcripts.md +102 -0
  48. data/lib/claude_memory/commands/skills/memory-recall.md +67 -0
  49. data/lib/claude_memory/commands/stats_command.rb +98 -2
  50. data/lib/claude_memory/configuration.rb +14 -1
  51. data/lib/claude_memory/core/fact_ranker.rb +2 -2
  52. data/lib/claude_memory/core/rr_fusion.rb +23 -6
  53. data/lib/claude_memory/core/snippet_extractor.rb +7 -3
  54. data/lib/claude_memory/core/text_builder.rb +11 -0
  55. data/lib/claude_memory/distill/json_schema.md +8 -4
  56. data/lib/claude_memory/distill/null_distiller.rb +2 -0
  57. data/lib/claude_memory/domain/entity.rb +13 -1
  58. data/lib/claude_memory/domain/fact.rb +26 -2
  59. data/lib/claude_memory/domain/provenance.rb +0 -1
  60. data/lib/claude_memory/embeddings/api_adapter.rb +97 -0
  61. data/lib/claude_memory/embeddings/dimension_check.rb +23 -0
  62. data/lib/claude_memory/embeddings/fastembed_adapter.rb +46 -12
  63. data/lib/claude_memory/embeddings/generator.rb +4 -0
  64. data/lib/claude_memory/embeddings/inspector.rb +91 -0
  65. data/lib/claude_memory/embeddings/model_registry.rb +210 -0
  66. data/lib/claude_memory/embeddings/resolver.rb +44 -0
  67. data/lib/claude_memory/hook/context_injector.rb +58 -2
  68. data/lib/claude_memory/hook/distillation_runner.rb +46 -0
  69. data/lib/claude_memory/hook/handler.rb +11 -2
  70. data/lib/claude_memory/index/vector_index.rb +15 -2
  71. data/lib/claude_memory/infrastructure/schema_validator.rb +3 -3
  72. data/lib/claude_memory/ingest/ingester.rb +17 -0
  73. data/lib/claude_memory/mcp/handlers/context_handlers.rb +38 -0
  74. data/lib/claude_memory/mcp/handlers/management_handlers.rb +169 -0
  75. data/lib/claude_memory/mcp/handlers/query_handlers.rb +115 -0
  76. data/lib/claude_memory/mcp/handlers/setup_handlers.rb +211 -0
  77. data/lib/claude_memory/mcp/handlers/shortcut_handlers.rb +37 -0
  78. data/lib/claude_memory/mcp/handlers/stats_handlers.rb +205 -0
  79. data/lib/claude_memory/mcp/instructions_builder.rb +19 -1
  80. data/lib/claude_memory/mcp/query_guide.rb +10 -0
  81. data/lib/claude_memory/mcp/response_formatter.rb +1 -0
  82. data/lib/claude_memory/mcp/server.rb +22 -1
  83. data/lib/claude_memory/mcp/telemetry.rb +86 -0
  84. data/lib/claude_memory/mcp/text_summary.rb +26 -0
  85. data/lib/claude_memory/mcp/tool_definitions.rb +116 -4
  86. data/lib/claude_memory/mcp/tool_helpers.rb +43 -0
  87. data/lib/claude_memory/mcp/tools.rb +50 -679
  88. data/lib/claude_memory/publish.rb +40 -5
  89. data/lib/claude_memory/recall/dual_engine.rb +105 -0
  90. data/lib/claude_memory/recall/legacy_engine.rb +138 -0
  91. data/lib/claude_memory/recall/query_core.rb +371 -0
  92. data/lib/claude_memory/recall.rb +121 -673
  93. data/lib/claude_memory/resolve/predicate_policy.rb +63 -3
  94. data/lib/claude_memory/resolve/resolver.rb +43 -0
  95. data/lib/claude_memory/shortcuts.rb +4 -4
  96. data/lib/claude_memory/store/retry_handler.rb +61 -0
  97. data/lib/claude_memory/store/schema_manager.rb +68 -0
  98. data/lib/claude_memory/store/sqlite_store.rb +334 -201
  99. data/lib/claude_memory/store/store_manager.rb +50 -1
  100. data/lib/claude_memory/sweep/maintenance.rb +115 -1
  101. data/lib/claude_memory/sweep/sweeper.rb +3 -0
  102. data/lib/claude_memory/templates/hooks.example.json +26 -7
  103. data/lib/claude_memory/version.rb +1 -1
  104. data/lib/claude_memory.rb +16 -0
  105. metadata +48 -8
  106. data/.claude/memory.sqlite3-shm +0 -0
  107. data/.claude/memory.sqlite3-wal +0 -0
@@ -1,18 +1,20 @@
1
1
  # Code Quality Review - Ruby Best Practices
2
2
 
3
- **Review Date:** 2026-03-09
4
- **Previous Review:** 2026-02-04
5
- **Last Quality Update:** 2026-02-04 (21/24 items completed)
3
+ **Review Date:** 2026-03-19
4
+ **Previous Review:** 2026-03-09
5
+ **Last Quality Update:** 2026-03-20 (13 items completed)
6
6
 
7
7
  ---
8
8
 
9
9
  ## Executive Summary
10
10
 
11
- The codebase has grown from 9,982 to 11,392 LOC since the Feb 4 review. Core architecture remains solid functional core, proper layering, zero bare rescues, zero N+1 queries in hot paths. The 3 files on the previous watch list have all grown: `tools.rb` (610728), `recall.rb` (608681), `sqlite_store.rb` (481→547). All three now exceed 500 lines.
11
+ The codebase has grown from 11,392 to 12,239 LOC since the Mar 9 review. A configurable embedding provider system was added (+847 LOC across 5 new files and 8 modified files). The three watch-list files have grown further: `tools.rb` (728745), `recall.rb` (681727), `sqlite_store.rb` (547→547, unchanged). All three remain above 500 lines.
12
12
 
13
- **New issues found:** 18 items (2 critical, 4 high, 7 medium, 5 low)
14
- **Carried forward:** 9 items from previous review (1 medium, 8 low)
15
- **Total remaining:** 27 items
13
+ Five items were resolved in the Mar 19 quality update session: the Shortcuts scope bug (correctness), ApiAdapter exception typing, silent exception logging (3 locations), dead Configuration accessors removed, and `index_database` decomposed into focused methods. No known correctness bugs remain.
14
+
15
+ **Resolved this session:** 13 items (#1 Tools god object, #2 Recall legacy/dual strategy, #3 Shortcuts scope, #4 SQLiteStore extraction, #9 test coverage for critical files, NEW-1 ApiError, NEW-2 dead accessors, NEW-3 silent rescues, #5 index_database decomposition, #6 promote_fact transaction, #7 provenance nil content_item_id, #12 Resolver mutable state, #19 SnippetExtractor DRY)
16
+ **Resolved since last review:** 3 additional items (ExportCommand N+1, `discover_other_projects` bare rescue, embedding test coverage)
17
+ **Total remaining:** 12 items (0 high, 5 medium, 4 low, 5 carried forward — all structural work complete)
16
18
 
17
19
  ### Current Strengths
18
20
 
@@ -20,70 +22,36 @@ The codebase has grown from 9,982 to 11,392 LOC since the Feb 4 review. Core arc
20
22
  - Domain objects: properly frozen and self-validating
21
23
  - Null object pattern: NullFact, NullExplanation
22
24
  - Result monad: Core::Result for Success/Failure
23
- - 100% frozen_string_literal compliance (112 files)
24
- - 1.90:1 test-to-code ratio (21,632 spec : 11,392 lib)
25
- - Zero bare rescues in hot paths, zero N+1 in query paths
26
- - Well-structured batch loading via FactQueryBuilder
25
+ - 100% frozen_string_literal compliance (117 files)
26
+ - 1.84:1 test-to-code ratio (22,563 spec : 12,239 lib)
27
+ - New embedding subsystem: shared RSpec examples, duck-typed providers, Data.define value object
28
+ - Zero N+1 patterns in hot paths
29
+ - Proper batch loading via FactQueryBuilder
30
+ - Content-addressed dedup in IndexCommand
31
+ - DimensionCheck value object (functional core, no side effects)
32
+ - Zero known correctness bugs
27
33
 
28
34
  ---
29
35
 
30
36
  ## 1. Sandi Metz Perspective
31
37
 
32
38
  ### What's Been Fixed ✅
33
- - N+1 queries eliminated in prior review
34
- - Long methods decomposed (resolve_fact, detailed_stats, check_setup)
35
- - Classes extracted (SchemaValidator, OperationTracker)
39
+ - New embedding providers follow duck typing contract (no base class inheritance)
40
+ - Shared RSpec examples verify provider contract (`spec/support/shared_examples/embedding_provider.rb`)
41
+ - `set_meta`/`get_meta` promoted to public API (needed by DimensionCheck, VectorIndex)
42
+ - ExportCommand N+1 eliminated with batch loading
43
+ - **Shortcuts scope bug fixed** — scopes changed from symbols to strings to match DualQueryTemplate comparisons
44
+ - **`index_database` decomposed** — split 130-line method into 5 focused methods: `index_database` (orchestrator), `handle_dimension_mismatch`, `find_facts_to_index`, `run_indexing`, `process_batch`, `report_dedup_stats`
45
+ - **Tools god object eliminated** — split 745-line `Tools` class into thin 104-line dispatcher + 6 handler modules: `QueryHandlers` (90), `ShortcutHandlers` (37), `ContextHandlers` (38), `ManagementHandlers` (124), `StatsHandlers` (188), `SetupHandlers` (211). No file exceeds 211 lines. Public API unchanged.
46
+ - **Recall legacy/dual duplication eliminated** — split 727-line `Recall` into thin 94-line facade + strategy engines: `DualEngine` (101), `LegacyEngine` (134), shared `QueryCore` module (357). The `if @legacy_mode` branching in every public method is replaced by engine delegation. Public API unchanged.
36
47
 
37
48
  ### Critical Issues 🔴
38
49
 
39
- | # | Issue | File:Line | Effort |
40
- |---|-------|-----------|--------|
41
- | 1 | **Tools god object (728 lines, 48 methods)** | `mcp/tools.rb:1-728` | 2-3 days |
42
-
43
- The `Tools` class handles all 21 MCP tool implementations in a single file. The `call` dispatcher (lines 29-76) is a 21-branch case statement. Each handler mixes parameter extraction, domain logic, and response formatting. Individual tool handlers like `store_extraction` (lines 221-262, 41 lines) and `discover_other_projects` (lines 565-614, 50 lines) violate the 15-line method limit.
44
-
45
- **Fix:** Extract each tool handler into its own class (e.g., `RecallHandler`, `StoreExtractionHandler`) with a common interface, registered via a handler registry.
46
-
47
- | # | Issue | File:Line | Effort |
48
- |---|-------|-----------|--------|
49
- | 2 | **Recall class too large (681 lines, 74 methods)** | `recall.rb:1-681` | 2 days |
50
-
51
- Every public method branches on `@legacy_mode` with parallel `_legacy` / `_dual` implementations (lines 42-56, 58-66, etc.). ~150+ lines of duplicated branching logic. The class has 74 methods — far beyond single responsibility.
52
-
53
- **Fix:** Extract `LegacyQueryEngine` and `DualQueryEngine` classes implementing a common `QueryEngine` interface. Inject the appropriate engine at initialization based on `store_or_manager` type.
50
+ None remaining.
54
51
 
55
52
  ### High Priority Issues
56
53
 
57
- | # | Issue | File:Line | Effort |
58
- |---|-------|-----------|--------|
59
- | 3 | **ExportCommand N+1 queries** | `commands/export_command.rb:83-85` | 30 min |
60
-
61
- Inside the `collect_from_store` loop, each fact triggers 2 individual queries — one for the subject entity (line 84) and one for provenance records (line 85). With 100+ facts this becomes 200+ queries.
62
-
63
- ```ruby
64
- # Current (N+1):
65
- facts_ds.each do |fact|
66
- subject = store.entities.where(id: fact[:subject_entity_id]).first
67
- receipts = store.provenance.where(fact_id: fact[:id]).all
68
- end
69
-
70
- # Fix (batch):
71
- all_facts = facts_ds.all
72
- entity_ids = all_facts.map { |f| f[:subject_entity_id] }.compact.uniq
73
- entities_by_id = store.entities.where(id: entity_ids).all
74
- .each_with_object({}) { |e, h| h[e[:id]] = e }
75
- fact_ids = all_facts.map { |f| f[:id] }
76
- provenance_by_fact = store.provenance.where(fact_id: fact_ids).all
77
- .group_by { |p| p[:fact_id] }
78
- ```
79
-
80
- | # | Issue | File:Line | Effort |
81
- |---|-------|-----------|--------|
82
- | 4 | **SQLiteStore exceeds 500 lines (547)** | `store/sqlite_store.rb:1-547` | 1 day |
83
-
84
- The file combines database connection, retry logic, schema management, migrations, and all CRUD operations. Schema migrations alone account for ~100 lines.
85
-
86
- **Fix:** Extract `SchemaManager` module for migration methods, and consider a `RetryHandler` module for retry logic (lines 24-60).
54
+ None remaining. All >500-line files have been decomposed.
87
55
 
88
56
  ---
89
57
 
@@ -92,88 +60,46 @@ The file combines database connection, retry logic, schema management, migration
92
60
  ### What's Been Fixed ✅
93
61
  - Batch queries in Recall pipeline (FactQueryBuilder)
94
62
  - Transaction wrapping in Resolver
95
- - Proper Sequel DSL usage throughout
96
-
97
- ### Critical Issues 🔴
98
-
99
- | # | Issue | File:Line | Effort |
100
- |---|-------|-----------|--------|
101
- | 5 | **Bare rescue in discover_other_projects** | `mcp/tools.rb:607` | 10 min |
102
-
103
- ```ruby
104
- rescue => _e
105
- entry[:error] = "Could not read database"
106
- end
107
- ```
108
-
109
- This catches *all* exceptions including `NoMemoryError`, `SystemExit`, `Interrupt`. Should use specific exception types.
110
-
111
- **Fix:** `rescue Sequel::DatabaseError, Extralite::Error, IOError => _e`
63
+ - ExportCommand N+1 eliminated
64
+ - `discover_other_projects` now catches specific exception types (`Sequel::DatabaseError, Extralite::Error, IOError`)
65
+ - **promote_fact transaction boundary fixed** — project data read before global transaction (already correct in code; verified and confirmed)
66
+ - **Provenance nil content_item_id fixed** — removed mandatory `content_item_id` validation from `Domain::Provenance`, allowing nil for promoted facts
112
67
 
113
68
  ### Medium Issues 🟡
114
69
 
115
- | # | Issue | File:Line | Effort |
116
- |---|-------|-----------|--------|
117
- | 6 | **Transaction boundary mismatch in promote_fact** | `store/store_manager.rb:89-124` | 30 min |
118
-
119
- The transaction wraps `@global_store.db.transaction` but `copy_provenance` (line 121) reads from `@project_store` inside the global transaction. If `@project_store` fails mid-read after global writes, the global transaction still commits (autocommit on the project side). Not a data loss risk but a consistency concern.
120
-
121
- **Fix:** Read all provenance records from project store *before* the global transaction:
122
-
123
- ```ruby
124
- provenance_records = @project_store.provenance.where(fact_id: fact_id).all
125
- @global_store.db.transaction do
126
- # ... create entities and fact ...
127
- provenance_records.each { |prov| @global_store.insert_provenance(...) }
128
- end
129
- ```
130
-
131
- | # | Issue | File:Line | Effort |
132
- |---|-------|-----------|--------|
133
- | 7 | **Provenance insert with nil content_item_id** | `store/store_manager.rb:133` | 20 min |
134
-
135
- `copy_provenance` passes `content_item_id: nil` when promoting facts. If `insert_provenance` in SQLiteStore validates this field, it will fail silently or raise. The Domain `Provenance` class validates non-nil `content_item_id`.
136
-
137
- **Fix:** Use a sentinel value like `"promoted"` or make `content_item_id` nullable in the provenance domain model for promoted facts.
138
-
139
70
  | # | Issue | File:Line | Effort |
140
71
  |---|-------|-----------|--------|
141
72
  | 8 | **upsert_content_item has 11 keyword parameters** | `store/sqlite_store.rb:158-184` | 1 hour |
142
73
 
143
- Exceeds the 5-parameter guideline significantly. Suggests the method is doing too much.
74
+ Exceeds the 5-parameter guideline. Suggests the method is doing too much.
144
75
 
145
- **Fix:** Introduce a `ContentItemAttributes` value object:
146
-
147
- ```ruby
148
- attrs = ContentItemAttributes.new(source:, session_id:, text_hash:, byte_len:, ...)
149
- store.upsert_content_item(attrs)
150
- ```
76
+ **Fix:** Introduce a `ContentItemAttributes` value object.
151
77
 
152
78
  ---
153
79
 
154
80
  ## 3. Kent Beck Perspective
155
81
 
156
82
  ### What's Been Fixed ✅
157
- - Test-to-code ratio improved to 1.90:1
158
- - Clear boundaries between layers
83
+ - New embedding subsystem has full test coverage (4 spec files, shared examples)
84
+ - `generator_spec.rb` now tests `name`/`dimensions` contract
85
+ - DimensionCheck tested for all 3 states (`:fresh`, `:match`, `:mismatch`)
86
+ - ApiAdapter tested with HTTP mocks (no WebMock dependency)
87
+ - **5 critical untested files now have specs** — `similarity.rb` (10 tests), `metadata_extractor.rb` (9 tests), `tool_extractor.rb` (7 tests), `recover_command.rb` (3 tests), `schema_validator.rb` (6 tests). Total: +36 specs, suite now at 1444.
159
88
 
160
89
  ### High Priority Issues
161
90
 
162
- | # | Issue | File:Line | Effort |
163
- |---|-------|-----------|--------|
164
- | 9 | **16 lib files without tests** | Multiple | 2-3 days |
91
+ None remaining. All high-priority items are resolved.
165
92
 
166
- Critical untested files:
167
- - `embeddings/generator.rb` (161 lines of math/algorithm code)
168
- - `embeddings/similarity.rb`
169
- - `embeddings/fastembed_adapter.rb`
170
- - `commands/stats_command.rb` (239 lines)
171
- - `commands/export_command.rb` (108 lines)
172
- - `commands/recover_command.rb` (75 lines)
173
- - `infrastructure/schema_validator.rb` (215 lines)
174
- - `ingest/metadata_extractor.rb`, `ingest/tool_extractor.rb`
93
+ ### Remaining Untested Files (lower priority)
175
94
 
176
- Database migrations 8-11 also lack migration-specific tests.
95
+ Thin CLI wrappers that delegate to already-tested classes:
96
+ - `commands/stats_command.rb`, `commands/export_command.rb`
97
+ - `commands/changes_command.rb`, `commands/conflicts_command.rb`, `commands/explain_command.rb`
98
+ - `commands/recall_command.rb`, `commands/search_command.rb`
99
+ - `commands/sweep_command.rb`, `commands/publish_command.rb`, `commands/ingest_command.rb`
100
+ - `commands/db_init_command.rb`
101
+ - `commands/checks/` (6 files), `commands/initializers/` (5 files)
102
+ - `mcp/tool_helpers.rb`, `embeddings/fastembed_adapter.rb`, `distill/distiller.rb`
177
103
 
178
104
  ### Medium Issues 🟡
179
105
 
@@ -198,78 +124,65 @@ Three `sleep 1.01` calls wait for filesystem mtime changes. `publish_spec.rb:189
198
124
  ## 4. Avdi Grimm Perspective
199
125
 
200
126
  ### What's Been Fixed ✅
201
- - Null objects used properly (NullFact, NullExplanation)
202
- - Result monad for Success/Failure
203
- - Domain objects frozen and self-validating
127
+ - DimensionCheck returns a Result value object — no exceptions, no side effects
128
+ - `Embeddings.resolve` raises `ArgumentError` with clear message for unknown providers
129
+ - ApiAdapter raises with descriptive messages for missing API keys
130
+ - Duck typing for embedding providers (no base class)
131
+ - **ApiAdapter now uses typed `ApiError < StandardError`** instead of bare `raise "message"`
132
+ - **Resolver mutable state resolved** — verified that `project_path` and `scope` are already threaded as parameters through the entire method chain; no mutable instance state exists
204
133
 
205
- ### Medium Issues 🟡
206
-
207
- | # | Issue | File:Line | Effort |
208
- |---|-------|-----------|--------|
209
- | 12 | **Resolver mutable state after init** | `resolve/resolver.rb:12-13` | 30 min |
210
-
211
- `@current_project_path` and `@current_scope` are set in `apply()` (line 12-13) rather than threaded as parameters through the method chain. This makes the Resolver stateful between calls.
212
-
213
- **Fix:** Pass `project_path` and `scope` as explicit parameters to `resolve_fact`, `create_conflict`, etc.
214
-
215
- *(Carried forward from previous review as #16)*
134
+ ### Carried Forward Issues 🟡
216
135
 
217
136
  | # | Issue | File:Line | Effort |
218
137
  |---|-------|-----------|--------|
219
138
  | 13 | **Inconsistent payload validation in hooks** | `hook/handler.rb:17-53` | 30 min |
220
139
 
221
- `ingest` uses `.fetch("field")` with fallback, `sweep` uses `.fetch("budget", default)`, `publish` uses `.fetch("mode", "shared")`. No consistent schema validation pattern.
222
-
223
- **Fix:** Create a `PayloadValidator` or use a simple schema hash to validate required/optional fields uniformly.
140
+ `ingest` uses `.fetch("field")` with fallback, `sweep` uses `.fetch("budget", default)`, `publish` uses `.fetch("mode", "shared")`. No consistent validation pattern.
224
141
 
225
142
  ---
226
143
 
227
144
  ## 5. Gary Bernhardt Perspective
228
145
 
229
146
  ### What's Been Fixed ✅
230
- - Strong functional core / imperative shell separation
231
- - Value objects (FactId, SessionId, TranscriptPath) are immutable
232
- - Pure logic in FactRanker, ConceptRanker, SnippetExtractor
147
+ - DimensionCheck is pure: takes store + provider, returns immutable Result. No hidden side effects.
148
+ - `clear_stale_embeddings` was moved from hidden infrastructure setup to explicit command-level call.
149
+ - VectorIndex#clear! encapsulates vec0 table knowledge (no raw SQL in command).
150
+ - **Dead Configuration embedding accessors removed** — resolver and ApiAdapter read ENV directly, no unused indirection.
233
151
 
234
- ### High Priority Issues
152
+ ### Carried Forward Issues 🟡
235
153
 
236
154
  | # | Issue | File:Line | Effort |
237
155
  |---|-------|-----------|--------|
238
156
  | 14 | **I/O mixed with logic in discover_other_projects** | `mcp/tools.rb:565-614` | 1 hour |
239
157
 
240
- This method performs: SQL queries (lines 572-590), filesystem checks (line 598: `File.exist?`), database connections in a loop (lines 602-606), and error handling. Pure imperative shell with no separation.
241
-
242
- **Fix:** Extract database discovery to a pure function that returns paths, and filesystem/DB checks to an imperative wrapper.
243
-
244
- ### Medium Issues 🟡
158
+ SQL queries, filesystem checks, database connections in a loop, and error handling all mixed together.
245
159
 
246
160
  | # | Issue | File:Line | Effort |
247
161
  |---|-------|-----------|--------|
248
162
  | 15 | **Sweeper mutable state** | `sweep/sweeper.rb:16-17` | 20 min |
249
163
 
250
- *(Carried forward from previous review as #21)*
251
-
252
164
  | # | Issue | File:Line | Effort |
253
165
  |---|-------|-----------|--------|
254
166
  | 16 | **Dir.chdir in publish tests** | `spec/publish_spec.rb:14` | 15 min |
255
167
 
256
- Tests use `Dir.chdir(test_dir)` which modifies global state. Fragile if tests ever run in parallel.
257
-
258
- **Fix:** Use `Dir.chdir(test_dir) { ... }` block form or inject working directory.
259
-
260
168
  ---
261
169
 
262
170
  ## 6. General Ruby Idioms
263
171
 
172
+ ### What's Been Fixed ✅
173
+ - **Silent exception swallowing resolved** — 3 bare rescue blocks now log via `ClaudeMemory.logger.debug(...)`:
174
+ - `mcp/instructions_builder.rb:29`
175
+ - `hook/context_injector.rb:47`
176
+ - `commands/checks/vec_check.rb:55`
177
+
178
+ - **SnippetExtractor range calculation DRY** — extracted `snippet_range` method to eliminate duplicated start/end index computation between `extract_with_lines` and `build_snippet`
179
+
180
+ ### Carried Forward Issues
181
+
264
182
  | # | Issue | File:Line | Severity | Effort |
265
183
  |---|-------|-----------|----------|--------|
266
184
  | 17 | **ResponseFormatter duplication** | `mcp/response_formatter.rb:27-280` | 🟡 Medium | 1 hour |
267
185
  | 18 | **Publish section generator repetition** | `publish.rb:100-154` | Low | 30 min |
268
- | 19 | **SnippetExtractor validation duplication** | `core/snippet_extractor.rb:18-31` | Low | 10 min |
269
-
270
- `ResponseFormatter` has 4 nearly identical `format_*_fact` methods (`format_recall_fact`, `format_semantic_fact`, `format_concept_fact`, etc.) sharing ~80% of code. Extract a base `format_fact` method with field selection.
271
-
272
- `Publish` has 4 similar section generators (decisions, conventions, constraints, conflicts) each filtering facts by predicate and building markdown. Extract a `SectionBuilder`.
273
186
 
274
187
  ---
275
188
 
@@ -281,37 +194,26 @@ Tests use `Dir.chdir(test_dir)` which modifies global state. Fragile if tests ev
281
194
  - **Proper Sequel usage**: Datasets used consistently, raw SQL avoided almost entirely
282
195
  - **Excellent domain modeling**: Fact, Entity, Provenance are immutable value objects with validation
283
196
  - **Good file organization**: ~1 class per file, consistent naming, clear module nesting
284
- - **Strong test culture**: 1.90:1 test-to-code ratio, behavior-focused tests
197
+ - **Strong test culture**: 1.84:1 test-to-code ratio, behavior-focused tests
285
198
  - **Infrastructure abstractions**: `FileSystem`, `InMemoryFileSystem` enable fast tests
286
199
  - **Core::Result monad**: Consistent Success/Failure pattern throughout
200
+ - **New embedding subsystem**: Clean duck typing with shared RSpec examples verifying provider contract. DimensionCheck is a textbook value object — pure function, immutable result, no side effects. The resolver uses simple case/when (no over-engineered factory/registry).
201
+ - **VectorIndex#clear!**: Properly encapsulates destructive vec0 operation behind the abstraction boundary
287
202
 
288
203
  ---
289
204
 
290
205
  ## 8. Priority Refactoring Recommendations
291
206
 
292
- ### Critical (This Week)
293
- | # | Item | Effort | Impact |
294
- |---|------|--------|--------|
295
- | 5 | Fix bare rescue in `discover_other_projects` | 10 min | Correctness |
296
- | 3 | Fix ExportCommand N+1 queries | 30 min | Performance |
297
-
298
207
  ### High Priority (Next Week)
299
- | # | Item | Effort | Impact |
300
- |---|------|--------|--------|
301
- | 1 | Extract Tools into handler classes | 2-3 days | Maintainability |
302
- | 2 | Extract Recall legacy/dual into strategy | 2 days | Maintainability |
303
- | 9 | Add tests for untested critical files | 2-3 days | Coverage |
304
- | 4 | Extract SQLiteStore schema/retry modules | 1 day | Maintainability |
208
+
209
+ None remaining. All high-priority items are resolved.
305
210
 
306
211
  ### Medium Priority (Next Sprint)
307
212
  | # | Item | Effort | Impact |
308
213
  |---|------|--------|--------|
309
- | 6 | Fix promote_fact transaction boundary | 30 min | Consistency |
310
- | 7 | Fix provenance nil content_item_id | 20 min | Correctness |
311
214
  | 8 | ContentItemAttributes value object | 1 hour | Readability |
312
215
  | 10 | Replace sleep-based tests with mocks | 1 hour | Test speed |
313
216
  | 11 | Shared test factory | 1 hour | DRY |
314
- | 12 | Thread Resolver state as params | 30 min | Immutability |
315
217
  | 17 | ResponseFormatter base method | 1 hour | DRY |
316
218
  | 14 | Separate I/O in discover_other_projects | 1 hour | Boundaries |
317
219
 
@@ -322,69 +224,62 @@ Tests use `Dir.chdir(test_dir)` which modifies global state. Fragile if tests ev
322
224
  | 15 | Sweeper mutable state | 20 min | Immutability |
323
225
  | 16 | Dir.chdir in tests | 15 min | Test isolation |
324
226
  | 18 | Publish section builder | 30 min | DRY |
325
- | 19 | SnippetExtractor validation DRY | 10 min | DRY |
326
227
 
327
- ### Carried Forward (Low Priority from Feb 4)
228
+ ### Carried Forward (Low Priority from Earlier Reviews)
328
229
  | # | Item | Original # |
329
230
  |---|------|-----------|
330
- | 20 | DateTime migration (string timestamps) | #17 |
331
- | 21 | Command manager helper (`with_manager`) | #19 |
332
- | 22 | release_connections polymorphism | #20 |
333
- | 23 | Provenance batch insert (`multi_insert`) | #22 |
334
- | 24 | Individual MCP tool classes | #23 (subsumed by #1) |
335
- | 25 | Result objects for all queries | #24 |
231
+ | 20 | DateTime migration (string timestamps) | Feb 4 #17 |
232
+ | 21 | Command manager helper (`with_manager`) | Feb 4 #19 |
233
+ | 22 | release_connections polymorphism | Feb 4 #20 |
234
+ | 23 | Provenance batch insert (`multi_insert`) | Feb 4 #22 |
235
+ | 25 | Result objects for all queries | Feb 4 #24 |
336
236
 
337
237
  ---
338
238
 
339
239
  ## 9. Conclusion
340
240
 
341
- The codebase maintains its strong architectural foundation but the three largest files have continued growing and now all exceed 500 lines. The most impactful improvements are: (1) fixing the bare rescue and N+1 in export (quick wins), (2) splitting `Tools` and `Recall` into focused classes (structural), and (3) adding tests for the 16 untested files (coverage).
241
+ The codebase maintains its strong architectural foundation. Nine quality items were resolved this session across two passes: the Shortcuts scope correctness bug, ApiAdapter exception typing, silent exception logging, dead code removal, method decomposition, promote_fact transaction verification, provenance nil validation fix, Resolver state verification, and SnippetExtractor DRY extraction.
342
242
 
343
- No correctness regressions found. The batch loading patterns, domain modeling, and test culture remain excellent. The main risk is the growing complexity of `tools.rb` and `recall.rb` making future changes harder.
243
+ No known correctness bugs remain. All three >500-line god objects have been eliminated: `Tools` (745→104), `Recall` (727→94), `SQLiteStore` (547→386). Zero critical or high-priority issues remain. All critical untested files now have specs (+36 tests). The remaining 12 items are medium/low priority (thin CLI wrappers, DRY improvements, carried-forward items).
344
244
 
345
245
  ---
346
246
 
347
247
  ## Appendix A: Metrics Comparison
348
248
 
349
- | Metric | Jan 29 | Feb 4 | Mar 9 |
350
- |--------|--------|-------|-------|
351
- | Ruby files (lib) | ~85 | 104 | 112 |
352
- | LOC (lib) | ~8,000 | 9,982 | 11,392 |
353
- | LOC (spec) | — | 17,693 | 21,632 |
354
- | Pure logic classes | 17+ | 20+ | 20+ |
355
- | Test files | 74+ | 98 | 128 |
356
- | Test-to-code ratio | ~1.5:1 | 1.77:1 | 1.90:1 |
357
- | Files >500 lines | 0 | 2 | **3** 🔴 |
358
- | Bare rescues | 0 | 0 | **1** 🔴 |
359
- | N+1 patterns (hot paths) | 0 | 0 | 0 ✅ |
360
- | N+1 patterns (cold paths) | — | — | **1** 🟡 |
361
- | Untested lib files | — | — | **16** 🟡 |
362
-
363
- ## Appendix B: Quick Wins
364
-
365
- These can be done immediately (< 30 min total):
366
-
367
- 1. **Fix bare rescue** (`mcp/tools.rb:607`): Change `rescue => _e` to `rescue Sequel::DatabaseError, Extralite::Error, IOError => _e`
368
- 2. **SnippetExtractor DRY** (`core/snippet_extractor.rb:18-31`): Extract shared validation to private method
369
- 3. **Dir.chdir block form** (`spec/publish_spec.rb:14`): Use `Dir.chdir(dir) { ... }` instead of global chdir
370
-
371
- ## Appendix C: File Size Report
372
-
373
- | File | Feb 4 | Mar 9 | Trend |
374
- |------|-------|-------|-------|
375
- | `mcp/tools.rb` | ~610 | 728 | ⬆️ +118 |
376
- | `recall.rb` | ~608 | 681 | ⬆️ +73 |
377
- | `store/sqlite_store.rb` | 481 | 547 | ⬆️ +66 |
378
- | `mcp/response_formatter.rb` | | 394 | new to watch |
379
- | `mcp/tool_definitions.rb` | | 303 | new to watch |
380
- | `mcp/text_summary.rb` | — | 257 | new to watch |
381
- | `commands/stats_command.rb` | — | 239 | |
382
- | `commands/uninstall_command.rb` | — | 226 | — |
383
- | `commands/index_command.rb` | — | 224 | — |
384
- | `publish.rb` | — | 221 | — |
385
- | `infrastructure/schema_validator.rb` | — | 215 | — |
386
- | `commands/hook_command.rb` | — | 214 | — |
249
+ | Metric | Jan 29 | Feb 4 | Mar 9 | Mar 19 |
250
+ |--------|--------|-------|-------|--------|
251
+ | Ruby files (lib) | ~85 | 104 | 112 | **117** |
252
+ | LOC (lib) | ~8,000 | 9,982 | 11,392 | **12,239** |
253
+ | LOC (spec) | — | 17,693 | 21,632 | **22,563** |
254
+ | Pure logic classes | 17+ | 20+ | 20+ | **22+** |
255
+ | Test files | 74+ | 98 | 128 | **122** |
256
+ | Test-to-code ratio | ~1.5:1 | 1.77:1 | 1.90:1 | **1.84:1** |
257
+ | Files >500 lines | 0 | 2 | 3 | **0** |
258
+ | Bare rescues (silent) | 0 | 0 | 1 | **0** |
259
+ | N+1 patterns (hot paths) | 0 | 0 | 0 | **0** ✅ |
260
+ | N+1 patterns (cold paths) | — | — | 1 | **0** |
261
+ | Untested lib files | — | — | 16 | **~7 critical** |
262
+ | Known correctness bugs | — | — | — | **0** ✅ |
263
+
264
+ ## Appendix B: File Size Report
265
+
266
+ | File | Mar 9 | Mar 19 | Trend |
267
+ |------|-------|--------|-------|
268
+ | `mcp/tools.rb` | 728 | **104** | ⬇️ -624 (extracted to 6 handler modules) |
269
+ | `recall.rb` | 681 | **94** | ⬇️ -587 (extracted to engine + query_core) |
270
+ | `store/sqlite_store.rb` | 547 | **386** | ⬇️ -161 (extracted retry_handler + schema_manager) |
271
+ | `mcp/response_formatter.rb` | 394 | 396 | ⬆️ +2 |
272
+ | `mcp/tool_definitions.rb` | 303 | 334 | ⬆️ +31 |
273
+ | `commands/index_command.rb` | 224 | 272 | ⬆️ +48 |
274
+ | `mcp/text_summary.rb` | 257 | 258 | ⬆️ +1 |
275
+ | `commands/stats_command.rb` | 239 | 250 | ⬆️ +11 |
276
+ | `commands/uninstall_command.rb` | 226 | 226 | |
277
+ | `publish.rb` | 221 | 221 | |
278
+ | `infrastructure/schema_validator.rb` | 215 | 215 | |
279
+ | `commands/hook_command.rb` | 214 | 214 | |
280
+ | `resolve/resolver.rb` | — | 195 | new to watch |
281
+ | `index/vector_index.rb` | — | 184 | new to watch |
387
282
 
388
283
  ---
389
284
 
390
- **Next review:** After Tools extraction or Recall strategy pattern refactoring
285
+ **Next review:** After Recall strategy pattern refactoring or SQLiteStore extraction
data/hooks/hooks.json CHANGED
@@ -6,7 +6,20 @@
6
6
  {
7
7
  "type": "command",
8
8
  "command": "${CLAUDE_PLUGIN_ROOT}/scripts/hook-runner.sh ingest",
9
- "timeout": 10
9
+ "timeout": 10,
10
+ "statusMessage": "Saving memory..."
11
+ }
12
+ ]
13
+ }
14
+ ],
15
+ "StopFailure": [
16
+ {
17
+ "hooks": [
18
+ {
19
+ "type": "command",
20
+ "command": "${CLAUDE_PLUGIN_ROOT}/scripts/hook-runner.sh ingest",
21
+ "timeout": 10,
22
+ "statusMessage": "Saving memory..."
10
23
  }
11
24
  ]
12
25
  }
@@ -17,12 +30,14 @@
17
30
  {
18
31
  "type": "command",
19
32
  "command": "${CLAUDE_PLUGIN_ROOT}/scripts/hook-runner.sh ingest",
20
- "timeout": 10
33
+ "timeout": 10,
34
+ "statusMessage": "Saving memory..."
21
35
  },
22
36
  {
23
37
  "type": "command",
24
38
  "command": "${CLAUDE_PLUGIN_ROOT}/scripts/hook-runner.sh context",
25
- "timeout": 5
39
+ "timeout": 5,
40
+ "statusMessage": "Loading memory..."
26
41
  }
27
42
  ]
28
43
  }
@@ -33,12 +48,14 @@
33
48
  {
34
49
  "type": "command",
35
50
  "command": "${CLAUDE_PLUGIN_ROOT}/scripts/hook-runner.sh ingest",
36
- "timeout": 30
51
+ "timeout": 30,
52
+ "statusMessage": "Saving memory..."
37
53
  },
38
54
  {
39
55
  "type": "command",
40
56
  "command": "${CLAUDE_PLUGIN_ROOT}/scripts/hook-runner.sh sweep",
41
- "timeout": 30
57
+ "timeout": 30,
58
+ "statusMessage": "Sweeping memory..."
42
59
  }
43
60
  ]
44
61
  }
@@ -49,12 +66,27 @@
49
66
  {
50
67
  "type": "command",
51
68
  "command": "${CLAUDE_PLUGIN_ROOT}/scripts/hook-runner.sh ingest",
52
- "timeout": 30
69
+ "timeout": 30,
70
+ "statusMessage": "Saving memory..."
53
71
  },
54
72
  {
55
73
  "type": "command",
56
74
  "command": "${CLAUDE_PLUGIN_ROOT}/scripts/hook-runner.sh sweep",
57
- "timeout": 30
75
+ "timeout": 30,
76
+ "statusMessage": "Sweeping memory..."
77
+ }
78
+ ]
79
+ }
80
+ ],
81
+ "Notification": [
82
+ {
83
+ "matcher": "idle_prompt",
84
+ "hooks": [
85
+ {
86
+ "type": "command",
87
+ "command": "${CLAUDE_PLUGIN_ROOT}/scripts/hook-runner.sh sweep",
88
+ "timeout": 10,
89
+ "statusMessage": "Sweeping memory..."
58
90
  }
59
91
  ]
60
92
  }
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClaudeMemory
4
+ module Commands
5
+ module Checks
6
+ # Checks for content items missing distillation metrics
7
+ class DistillCheck
8
+ def initialize(db_path, label)
9
+ @db_path = db_path
10
+ @label = label
11
+ end
12
+
13
+ def call
14
+ unless File.exist?(@db_path)
15
+ return {
16
+ status: :ok,
17
+ label: "#{@label}_distill",
18
+ message: "#{@label.capitalize} database not found (skipping distill check)",
19
+ details: {}
20
+ }
21
+ end
22
+
23
+ check_undistilled_content
24
+ rescue => e
25
+ {
26
+ status: :error,
27
+ label: "#{@label}_distill",
28
+ message: "#{@label.capitalize} distill check error: #{e.message}",
29
+ details: {}
30
+ }
31
+ end
32
+
33
+ private
34
+
35
+ def check_undistilled_content
36
+ store = ClaudeMemory::Store::SQLiteStore.new(@db_path)
37
+
38
+ undistilled_count = store.content_items
39
+ .left_join(:ingestion_metrics, content_item_id: :id)
40
+ .where(Sequel[:ingestion_metrics][:id] => nil)
41
+ .count
42
+
43
+ store.close
44
+
45
+ warnings = []
46
+ if undistilled_count > 0
47
+ warnings << "#{undistilled_count} content items have no distillation metrics. Run 'claude-memory init' to migrate."
48
+ end
49
+
50
+ {
51
+ status: warnings.any? ? :warning : :ok,
52
+ label: "#{@label}_distill",
53
+ message: "#{@label.capitalize} distillation metrics: #{(undistilled_count == 0) ? "all tracked" : "#{undistilled_count} untracked"}",
54
+ details: {undistilled_count: undistilled_count},
55
+ warnings: warnings
56
+ }
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -8,7 +8,7 @@ module ClaudeMemory
8
8
  # Checks hooks configuration in settings files
9
9
  class HooksCheck
10
10
  SETTINGS_PATHS = [".claude/settings.json", ".claude/settings.local.json"].freeze
11
- EXPECTED_HOOKS = %w[Stop SessionStart PreCompact SessionEnd].freeze
11
+ EXPECTED_HOOKS = %w[Stop StopFailure SessionStart PreCompact SessionEnd].freeze
12
12
 
13
13
  def call
14
14
  hooks_found = false
@@ -86,7 +86,7 @@ module ClaudeMemory
86
86
  # Check for missing recommended hooks
87
87
  missing = EXPECTED_HOOKS - config["hooks"].keys
88
88
  if missing.any?
89
- warnings << "Missing recommended hooks in #{path}: #{missing.join(", ")}"
89
+ warnings << "Missing recommended hooks in #{path}: #{missing.join(", ")}. Run 'claude-memory init' to add them"
90
90
  end
91
91
 
92
92
  {has_hooks: true, warnings: warnings}