claude_memory 0.10.0 → 0.12.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 (72) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/memory.sqlite3 +0 -0
  3. data/.claude/rules/claude_memory.generated.md +42 -64
  4. data/.claude/skills/release/SKILL.md +44 -6
  5. data/.claude/skills/study-repo/SKILL.md +15 -0
  6. data/.claude-plugin/commands/audit-memory.md +68 -0
  7. data/.claude-plugin/marketplace.json +1 -1
  8. data/.claude-plugin/plugin.json +1 -1
  9. data/CHANGELOG.md +70 -0
  10. data/CLAUDE.md +20 -5
  11. data/README.md +64 -2
  12. data/db/migrations/018_add_otel_telemetry.rb +81 -0
  13. data/docs/1_0_punchlist.md +522 -89
  14. data/docs/GETTING_STARTED.md +3 -1
  15. data/docs/api_stability.md +341 -0
  16. data/docs/architecture.md +3 -3
  17. data/docs/audit_runbook.md +209 -0
  18. data/docs/claude_monitoring.md +956 -0
  19. data/docs/dashboard.md +23 -3
  20. data/docs/improvements.md +329 -5
  21. data/docs/influence/ai-memory-systems-2026.md +403 -0
  22. data/docs/memory_audit_2026-05-21.md +303 -0
  23. data/docs/plugin.md +1 -1
  24. data/docs/quality_review.md +35 -0
  25. data/lib/claude_memory/audit/checks.rb +239 -0
  26. data/lib/claude_memory/audit/finding.rb +33 -0
  27. data/lib/claude_memory/audit/runner.rb +73 -0
  28. data/lib/claude_memory/commands/audit_command.rb +117 -0
  29. data/lib/claude_memory/commands/dashboard_command.rb +2 -1
  30. data/lib/claude_memory/commands/digest_command.rb +95 -3
  31. data/lib/claude_memory/commands/hook_command.rb +27 -2
  32. data/lib/claude_memory/commands/import_auto_memory_command.rb +180 -0
  33. data/lib/claude_memory/commands/initializers/hooks_configurator.rb +7 -4
  34. data/lib/claude_memory/commands/otel_command.rb +240 -0
  35. data/lib/claude_memory/commands/registry.rb +5 -1
  36. data/lib/claude_memory/commands/show_command.rb +90 -0
  37. data/lib/claude_memory/commands/stats_command.rb +94 -2
  38. data/lib/claude_memory/configuration.rb +60 -0
  39. data/lib/claude_memory/core/fact_query_builder.rb +1 -0
  40. data/lib/claude_memory/dashboard/api.rb +8 -0
  41. data/lib/claude_memory/dashboard/index.html +140 -1
  42. data/lib/claude_memory/dashboard/prompt_journey.rb +48 -0
  43. data/lib/claude_memory/dashboard/server.rb +86 -0
  44. data/lib/claude_memory/dashboard/telemetry.rb +156 -0
  45. data/lib/claude_memory/dashboard/trust.rb +180 -11
  46. data/lib/claude_memory/deprecations.rb +106 -0
  47. data/lib/claude_memory/distill/bare_conclusion_detector.rb +71 -0
  48. data/lib/claude_memory/distill/reference_material_detector.rb +37 -4
  49. data/lib/claude_memory/hook/auto_memory_mirror.rb +7 -3
  50. data/lib/claude_memory/hook/context_injector.rb +11 -2
  51. data/lib/claude_memory/hook/handler.rb +142 -1
  52. data/lib/claude_memory/mcp/tool_definitions.rb +3 -3
  53. data/lib/claude_memory/otel/attributes.rb +118 -0
  54. data/lib/claude_memory/otel/constants.rb +32 -0
  55. data/lib/claude_memory/otel/ingestor.rb +54 -0
  56. data/lib/claude_memory/otel/otlp_json_envelope.rb +254 -0
  57. data/lib/claude_memory/otel/prompt_scope.rb +108 -0
  58. data/lib/claude_memory/otel/settings_writer.rb +122 -0
  59. data/lib/claude_memory/otel/status.rb +58 -0
  60. data/lib/claude_memory/recall/staleness_annotator.rb +73 -0
  61. data/lib/claude_memory/resolve/predicate_policy.rb +17 -1
  62. data/lib/claude_memory/resolve/resolver.rb +30 -3
  63. data/lib/claude_memory/shortcuts.rb +61 -18
  64. data/lib/claude_memory/store/prompt_journey_query.rb +87 -0
  65. data/lib/claude_memory/store/schema_manager.rb +1 -1
  66. data/lib/claude_memory/store/sqlite_store.rb +136 -0
  67. data/lib/claude_memory/sweep/maintenance.rb +31 -1
  68. data/lib/claude_memory/sweep/sweeper.rb +6 -0
  69. data/lib/claude_memory/templates/hooks.example.json +5 -0
  70. data/lib/claude_memory/version.rb +1 -1
  71. data/lib/claude_memory.rb +20 -0
  72. metadata +28 -1
@@ -0,0 +1,303 @@
1
+ # Memory Database Audit — 2026-05-21
2
+
3
+ <no-memory>
4
+ This document discusses hallucinated facts and example stack names by way of diagnosis. It is wrapped in `<no-memory>` so the distiller does not re-extract the very words being audited. Human readers ignore the tags.
5
+ </no-memory>
6
+
7
+ <no-memory>
8
+
9
+ **Scope:** Full audit of the ClaudeMemory project's own memory database (global + project) against the actual state of the codebase at v0.11.0. Grades how useful the distilled knowledge is for coding agents working in this repo, and defines a systemic remediation pipeline.
10
+
11
+ **Snapshot:** `claude_memory @ main`, schema v18, 23 MCP tools, 9 predicates in `PredicatePolicy::POLICIES`.
12
+
13
+ ---
14
+
15
+ ## Executive Summary
16
+
17
+ The memory pipeline is structurally sound but **contaminated**. Three independent surfaces (global DB, project DB, generated snapshot) carry mixed signal — the curated snapshot is genuinely useful (~70% signal), but the MCP shortcut tools (`memory.decisions`, `memory.conventions`, `memory.architecture`) are net-misleading in their current form: they surface hallucinated stack diversity and global terminal preferences instead of the rich project knowledge that exists in the DB.
18
+
19
+ **Root cause:** CLAUDE.md's scope-system example text ("this app uses PostgreSQL", "I prefer 4-space indentation") is repeatedly re-distilled as fact. This compounds with a 99-item undistilled backlog and inconsistent shortcut-tool scope filters.
20
+
21
+ **Verdict:** Stop the bleeding (source-text fix + doc drift fix), then clean the noise floor (bulk-reject hallucinated `uses_*` facts), then fix the shortcut tool filters. The system can become trustworthy with ~1 day of focused work.
22
+
23
+ ---
24
+
25
+ ## Part 1 — Ground Truth (from the code)
26
+
27
+ ClaudeMemory v0.11.0, "Trust & Cost" release. Authoritative findings from full repo audit:
28
+
29
+ ### Stack
30
+ - Pure Ruby gem. Ruby ≥ 3.2.
31
+ - Sequel + Extralite over SQLite (`Sequel.connect("extralite:#{path}")` only — never `Sequel.sqlite`).
32
+ - sqlite-vec for KNN; fastembed-rb optional for semantic search.
33
+ - **No Rails, no React, no Django, no Express, no Next.js, no MySQL, no Postgres, no Redis, no AWS/GCP/Azure/Vercel/Docker deployment.** It's a gem you install locally.
34
+
35
+ ### Architecture (7 layers, verified)
36
+ - **Application:** `CLI` (41-line router) → 35 commands under `lib/claude_memory/commands/`, all inheriting `BaseCommand` with stdin/stdout/stderr DI.
37
+ - **Core Domain:** `domain/` (Fact, Entity, Provenance, Conflict — frozen + validated) + `core/` (21 value/null objects: Result, SessionId, NullFact, FactRanker, RRFusion, etc.).
38
+ - **Infrastructure:** `Store::SQLiteStore` (with `RetryHandler`, `SchemaManager`, `LLMCache`, `MetricsAggregator` mixins), `Store::StoreManager` (dual-DB router), `Infrastructure::FileSystem` / `InMemoryFileSystem`.
39
+ - **Business Logic:** `Ingest`, `Index` (`LexicalFTS` + `VectorIndex`), `Distill` (`NullDistiller`, `ReferenceMaterialDetector`, `BareConclusionDetector`), `Resolve::Resolver` + `Resolve::PredicatePolicy`, `Recall` (facade → `DualEngine`/`LegacyEngine` both including `QueryCore`), `Sweep`, `Publish`, `MCP`, `Hook`.
40
+ - **Dashboard:** 14 panel modules under `lib/claude_memory/dashboard/`.
41
+
42
+ ### Schema
43
+ Current `SCHEMA_VERSION = 18` in `lib/claude_memory/store/schema_manager.rb:8`. 18 migrations in `db/migrations/`:
44
+ - v13 → `mcp_tool_calls` (telemetry, minimal columns — no query_text/hash by YAGNI).
45
+ - v15 → `activity_events` (hook/recall/context/sweep/store_extraction/roi_nudge events).
46
+ - v16 → `moment_feedback` (per-event 👍/👎 verdicts, upsert on event_id).
47
+ - v17 → `facts.last_recalled_at` (access-based staleness via `Sweep::RecallTimestampRefresher`).
48
+ - v18 → `otel_metrics`/`otel_events`/`otel_traces` + `activity_events.prompt_id` for prompt-journey correlation.
49
+
50
+ ### Predicates (9, not 8)
51
+ From `lib/claude_memory/resolve/predicate_policy.rb`:
52
+
53
+ | Predicate | Cardinality | Section |
54
+ |---|---|---|
55
+ | `convention` | multi | conventions |
56
+ | `decision` | multi | decisions |
57
+ | `architecture` | multi | additional (intentionally unmapped) |
58
+ | `reference` | multi | references *(new in 0.11.0)* |
59
+ | `uses_framework` | multi | constraints |
60
+ | `uses_language` | multi | constraints |
61
+ | `uses_database` | single (exclusive) | constraints |
62
+ | `deployment_platform` | single (exclusive) | constraints |
63
+ | `auth_method` | single (exclusive) | constraints |
64
+
65
+ Synonyms (with `Deprecations.warn`, removal in 1.0.0): `has_convention → convention`, `primary_language → uses_language`.
66
+
67
+ ### MCP (23 tools, not 25)
68
+ Six handler modules under `lib/claude_memory/mcp/handlers/`: `QueryHandlers`, `ShortcutHandlers`, `ContextHandlers`, `ManagementHandlers`, `StatsHandlers`, `SetupHandlers`. `MCP::Telemetry` wraps `Server#handle_tools_call`, records to `mcp_tool_calls`, swallows DB errors so telemetry never breaks a tool response.
69
+
70
+ ### Hooks
71
+ Five events: **ingest**, **context**, **sweep**, **publish**, **nudge** (new in 0.11.0, `MAX_NUDGES=10`, silently no-ops on empty sessions or `CLAUDE_MEMORY_NO_NUDGE=1`). `AutoMemoryMirror` runs on fresh `SessionStart`, surfaces up to 5 candidates × 1500 chars from `~/.claude/projects/<slug>/memory/*.md`.
72
+
73
+ ### Distillation (three layers, all wired)
74
+ - **Layer 1 — NullDistiller:** regex, P95 < 5ms, runs every hook.
75
+ - **Layer 2 — SessionStart context injection:** `hookSpecificOutput.additionalContext` with reason-clause-required extraction prompt. Claude Code itself acts as distiller at no extra API cost.
76
+ - **Layer 3 — `/distill-transcripts` skill:** manual, deep extraction with depth-aware prompts.
77
+ - `ReferenceMaterialDetector` is wired at `mcp/handlers/management_handlers.rb:37` so external-project descriptions can't persist as `convention` facts.
78
+ - `BareConclusionDetector` is wired into the Trust panel's `quality_score` and the digest's Quality section.
79
+
80
+ ### Documentation drift in CLAUDE.md / generated rules
81
+ - "8 predicates" → actually **9** (CLAUDE.md predates the `reference` predicate added in 0.11.0).
82
+ - "25 MCP tools total" → actually **23**.
83
+
84
+ ---
85
+
86
+ ## Part 2 — What Memory Actually Believes
87
+
88
+ ### Database state (`memory.stats`)
89
+
90
+ | Scope | Total | Active | Superseded | Open Conflicts | Pending Distillation |
91
+ |---|---|---|---|---|---|
92
+ | Global | 12 | 7 | 2 | 0 | — |
93
+ | Project | 201 | 46 | 37 | **10** | **99** |
94
+
95
+ ### Project predicate distribution (46 active)
96
+ - `convention`: 28
97
+ - `architecture`: 6
98
+ - `uses_language`: 3 *(ruby, go, python — repo is Ruby-only)*
99
+ - `decision`: 3
100
+ - `uses_framework`: 2 *(rails, react — neither present in code)*
101
+ - `reference`: 2
102
+ - `uses_database`: 1
103
+ - `deployment_platform`: 1
104
+
105
+ ### Global memory (7 active)
106
+ All `convention` predicate, all user-level workflow prefs: Docker, tmux, iTerm2, VS Code + Ruby LSP. Several near-duplicates (ids 1↔8, 6↔11, 7↔12, 5↔10).
107
+
108
+ ### Open conflicts (10, all cluster around hallucination loop)
109
+ - Fact #21 `uses_database=sqlite` vs #139 postgresql, #148 postgres, #154, #155 redis.
110
+ - Fact #45 `uses_framework=rails` vs sinatra/express/django/next.js/react.
111
+ - Fact #48 `deployment_platform=aws` vs gcp/vercel/docker/azure.
112
+
113
+ All caused by CLAUDE.md scope-system example text being repeatedly re-extracted; resolver dutifully creates a new conflict each pass because none of the values can be authoritatively chosen.
114
+
115
+ ---
116
+
117
+ ## Part 3 — Tool-by-Tool Grading
118
+
119
+ ### `memory.decisions` — Grade: D
120
+ Returns 23 results, only **3** are actual `decision`-predicate facts. The rest are `uses_*`/`deployment_platform`/`reference` rows. Output mixes contradictory single-cardinality predicates as if they all hold simultaneously. **Net effect: agent concludes this gem talks to MySQL + Postgres + Redis + SQLite at once.** It doesn't.
121
+
122
+ ### `memory.conventions` — Grade: F (for project work)
123
+ Returns 10 results, **all global scope**. The 28 high-value project conventions (Configuration instance-only, Sequel/extralite adapter rule, version-in-3-places, EXPECTED_HOOKS sync, block-style rule, A/B testing methodology, `/release` workflow, etc.) are **not returned**. Worse, the global list is half-duplicates.
124
+
125
+ ### `memory.architecture` — Grade: D
126
+ Returns 31 results. ~25 are hallucinated `uses_*` / `deployment_platform` facts; ~6 are global user-preference noise. The real architectural facts (PredicatePolicy SoT, MCP::Tools 6-handler split, Recall facade structure, SQLiteStore mixin pattern, Embeddings::DimensionCheck) **don't appear in the top 31**.
127
+
128
+ ### Generated snapshot (`.claude/rules/claude_memory.generated.md`) — Grade: B+
129
+ Auto-loads into every Claude Code session. Genuinely useful:
130
+ - 3 real decisions (MCP telemetry minimal columns, QMD restudy, claude-supermemory study), all with reason clauses.
131
+ - ~25 real project conventions covering the genuine gotchas.
132
+ - 6 architecture facts (PredicatePolicy SoT, MCP::Tools dispatcher, Recall facade, SQLiteStore mixins, pluggable Embeddings, DimensionCheck value object).
133
+
134
+ **But:** the "Technical Constraints" block says `uses_framework=rails`, `deployment_platform=aws` (both wrong), and the Open Conflicts list at the bottom is a 47-row tail that pushes useful content down.
135
+
136
+ ### Auto-memory files (`~/.claude/projects/-Users-…/memory/*.md`) — Grade: A
137
+ Separately maintained, never made it into the SQLite DB. Highest-quality knowledge in the system: SchemaVersion-in-tests, `upsert_content_item` requires `text_hash`+`byte_len`, FTS indexing pattern, hook context test isolation, WAL stale-cache phantom corruption, FTS5 rank corruption after `.recover`, scope_hint vs scope routing, round-trip migration specs. **Invisible to `memory.recall`.** Only surfaced transiently via `AutoMemoryMirror` at SessionStart.
138
+
139
+ ---
140
+
141
+ ## Part 4 — Root Causes
142
+
143
+ 1. **CLAUDE.md scope-system example is a hallucination factory.** It contains literal phrases ("this app uses PostgreSQL", "I prefer 4-space indentation") that Layer 1 and Layer 2 distillers extract as ground truth. Known open product gap (`feedback_hallucination_source_vs_cleanup.md`).
144
+ 2. **99-item distillation backlog.** SessionStart prompts for deep distillation but `mark_distilled` isn't being called for most items. Same text gets re-extracted across sessions, conflicts re-open.
145
+ 3. **Shortcut tools have inconsistent filters.** `memory.conventions` is hardcoded to global scope; `memory.decisions` aggregates across `decision`/`uses_*`/`reference` predicates; `memory.architecture` mixes scopes and predicates differently again. None correctly answer "what does this project actually look like?"
146
+ 4. **Single-cardinality predicates can't self-heal.** `uses_database`, `deployment_platform` keep flipping as new hallucinated facts arrive; resolver creates a new conflict each time and never closes the old ones.
147
+ 5. **Documentation drift propagates.** CLAUDE.md says "8 predicates", "25 MCP tools" — if a coding agent re-extracts from CLAUDE.md, these incorrect counts become persisted facts.
148
+ 6. **Auto-memory bypass.** The highest-quality knowledge (gotcha files) lives in markdown files outside the DB, so retrieval tools can't reach it.
149
+
150
+ ---
151
+
152
+ ## Part 5 — Remediation Pipeline
153
+
154
+ Four phases, lowest-risk highest-leverage first. Each phase has explicit done criteria and a verification step. The whole pipeline should be re-runnable: see Phase 4 for the systemic check.
155
+
156
+ ### Phase 1 — Stop the bleeding *(blocks further drift, ~30 min)*
157
+
158
+ | # | Action | Verification |
159
+ |---|---|---|
160
+ | 1.1 | Wrap CLAUDE.md scope-system example text in `<no-memory>` tags (around the "this app uses PostgreSQL" / "I prefer 4-space indentation" example block in the Scope System section). | Re-ingest CLAUDE.md, confirm no new `uses_database=postgresql` or `convention=4-space indentation` facts appear. |
161
+ | 1.2 | Update CLAUDE.md predicate count: "8 entries" → "9 entries (includes `reference`)". | grep "8 entries" returns 0 matches in CLAUDE.md. |
162
+ | 1.3 | Update CLAUDE.md MCP tool count: "25 tools total" → "23 tools total". | grep "25 tools" returns 0 matches in CLAUDE.md. |
163
+ | 1.4 | Update `.claude/rules/claude_memory.generated.md` regeneration path: run `claude-memory publish` after Phase 2 cleanup to refresh. | Generated file matches active facts. |
164
+
165
+ **Done criteria:** Source-text fix in place. No new hallucinations will be created from these specific triggers.
166
+
167
+ ### Phase 2 — Clean the noise floor *(reclaims trust in the DB, ~45 min)*
168
+
169
+ Bulk-reject the hallucinated facts. Order matters — reject leaves before clearing conflicts.
170
+
171
+ | # | Action | Command | Verification |
172
+ |---|---|---|---|
173
+ | 2.1 | Reject hallucinated `uses_framework` facts. Keep: none in production code (this gem has no framework dependency in the runtime sense). | `claude-memory reject 45 46 47 50 51 53 54 55 56 57 61 65 66 67 72 73 74 134 196 197 198` (rails, sinatra, react, express, next.js, django + tail of synonyms) | `memory.recall "uses_framework" scope=project` returns 0. |
174
+ | 2.2 | Reject hallucinated `uses_language` facts. Keep: `ruby` (id 76). Reject the rest. | `claude-memory reject 75 78 91 149 195` (javascript, go, python, typescript, rust) | `memory.recall "uses_language" scope=project` returns only ruby. |
175
+ | 2.3 | Reject hallucinated `uses_database` facts. Keep: `sqlite` (id 21). Reject the rest. | `claude-memory reject 62 63 139 148 154 155` (mysql, postgres, postgresql, redis) | `memory.recall "uses_database" scope=project` returns only sqlite. |
176
+ | 2.4 | Reject hallucinated `deployment_platform` facts. The gem has none — reject all. | `claude-memory reject 27 48 49 52 70` (azure, aws, gcp, vercel, docker) | `memory.recall "deployment_platform" scope=project` returns 0. |
177
+ | 2.5 | Dedupe global `convention` facts (Docker, tmux, iTerm2, VS Code duplicates). | `claude-memory reject 8 11 12 10` (keep ids 1, 6, 7, 5 from each duplicate pair) | `memory.conventions scope=global` returns ≤ 7 distinct facts. |
178
+ | 2.6 | Triage the 99-item distillation backlog. Either `/distill-transcripts` to deeply extract or bulk-call `memory.mark_distilled` to clear. | Use `memory.undistilled` to inspect, then run `/distill-transcripts` for items with potential, mark-distilled for the rest. | `memory.stats` pending_distillation < 10. |
179
+ | 2.7 | Conflicts close as a side effect of rejection (reject resolves the open conflict in the same transaction per `claude-memory reject` convention). | — | `memory.conflicts` returns 0. |
180
+
181
+ **Done criteria:** Project DB has ~25 high-signal active facts. Conflicts: 0. Pending distillation: < 10.
182
+
183
+ **Note:** The exact fact IDs above are from the 2026-05-21 snapshot. Re-query `memory.stats` and `memory.recall` before executing to confirm the IDs haven't shifted.
184
+
185
+ ### Phase 3 — Fix the system *(prevents recurrence, ~2-3 hours)*
186
+
187
+ | # | Action | Where | Verification |
188
+ |---|---|---|---|
189
+ | 3.1 | **Fix `memory.conventions` scope filter.** Default should return project conventions (with optional global merge), not global-only. | `lib/claude_memory/mcp/handlers/shortcut_handlers.rb` (convention shortcut) + `lib/claude_memory/mcp/tool_definitions.rb` (tool description). | New spec: `memory.conventions` on this repo returns the 28 project conventions, not just the 7 global ones. |
190
+ | 3.2 | **Fix `memory.decisions` predicate filter.** Should return only `decision`-predicate facts (with reason clauses), not `uses_*` rows. | `lib/claude_memory/mcp/handlers/shortcut_handlers.rb` (decision shortcut). | `memory.decisions` returns only facts where `predicate = 'decision'`. |
191
+ | 3.3 | **Fix `memory.architecture` predicate filter.** Should return only `architecture`-predicate facts, not `uses_*` aggregates. | `lib/claude_memory/mcp/handlers/shortcut_handlers.rb`. | `memory.architecture` returns only facts where `predicate = 'architecture'`. |
192
+ | 3.4 | **Migrate auto-memory `~/.claude/projects/.../memory/*.md` content into the project DB.** Each gotcha becomes a `convention` or `architecture` fact with full reason clause. Today it's only surfaced transiently via `AutoMemoryMirror`. | One-off script or a new `claude-memory import-auto-memory` command. | Top auto-memory gotchas (Configuration-instance-only, schema-version-in-tests, FTS indexing pattern, etc.) are reachable via `memory.recall`. |
193
+ | 3.5 | **Strengthen `ReferenceMaterialDetector`** so it catches the specific scope-system example phrasing if `<no-memory>` is ever removed. Add unit test: extracting from "this app uses PostgreSQL" inside a paragraph titled "Scope System" should be rejected as reference material. | `lib/claude_memory/distill/reference_material_detector.rb`. | New spec passes. |
194
+ | 3.6 | **Add `Resolve::Resolver` conflict auto-resolution heuristic** for single-cardinality predicates when one fact has reference-material signals and the other doesn't. | `lib/claude_memory/resolve/resolver.rb`. | New spec: conflict between authoritative SQLite fact and example-text Postgres fact auto-resolves in SQLite's favor. |
195
+
196
+ **Done criteria:** Shortcut tools return signal-only. Auto-memory gotchas are first-class facts. Future hallucinations are caught earlier.
197
+
198
+ ### Phase 4 — Verify & make systemic *(prevents regression, ~1 hour)*
199
+
200
+ | # | Action | Where | Verification |
201
+ |---|---|---|---|
202
+ | 4.1 | **Create `bin/memory-audit`** — script that runs `memory.stats`, lists active facts by predicate, flags `uses_*` outliers (e.g. multiple `uses_database` values active), reports open conflicts, reports pending distillation count, and exits non-zero if thresholds are exceeded. | `bin/memory-audit`. | Script run on a clean DB exits 0. Inject a hallucinated fact and confirm exit 1. |
203
+ | 4.2 | **Add a benchmark `spec/benchmarks/health/database_signal_spec.rb`** that codifies expected signal-to-noise ratio for this project: minimum conventions reachable via `memory.conventions`, maximum `uses_*` cardinality for single-cardinality predicates, zero open conflicts on main. | `spec/benchmarks/health/`. | `bundle exec rspec spec/benchmarks/health/` passes on a clean DB. |
204
+ | 4.3 | **Add a quarterly recurring task** (or schedule via the `schedule` skill) to re-run this audit and produce a dated `docs/memory_audit_<date>.md`. | `bin/run-evals --health` or a cron entry. | An audit is generated on schedule, diffed against the prior. |
205
+ | 4.4 | **Update `docs/api_stability.md`** to mention the audit script and the signal-health benchmark as part of pre-release verification. | `docs/api_stability.md`. | Mention is present. |
206
+
207
+ **Done criteria:** Future drift will be caught by a script, not by accident.
208
+
209
+ ---
210
+
211
+ ## Appendix A — Fact IDs to Reject (2026-05-21 snapshot)
212
+
213
+ Re-verify before executing; IDs may shift if other writes occur.
214
+
215
+ **`uses_framework` (reject all):** 45, 46, 47, 50, 51, 53, 54, 55, 56, 57, 61, 65, 66, 67, 72, 73, 74, 134, 196, 197, 198.
216
+
217
+ **`uses_language` (keep 76 ruby; reject):** 75, 78, 91, 149, 195.
218
+
219
+ **`uses_database` (keep 21 sqlite; reject):** 62, 63, 139, 148, 154, 155.
220
+
221
+ **`deployment_platform` (reject all):** 27, 48, 49, 52, 70.
222
+
223
+ **Global convention duplicates (reject):** 8, 10, 11, 12.
224
+
225
+ **Estimated cleanup result:** project active facts drop from 46 → ~25, all open conflicts close, generated snapshot's "Technical Constraints" block becomes accurate.
226
+
227
+ ## Appendix B — Out of Scope (intentional)
228
+
229
+ - Migrating to a different vector store. Not the bottleneck.
230
+ - Re-architecting the distillation pipeline. The three-layer design is sound; only inputs and shortcut filters are broken.
231
+ - Changing the dual-DB model. Working as intended.
232
+
233
+ ## Appendix C — Audit Methodology
234
+
235
+ 1. Repo audit via Explore subagent covering 16 areas (layout, layers, dual-DB, distillation, predicates, hooks, recall engine, embeddings, MCP, dashboard, telemetry, tests, conventions, recent changes, public API, contradictions).
236
+ 2. Memory database query via MCP tools: `memory.stats`, `memory.status`, `memory.decisions`, `memory.conventions`, `memory.architecture`, `memory.conflicts`, `memory.changes`, `memory.list_projects`.
237
+ 3. Cross-reference: for each documented claim, verify against code and grade tool output usefulness for a hypothetical coding agent dropped into the repo.
238
+
239
+ ---
240
+
241
+ *End of audit. Next snapshot: 2026-08-21 (quarterly) or after Phase 3 ships, whichever comes first.*
242
+
243
+ </no-memory>
244
+
245
+ <no-memory>
246
+
247
+ ## Pipeline Execution Results — 2026-05-21
248
+
249
+ All four phases executed in a single session. Final state:
250
+
251
+ | Metric | Before | After |
252
+ |---|---|---|
253
+ | Global active facts | 7 | 4 |
254
+ | Project active facts | 46 | 68 *(↑ via auto-memory import)* |
255
+ | Open conflicts | 10 | 0 |
256
+ | Pending distillation | 99 | 0 |
257
+ | Single-cardinality violations | 1+ | 0 |
258
+ | `uses_database` active | 4 contradictory | 1 (sqlite) |
259
+ | `deployment_platform` active | 1 hallucinated | 0 |
260
+ | `uses_framework` active | 2 hallucinated | 0 |
261
+ | `uses_language` active | 3 (1 real, 2 halluc.) | 1 (ruby) |
262
+
263
+ ### Phase 1 — Stop the bleeding (DONE)
264
+ - Wrapped CLAUDE.md scope-system example in `<no-memory>` tags.
265
+ - Fixed "25 tools" → "23 tools" drift in `CLAUDE.md`, `docs/api_stability.md`, `docs/plugin.md`.
266
+ - Wrapped this audit doc in `<no-memory>` so it doesn't self-contaminate on re-ingestion.
267
+
268
+ ### Phase 2 — Clean the noise floor (DONE)
269
+ - Rejected 9 hallucinated project facts plus 3 global duplicates (Docker / iTerm2 / tmux variants).
270
+ - Bulk-marked 99 backlog content items as distilled.
271
+ - All 10 open conflicts closed.
272
+
273
+ ### Phase 3 — Fix the system (DONE)
274
+ - **Shortcuts refactored** (`lib/claude_memory/shortcuts.rb`): switched from FTS text search to predicate-based filtering. `memory.conventions` now returns both project and global facts (was global-only); `memory.decisions` now returns only `decision`-predicate facts (was returning `uses_*` too); `memory.architecture` returns only architecture + stack-shaping predicates.
275
+ - **Tool descriptions updated** in `lib/claude_memory/mcp/tool_definitions.rb` to reflect new behavior.
276
+ - **AutoMemoryMirror slug bug** fixed (`tr("/", "-")` → `tr("/_", "-")`) — previously silently missed auto-memory for any project name with underscores, including `claude_memory` itself.
277
+ - **New CLI command** `claude-memory import-auto-memory [--dry-run]` migrates `~/.claude/projects/<slug>/memory/*.md` into the project DB as durable facts. Imported 27 gotchas/feedback/reference files.
278
+ - **ReferenceMaterialDetector strengthened** with `QUOTE_GUARDED_PREDICATES` and `EXAMPLE_QUOTE_PATTERNS`: stack predicates extracted from example-text quotes ('e.g., ...PostgreSQL') now reroute to `reference`.
279
+ - **Resolver discard heuristic** added: single-cardinality predicates with example-text quotes get silently dropped instead of creating disputed-fact + conflict rows. Catches the case where Layer-1 NullDistiller bypasses ReferenceMaterialDetector.
280
+ - Specs added/updated for each (50 examples across `shortcuts_spec.rb`, `reference_material_detector_spec.rb`, `resolver_spec.rb`). Full suite: **2121 passing, 0 failures**.
281
+
282
+ ### Phase 4 — Verify & make systemic (DONE)
283
+ - **`bin/memory-audit`** script writes a stable JSON shape (`--json`) and exits non-zero on threshold breach. Hard fails on: any open conflict, >1 active fact per single-cardinality predicate, ≥ 100 pending distillation. Warns on ≥ 25 pending.
284
+ - **`spec/benchmarks/health/database_signal_spec.rb`** codifies the contracts as a `:benchmark`-tagged RSpec suite. Runs against the live DB. 10 examples passing.
285
+ - **`docs/api_stability.md` Section 7** documents the audit script's stable surface and the benchmark spec; corrected schema version from v17 → v18 (8 → 18 migrations).
286
+
287
+ ### Re-running this pipeline
288
+
289
+ If this audit goes stale (drift creeps back in), re-run from the top:
290
+
291
+ ```bash
292
+ bin/memory-audit # see current state
293
+ bundle exec rspec spec/benchmarks/health/ --tag benchmark # confirm contracts
294
+ ./exe/claude-memory import-auto-memory --dry-run # preview new auto-memory
295
+ ```
296
+
297
+ Failure modes the audit will catch:
298
+ - A new contamination source landing in CLAUDE.md or another auto-ingested file.
299
+ - A shortcut handler losing predicate-filter semantics in a refactor.
300
+ - A resolver bug re-introducing single-cardinality conflicts.
301
+ - An undistilled-content backlog from a high-traffic week.
302
+
303
+ </no-memory>
data/docs/plugin.md CHANGED
@@ -133,7 +133,7 @@ Unlike traditional approaches that require a separate API key, ClaudeMemory uses
133
133
 
134
134
  ### MCP Server
135
135
 
136
- The plugin exposes 25 tools to Claude. Highlights:
136
+ The plugin exposes 23 tools to Claude. Highlights:
137
137
 
138
138
  | Tool | Description |
139
139
  |------|-------------|
@@ -9,6 +9,41 @@
9
9
 
10
10
  ---
11
11
 
12
+ ## Post-0.11 Investigation: Hallucination Rate Metric Calibration (2026-04-30)
13
+
14
+ When #48 (hallucination-rate metric) was first run against this project's real DB, it surfaced numbers that *looked* alarming:
15
+
16
+ - Quality score: 39/100
17
+ - Bare conclusions: 34 / 59 active facts (57.6%)
18
+ - 7-day rejection rate: 27 of 32 facts (84.4%)
19
+
20
+ The first read was that the LLM extractor was producing noise faster than usable knowledge. Per `improvements.md` #60, four causes were proposed; diagnostics ran 2026-04-30:
21
+
22
+ | Cause | Verdict | Evidence |
23
+ |---|---|---|
24
+ | Prompt drift in `distill-transcripts.md` | **Confirmed dominant** | 34/35 (97%) bare-conclusion facts pre-date the reason-clause prompt commit `f22d12f` (2026-04-20). Only 1 was created post-commit (and that one is a meta-convention added during this session). |
25
+ | Auto-memory mirror regurgitation | Rejected | 0/35 substring matches in `~/.claude/projects/.../memory/*.md`. Auto-memory mirror only landed in 0.10.0 (2026-04-28), after the bare-fact creation window — temporally impossible to be the source. |
26
+ | `ReferenceMaterialDetector` predicate scope too narrow | Not material | Only 3/35 bare facts are `decision`-predicate; 0 of those match the strong reference-material patterns. Expanding `GUARDED_PREDICATES` would not move the needle on the bare-conclusion count. |
27
+ | Junky corpus / rejection cluster | **Confirmed in single class** | All 27 rejected facts in the 7-day window are `uses_database` (18) or `deployment_platform` (9), all with `session_id=nil` (MCP-originated, almost certainly `/study-repo` runs misattributing external-project tech to this project), all from 2026-04-23 to 04-24. Systemic single-class failure, correctly cleaned up after detection — not ongoing extraction noise. |
28
+
29
+ **What this means for #48 as currently shipped:**
30
+
31
+ The metric is *technically correct* but *pragmatically misleading*. It bakes historical noise (pre-prompt-commit bare conclusions) into a signal that users will read as "ongoing extraction quality." A 57.6% bare-conclusion rate looks like the LLM is broken; in reality the live extraction rate (post-2026-04-20) is ~3% (1 bare fact out of ~30+ created since the prompt commit landed).
32
+
33
+ The 84% rejection rate has a similar structural issue: it counts cleanup of a bursty `/study-repo` regression against the active-facts denominator, not against the actual extraction quality of the live window.
34
+
35
+ **Quick fix shipping now (this session):** restrict `quality_score` and the digest's "Quality" section to facts created within the same 30-day window already used by `token_budget`. Surface a separate "historical" line so users can see both numbers, but the headline is the live one. This makes the metric actionable: a high live bare-conclusion rate = live LLM calibration drift; a high historical rate = legacy data, not a current alarm.
36
+
37
+ **Deferred to 0.12 / 1.x:**
38
+
39
+ 1. The systemic `/study-repo` misattribution failure mode (cause 4) deserves its own guard. External-project READMEs being studied should land in `reference` predicates, not as `uses_database`/`deployment_platform`. Track this as a follow-up entry.
40
+ 2. A backfill/cleanup pass on the 34 historical bare-conclusion facts: either retroactive rejection, or a one-shot reclassification that moves them to a `legacy_observation` predicate that the prompt's reason-clause requirement doesn't apply to.
41
+ 3. The metric's calibration assumes "bare conclusion = bad", but spot-checking shows several flagged facts are perfectly informative ("MCP tools return dual content + structuredContent via TextSummary module") — they describe mechanics implicitly. The vocabulary may itself be too strict; revisit during 1.0 soak with real usage data.
42
+
43
+ **Process win:** the metric did its job — it surfaced a real signal that would otherwise have stayed invisible, and the investigation distinguished historical noise from live calibration. Without #48 we'd have no way to know.
44
+
45
+ ---
46
+
12
47
  ## Executive Summary
13
48
 
14
49
  Six days, +2,011 LOC. The headline finding: **the watch-list item from 2026-04-22 (#28 — extract per-endpoint helpers from `Dashboard::API`) was not just deferred, it actively regressed.** `dashboard/api.rb` grew from 627 → 807 LOC (+180, +29%), is now the only file in `lib/` over 750 lines, and gained four new methods all exceeding 15 lines. Method-size pressure increased: the previous worst case (`recall` at 39 lines) is now `timeline` at 52 lines, and the file has 11 methods over 15 lines (vs 11 last review) but with a higher mean.
@@ -0,0 +1,239 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClaudeMemory
4
+ module Audit
5
+ # Individual audit checks. Each method takes a Store::StoreManager and
6
+ # returns an Array<Finding>. Checks must be read-only — write
7
+ # operations belong in dedicated commands the user opts into.
8
+ #
9
+ # Adding a new check:
10
+ # 1. Define a method here with an explicit C### id assignment.
11
+ # 2. Append the method name to Runner::CHECK_METHODS.
12
+ # 3. Document it in docs/audit_runbook.md.
13
+ module Checks
14
+ module_function
15
+
16
+ # C001 — Open conflicts in either DB.
17
+ def open_conflicts(manager)
18
+ findings = []
19
+ {project: manager.store_if_exists("project"), global: manager.store_if_exists("global")}.each do |scope, store|
20
+ next unless store
21
+ conflicts = store.open_conflicts
22
+ next if conflicts.empty?
23
+ findings << Finding.new(
24
+ id: "C001",
25
+ severity: :error,
26
+ title: "#{conflicts.size} open conflict(s) in #{scope} DB",
27
+ detail: "Open conflicts indicate unresolved single-cardinality disputes. Each will keep re-firing until the losing fact is rejected.",
28
+ suggestion: "claude-memory conflicts && claude-memory reject <fact_id>",
29
+ fact_ids: conflicts.flat_map { |c| [c[:fact_a_id], c[:fact_b_id]] }.uniq
30
+ )
31
+ end
32
+ findings
33
+ end
34
+
35
+ # C002 — Single-cardinality predicates with > 1 active fact.
36
+ SINGLE_CARDINALITY_PREDICATES = %w[uses_database deployment_platform auth_method].freeze
37
+
38
+ def single_cardinality_multiplicity(manager)
39
+ store = manager.store_if_exists("project")
40
+ return [] unless store
41
+
42
+ SINGLE_CARDINALITY_PREDICATES.flat_map do |predicate|
43
+ rows = store.facts.where(status: "active", predicate: predicate).all
44
+ next [] if rows.size <= 1
45
+ [Finding.new(
46
+ id: "C002",
47
+ severity: :error,
48
+ title: "predicate=#{predicate} has #{rows.size} active facts (single-cardinality)",
49
+ detail: "Single-cardinality predicates must have at most one active value. Multiple actives mean resolver dropped a supersession or distillation produced contradictory claims.",
50
+ suggestion: "Inspect with: claude-memory explain <fact_id>. Reject the wrong ones: claude-memory reject <fact_id>",
51
+ fact_ids: rows.map { |r| r[:id] }
52
+ )]
53
+ end
54
+ end
55
+
56
+ # C003 — Distillation backlog (warn ≥ 25, error ≥ 100).
57
+ def distillation_backlog(manager)
58
+ store = manager.store_if_exists("project")
59
+ return [] unless store
60
+ distilled_ids = store.ingestion_metrics.select(:content_item_id).distinct
61
+ pending = store.content_items.exclude(id: distilled_ids).count
62
+ return [] if pending < 25
63
+
64
+ severity = (pending >= 100) ? :error : :warn
65
+ [Finding.new(
66
+ id: "C003",
67
+ severity: severity,
68
+ title: "#{pending} content items not yet deeply distilled",
69
+ detail: "Backlog grows when SessionStart distillation prompts aren't acknowledged with memory.mark_distilled. A large backlog means the same text gets re-extracted across sessions, increasing hallucination rate.",
70
+ suggestion: "Triage with /distill-transcripts (interactive) OR mark all distilled if you accept the backlog is noise: claude-memory sweep --mark-all-distilled",
71
+ fact_ids: []
72
+ )]
73
+ end
74
+
75
+ # C004 — memory.decisions leaking non-decision predicates.
76
+ def shortcut_decision_leak(manager)
77
+ results = Shortcuts.decisions(manager, limit: 50)
78
+ leaked = results.map { |r| r[:fact][:predicate] }.uniq - ["decision"]
79
+ return [] if leaked.empty?
80
+
81
+ [Finding.new(
82
+ id: "C004",
83
+ severity: :error,
84
+ title: "memory.decisions returns non-decision predicates: #{leaked.inspect}",
85
+ detail: "memory.decisions should return only `decision`-predicate facts. Predicate leakage suggests the shortcut implementation has regressed back to text-search filtering (pre-2026-05-21 audit).",
86
+ suggestion: "Inspect lib/claude_memory/shortcuts.rb — SHORTCUTS[:decisions][:predicates] should equal ['decision']. Run `bundle exec rspec spec/claude_memory/shortcuts_spec.rb`.",
87
+ fact_ids: results.select { |r| leaked.include?(r[:fact][:predicate]) }.map { |r| r[:fact][:id] }
88
+ )]
89
+ end
90
+
91
+ # C005 — memory.conventions returns no project facts despite project conventions existing.
92
+ def shortcut_convention_scope(manager)
93
+ project_store = manager.store_if_exists("project")
94
+ return [] unless project_store
95
+ project_count = project_store.facts.where(status: "active", predicate: "convention").count
96
+ return [] if project_count.zero?
97
+
98
+ results = Shortcuts.conventions(manager, limit: 50)
99
+ project_returned = results.count { |r| r[:source] == "project" }
100
+ return [] if project_returned > 0
101
+
102
+ [Finding.new(
103
+ id: "C005",
104
+ severity: :warn,
105
+ title: "memory.conventions returned 0 project facts despite #{project_count} project conventions existing",
106
+ detail: "Pre-2026-05-21 audit, memory.conventions was hardcoded to scope=global. If you're seeing 0 project facts in a project with conventions, the shortcut has regressed.",
107
+ suggestion: "Check Shortcuts.collect_facts in lib/claude_memory/shortcuts.rb. Re-run `bundle exec rspec spec/claude_memory/shortcuts_spec.rb`.",
108
+ fact_ids: []
109
+ )]
110
+ end
111
+
112
+ # C006 — Duplicate global convention candidates (near-identical text).
113
+ def duplicate_global_conventions(manager)
114
+ store = manager.store_if_exists("global")
115
+ return [] unless store
116
+ rows = store.facts.where(status: "active", predicate: "convention").select(:id, :object_literal).all
117
+ return [] if rows.size < 2
118
+
119
+ # Group by normalized object text (lowercased, stripped of leading
120
+ # "uses"/"prefers"/punctuation). Pairs with the same normalized
121
+ # key are likely near-duplicates.
122
+ groups = rows.group_by { |r| normalize_convention(r[:object_literal]) }
123
+ dupe_groups = groups.select { |_, list| list.size > 1 }
124
+ return [] if dupe_groups.empty?
125
+
126
+ [Finding.new(
127
+ id: "C006",
128
+ severity: :info,
129
+ title: "#{dupe_groups.size} near-duplicate global convention group(s)",
130
+ detail: "Multiple global conventions normalize to the same phrasing. Pick the cleanest and reject the rest to keep memory.conventions output tight.",
131
+ suggestion: "Review with: claude-memory recall <concept> --scope=global. Reject duplicates: claude-memory reject <fact_id>",
132
+ fact_ids: dupe_groups.values.flatten.map { |r| r[:id] }
133
+ )]
134
+ end
135
+
136
+ # C007 — Bare-conclusion decisions/conventions (no reason clause).
137
+ def bare_conclusion_rate(manager)
138
+ store = manager.store_if_exists("project")
139
+ return [] unless store
140
+ detector = Distill::BareConclusionDetector.new
141
+ rows = store.facts.where(status: "active", predicate: %w[decision convention]).select(:id, :predicate, :object_literal).all
142
+ bare = rows.select { |r| detector.bare_conclusion?(predicate: r[:predicate], object_literal: r[:object_literal]) }
143
+ return [] if bare.empty?
144
+
145
+ ratio = bare.size.to_f / rows.size
146
+ return [] if ratio < 0.3
147
+
148
+ [Finding.new(
149
+ id: "C007",
150
+ severity: :info,
151
+ title: "#{(ratio * 100).round}% of decisions/conventions lack reason clauses (#{bare.size}/#{rows.size})",
152
+ detail: "Facts without 'because/so that/to avoid/...' lose their justification once context fades. Bare conclusions are dead weight when the team grows or you revisit a year later.",
153
+ suggestion: "Inspect with: claude-memory explain <fact_id>. Reject low-value bare facts or rewrite with reason clauses via memory.store_extraction.",
154
+ fact_ids: bare.map { |r| r[:id] }
155
+ )]
156
+ end
157
+
158
+ # C008 — Project DB starvation (< 5 active facts may indicate broken ingest).
159
+ def project_starvation(manager)
160
+ store = manager.store_if_exists("project")
161
+ return [] unless store
162
+ count = store.facts.where(status: "active").count
163
+ return [] if count >= 5
164
+
165
+ [Finding.new(
166
+ id: "C008",
167
+ severity: :warn,
168
+ title: "Only #{count} active project fact(s)",
169
+ detail: "A nearly-empty project DB suggests either a fresh install (ignore) OR a broken ingest pipeline / overzealous rejection. Verify hooks are firing: claude-memory doctor.",
170
+ suggestion: "claude-memory doctor; claude-memory stats; check .claude/settings.json hook configuration.",
171
+ fact_ids: []
172
+ )]
173
+ end
174
+
175
+ # C009 — Auto-memory drift (markdown files newer than project DB facts).
176
+ def auto_memory_unimported(manager)
177
+ config = Configuration.new
178
+ dir = Hook::AutoMemoryMirror.default_dir(config.project_dir, config.claude_config_dir)
179
+ return [] unless Dir.exist?(dir)
180
+
181
+ md_files = Dir.glob(File.join(dir, "*.md")).reject { |f| File.basename(f) == "MEMORY.md" }
182
+ return [] if md_files.empty?
183
+
184
+ store = manager.store_if_exists("project")
185
+ return [] unless store
186
+
187
+ # Look for auto_memory_import content items as evidence of prior
188
+ # import. Count files that would be new on the next import.
189
+ imported_count = store.content_items.where(source: "auto_memory_import").count
190
+ net_new = md_files.size - imported_count
191
+ return [] if net_new <= 0
192
+
193
+ [Finding.new(
194
+ id: "C009",
195
+ severity: :info,
196
+ title: "#{net_new} auto-memory file(s) not yet imported",
197
+ detail: "~/.claude/projects/<slug>/memory/*.md files contain durable knowledge that isn't reachable via memory.recall until imported. AutoMemoryMirror only surfaces them transiently at SessionStart.",
198
+ suggestion: "Preview: claude-memory import-auto-memory --dry-run. Import: claude-memory import-auto-memory.",
199
+ fact_ids: []
200
+ )]
201
+ end
202
+
203
+ # C010 — Recurring single-cardinality churn (history shows the same
204
+ # predicate has accumulated many superseded/disputed facts — sign of
205
+ # a persistent contamination source).
206
+ CHURN_THRESHOLD = 5
207
+
208
+ def single_cardinality_churn(manager)
209
+ store = manager.store_if_exists("project")
210
+ return [] unless store
211
+
212
+ SINGLE_CARDINALITY_PREDICATES.flat_map do |predicate|
213
+ non_active = store.facts
214
+ .where(predicate: predicate, status: %w[superseded disputed rejected])
215
+ .count
216
+ next [] if non_active < CHURN_THRESHOLD
217
+
218
+ [Finding.new(
219
+ id: "C010",
220
+ severity: :warn,
221
+ title: "predicate=#{predicate} shows churn: #{non_active} historical non-active facts",
222
+ detail: "Repeated supersession/dispute on a single-cardinality predicate usually means a contamination source (e.g., example text in CLAUDE.md or docs) keeps re-introducing the same hallucination.",
223
+ suggestion: "Find the contamination source: claude-memory recall <bad_value> --scope=project. Wrap the trigger text in <no-memory> tags. See docs/audit_runbook.md.",
224
+ fact_ids: []
225
+ )]
226
+ end
227
+ end
228
+
229
+ def normalize_convention(text)
230
+ text.to_s
231
+ .downcase
232
+ .gsub(/\b(?:uses|prefers|always|never)\b/, "")
233
+ .gsub(/[[:punct:]]/, "")
234
+ .gsub(/\s+/, " ")
235
+ .strip
236
+ end
237
+ end
238
+ end
239
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClaudeMemory
4
+ module Audit
5
+ # A single audit finding. Immutable value object emitted by checks
6
+ # (see Audit::Checks) and aggregated by Audit::Runner.
7
+ #
8
+ # Severity levels:
9
+ # - :error — a contract violation; CI/automation should fail
10
+ # - :warn — likely problem requiring attention but not blocking
11
+ # - :info — observation; suggests an optimization or cleanup
12
+ #
13
+ # Each finding embeds the suggested remediation command(s) as plain
14
+ # strings so the audit output is directly actionable. The skill
15
+ # `/audit-memory` reads these and offers to run them for the user.
16
+ Finding = Data.define(:id, :severity, :title, :detail, :suggestion, :fact_ids) do
17
+ def error? = severity == :error
18
+ def warn? = severity == :warn
19
+ def info? = severity == :info
20
+
21
+ def to_h
22
+ {
23
+ id: id,
24
+ severity: severity,
25
+ title: title,
26
+ detail: detail,
27
+ suggestion: suggestion,
28
+ fact_ids: fact_ids
29
+ }
30
+ end
31
+ end
32
+ end
33
+ end