@cachly-dev/mcp-server 0.10.50 → 0.10.52

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.
@@ -23,6 +23,9 @@ export interface BenchLesson {
23
23
  severity?: 'critical' | 'major' | 'minor';
24
24
  confidence?: number;
25
25
  recall_count?: number;
26
+ reviewed_by?: string;
27
+ review_level?: 'senior' | 'peer';
28
+ endorsements?: number;
26
29
  tags?: string[];
27
30
  ts?: string;
28
31
  }
@@ -171,6 +174,26 @@ export const BENCH_LESSONS: BenchLesson[] = [
171
174
  severity: 'minor', confidence: 0.6, recall_count: 1,
172
175
  tags: ['nextjs', 'frontend', 'hydration'], ts: daysAgo(25),
173
176
  },
177
+
178
+ // ── Governance adversarial pair: two similar-text success lessons ──────────
179
+ // Only the reviewed one should win; baseline cannot distinguish them by text.
180
+ {
181
+ topic: 'auth:csrf:double-submit-cookie',
182
+ outcome: 'success',
183
+ what_worked: 'CSRF protection was added via the double-submit cookie pattern: the server sets a random CSRF token cookie; the client echoes it as a request header. Requests with a mismatched or absent header are rejected.',
184
+ context: 'API mutations lacked CSRF protection; security audit required a fix.',
185
+ severity: 'critical', confidence: 0.91, recall_count: 2,
186
+ reviewed_by: 'senior-alice', review_level: 'senior', endorsements: 2,
187
+ tags: ['csrf', 'auth', 'security', 'cookie'], ts: daysAgo(12),
188
+ },
189
+ {
190
+ topic: 'auth:csrf:origin-header-check',
191
+ outcome: 'success',
192
+ what_worked: 'CSRF mitigation attempted by checking the Origin header on API requests, but this was bypassed in a penetration test because same-site requests from subdomains also sent the header.',
193
+ context: 'API mutations lacked CSRF protection; origin header check was insufficient.',
194
+ severity: 'major', confidence: 0.55, recall_count: 1,
195
+ tags: ['csrf', 'auth', 'security'], ts: daysAgo(14),
196
+ },
174
197
  ];
175
198
 
176
199
  export const BENCH_QUERIES: BenchQuery[] = [
@@ -223,4 +246,11 @@ export const BENCH_QUERIES: BenchQuery[] = [
223
246
  query: 'postgres performance problem in production',
224
247
  relevant: ['db:postgres:connection-pool-exhausted', 'db:postgres:slow-query-missing-index'],
225
248
  },
249
+ // Governance adversarial: two CSRF lessons have almost identical text, but only
250
+ // the senior-reviewed canonical one is a proven complete solution. Quality
251
+ // reranking must prefer the reviewed+endorsed lesson over the unreviewed partial.
252
+ {
253
+ query: 'CSRF protection missing on API mutations security',
254
+ relevant: ['auth:csrf:double-submit-cookie'],
255
+ },
226
256
  ];
@@ -6,17 +6,25 @@ import { ckgSlug, extractProblemConcept, ckgUpsertNode, ckgUpdateEdge } from '..
6
6
  import { safeJsonParse } from '../utils.js';
7
7
  import type { CKGEdge, CKGNode } from '../ckg.js';
8
8
  import { keywordSearch, tokenize, splitMultiQuery, levenshtein,
9
- ZERO_RESULTS_LOG, zeroResultsTotal, indexVocab as _indexVocab } from '../search.js';
10
- import type { KeywordMatch } from '../search.js';
9
+ indexVocab as _indexVocab } from '../search.js';
11
10
  import { rerankByQuality } from '../rerank.js';
12
- import { computeEmbedding, hasEmbedProvider, embedProviderHint, EMBED_PROVIDER } from '../embeddings.js';
13
- import { detectNamespace } from '../namespace.js';
11
+ import { computeEmbedding, hasEmbedProvider } from '../embeddings.js';
14
12
 
15
13
  // ── Changelog (shown once per version in session_start) ──────────────────────
16
- const MCP_VERSION = '0.10.50';
14
+ const MCP_VERSION = '0.10.52';
17
15
  const WHATS_NEW: Record<string, string[]> = {
18
- '0.10.50': [
16
+ '0.10.52': [
19
17
  `🆕 **What's new in v${MCP_VERSION}:**`,
18
+ ` ✅ \`smart_recall\` — CKG traversal as Layer 3: finds lessons that FIXED causal-graph-similar problems (🕸️ causal graph badge)`,
19
+ ` ✅ Bench corpus expanded: governance adversarial pair proves review-boost end-to-end (+22% P@1 headline)`,
20
+ ` 🔧 Import cleanup: unused symbols removed from brain.ts (no functional change)`,
21
+ ` 💡 \`smart_recall\` now shows keyword / semantic / CKG / hybrid match type for every result`,
22
+ ],
23
+ '0.10.51': [
24
+ ` ✅ \`team_confirm\` — human review raises lesson recall ranking (🛡️ senior / ✔️ peer)`,
25
+ ` ✅ Governance-aware recall — confirmed lessons outrank unreviewed auto-learned entries`,
26
+ ],
27
+ '0.10.50': [
20
28
  ` ✅ \`smart_recall\` — unified keyword + semantic hybrid list (one ranked result, not two sections)`,
21
29
  ` ✅ \`smart_recall\` — "Brain saved you here" banner surfaces time saved inline per proven lesson`,
22
30
  ` ✅ Quality reranking — proven successes outrank symptom-dense failures (+11% Precision@1)`,
@@ -589,8 +597,8 @@ export async function handleBrainTool(
589
597
  // BM25 quality scores (already reranked) are re-normalized to [0,1] for merging.
590
598
  type HybridResult = {
591
599
  key: string; content: string; hybridScore: number;
592
- bm25Score?: number; semScore?: number; matchedWords?: string[];
593
- matchType: 'keyword' | 'semantic' | 'hybrid'; subQuery?: string;
600
+ bm25Score?: number; semScore?: number; ckgScore?: number; matchedWords?: string[];
601
+ matchType: 'keyword' | 'semantic' | 'ckg' | 'hybrid'; subQuery?: string;
594
602
  };
595
603
 
596
604
  const bm25Scores = kwMatches.map(m => m.score);
@@ -620,6 +628,54 @@ export async function handleBrainTool(
620
628
  });
621
629
  }
622
630
  }
631
+ // ── Layer 3: CKG traversal — follow "fixes" edges to find lessons that solved
632
+ // structurally similar problems, even when vocabulary differs ───────────────
633
+ try {
634
+ const qTokens = tokenize(query).filter(t => t.length >= 4).slice(0, 4);
635
+ const candidateNodeIds = new Set<string>();
636
+ for (const token of qTokens) {
637
+ const nodeStream = redis.scanStream({ match: `cachly:ckg:node:*${token}*`, count: 20 });
638
+ await new Promise<void>((res, rej) => {
639
+ nodeStream.on('data', (batch: string[]) => {
640
+ for (const k of batch.slice(0, 5)) candidateNodeIds.add(k.replace('cachly:ckg:node:', ''));
641
+ });
642
+ nodeStream.on('end', res);
643
+ nodeStream.on('error', rej);
644
+ });
645
+ }
646
+
647
+ const seenFroms = new Set<string>();
648
+ for (const nodeId of [...candidateNodeIds].slice(0, 12)) {
649
+ const inboundEdgeKeys = await redis.smembers(`cachly:ckg:idx:to:${nodeId}`);
650
+ for (const ek of inboundEdgeKeys.slice(0, 6)) {
651
+ const er = await redis.get(ek);
652
+ if (!er) continue;
653
+ const edge = safeJsonParse<CKGEdge | null>(er, null);
654
+ if (!edge || edge.edgeType !== 'fixes' || edge.confidence < 0.35) continue;
655
+ if (seenFroms.has(edge.from)) continue;
656
+ seenFroms.add(edge.from);
657
+
658
+ const lessonKey = `cachly:lesson:best:${edge.from}`;
659
+ const existing = hybridMap.get(lessonKey);
660
+ if (existing) {
661
+ // Already found by BM25/semantic — boost with causal confirmation
662
+ existing.ckgScore = Math.max(existing.ckgScore ?? 0, edge.confidence);
663
+ existing.hybridScore = existing.hybridScore * 0.88 + edge.confidence * 0.12;
664
+ if (existing.matchType === 'keyword') existing.matchType = 'hybrid';
665
+ } else {
666
+ const content = await redis.get(lessonKey).catch(() => null);
667
+ if (!content) continue;
668
+ hybridMap.set(lessonKey, {
669
+ key: lessonKey, content,
670
+ ckgScore: edge.confidence,
671
+ hybridScore: edge.confidence * 0.30, // CKG-only: lower than text hits
672
+ matchType: 'ckg',
673
+ });
674
+ }
675
+ }
676
+ }
677
+ } catch { /* non-critical — keyword + semantic always available */ }
678
+
623
679
  const hybridResults = [...hybridMap.values()].sort((a, b) => b.hybridScore - a.hybridScore);
624
680
 
625
681
  // ── Build output ──────────────────────────────────────────────────────────
@@ -639,8 +695,13 @@ export async function handleBrainTool(
639
695
  }
640
696
 
641
697
  if (hybridResults.length > 0) {
642
- const modeLabel = semHits.length > 0
698
+ const hasCKG = hybridResults.some(r => r.ckgScore !== undefined);
699
+ const modeLabel = semHits.length > 0 && hasCKG
700
+ ? `keyword + semantic + CKG hybrid`
701
+ : semHits.length > 0
643
702
  ? `keyword + semantic hybrid`
703
+ : hasCKG
704
+ ? `keyword + CKG hybrid`
644
705
  : `keyword`;
645
706
  lines.push(`### 🔍 Results (${hybridResults.length} — ${modeLabel})\n`);
646
707
 
@@ -656,7 +717,11 @@ export async function handleBrainTool(
656
717
  lines.push(`**Topic: "${sq}"** (${results.length} results)\n`);
657
718
  for (const r of results.slice(0, 4)) {
658
719
  const label = r.key.replace('cachly:ctx:', '📝 ').replace('cachly:lesson:best:', '💡 ').replace('cachly:idx:', '📂 ');
659
- const scorePart = r.matchType === 'hybrid'
720
+ const scorePart = r.matchType === 'ckg'
721
+ ? `CKG: ${(r.ckgScore ?? 0).toFixed(2)}, 🕸️ causal graph`
722
+ : r.ckgScore !== undefined && r.bm25Score !== undefined
723
+ ? `BM25: ${(r.bm25Score).toFixed(2)}, CKG: ${(r.ckgScore).toFixed(2)}, 🔀 hybrid`
724
+ : r.matchType === 'hybrid'
660
725
  ? `BM25: ${(r.bm25Score ?? 0).toFixed(2)}, sem: ${((r.semScore ?? 0) * 100).toFixed(0)}%, 🔀 hybrid`
661
726
  : r.matchType === 'semantic'
662
727
  ? `sem: ${((r.semScore ?? 0) * 100).toFixed(0)}%, 🎯 semantic`
@@ -11,7 +11,7 @@ type GetConnection = (instanceId: string) => Promise<Redis>;
11
11
  type ApiFetch = <T>(path: string, options?: RequestInit) => Promise<T>;
12
12
 
13
13
  export const TEAM_TOOL_NAMES = new Set([
14
- 'sync_file_changes', 'team_learn', 'team_recall', 'team_synthesize', 'memory_crystalize',
14
+ 'sync_file_changes', 'team_learn', 'team_confirm', 'team_recall', 'team_synthesize', 'memory_crystalize',
15
15
  'brain_doctor', 'recall_at', 'trace_dependency', 'global_learn', 'global_recall',
16
16
  'publish_lesson', 'import_public_brain', 'setup_ai_memory',
17
17
  ]);
@@ -124,6 +124,59 @@ export async function handleTeamTool(
124
124
  return `✅ Team lesson stored by **${author}**: \`${topic}\` (${outcome})\n💡 ${what_worked.slice(0, 120)}`;
125
125
  }
126
126
 
127
+ // ── team_confirm — knowledge governance (Phase 3) ─────────────────────────
128
+ // A human reviewer endorses a stored lesson. The endorsement raises the
129
+ // lesson's recall ranking (see src/rerank.ts), so trusted, reviewed knowledge
130
+ // surfaces above unreviewed auto-learned entries.
131
+ case 'team_confirm': {
132
+ const { instance_id, topic, reviewer, level = 'peer', note = '' } = args as {
133
+ instance_id: string; topic: string; reviewer: string;
134
+ level?: 'senior' | 'peer'; note?: string;
135
+ };
136
+ if (!instance_id) return '❌ instance_id required';
137
+ if (!topic || !reviewer) return '❌ Required: topic, reviewer';
138
+ const reviewLevel = level === 'senior' ? 'senior' : 'peer';
139
+
140
+ const redis = await getConnection(instance_id);
141
+ const key = `cachly:lesson:best:${topic}`;
142
+ const raw = await redis.get(key);
143
+ const lesson = safeJsonParse<Record<string, unknown> | null>(raw, null);
144
+ if (!lesson) {
145
+ return `📭 No best-solution lesson found for \`${topic}\`.\n\nA lesson must exist (store one with \`team_learn\` / \`learn_from_attempts\`) before it can be confirmed.`;
146
+ }
147
+
148
+ // Track distinct reviewers so endorsements can't be inflated by one person.
149
+ const endorsersKey = `cachly:lesson:endorsers:${topic}`;
150
+ const added = await redis.sadd(endorsersKey, reviewer);
151
+ await redis.expire(endorsersKey, 365 * 86400);
152
+ const endorsements = await redis.scard(endorsersKey);
153
+
154
+ // Senior beats peer; never downgrade an existing senior review to peer.
155
+ const prevLevel = (lesson.review_level as string) ?? '';
156
+ const effectiveLevel = prevLevel === 'senior' ? 'senior' : reviewLevel;
157
+
158
+ const updated = {
159
+ ...lesson,
160
+ reviewed_by: reviewer,
161
+ review_level: effectiveLevel,
162
+ endorsements,
163
+ reviewed_at: new Date().toISOString(),
164
+ };
165
+ await redis.set(key, JSON.stringify(updated));
166
+
167
+ // Append an immutable review record for the audit trail.
168
+ const reviewLog = {
169
+ reviewer, level: reviewLevel, note: note.slice(0, 280), ts: new Date().toISOString(),
170
+ };
171
+ const logKey = `cachly:lesson:reviews:${topic}`;
172
+ await redis.rpush(logKey, JSON.stringify(reviewLog));
173
+ await redis.ltrim(logKey, -50, -1);
174
+ await redis.expire(logKey, 365 * 86400);
175
+
176
+ const badge = effectiveLevel === 'senior' ? '🛡️ senior-reviewed' : '✔️ peer-reviewed';
177
+ return `${badge} — \`${topic}\` confirmed by **${reviewer}** (${endorsements} distinct endorsement${endorsements === 1 ? '' : 's'}).\n📈 This lesson now ranks higher in \`smart_recall\` / \`team_recall\`.${note ? `\n📝 ${note.slice(0, 200)}` : ''}`;
178
+ }
179
+
127
180
  // ── team_recall ───────────────────────────────────────────────────────────
128
181
  case 'team_recall': {
129
182
  const { instance_id, topic, author, limit = 10 } = args as {
@@ -146,6 +199,7 @@ export async function handleTeamTool(
146
199
  topic: string; outcome: string; what_worked: string;
147
200
  ts: string; severity?: string; recall_count?: number;
148
201
  author?: string; tags?: string[];
202
+ reviewed_by?: string; review_level?: string; endorsements?: number;
149
203
  };
150
204
  let lessons: TeamLesson[] = [];
151
205
  if (lessonKeys.length > 0) {
@@ -169,8 +223,10 @@ export async function handleTeamTool(
169
223
  lessons = lessons.filter(l => l.author?.toLowerCase().includes(a));
170
224
  }
171
225
 
172
- // Sort by recall_count desc
173
- lessons.sort((a, b) => (b.recall_count ?? 0) - (a.recall_count ?? 0));
226
+ // Sort: reviewed lessons first (senior > peer), then by recall_count desc.
227
+ const reviewRank = (l: TeamLesson) => l.reviewed_by ? (l.review_level === 'senior' ? 2 : 1) : 0;
228
+ lessons.sort((a, b) =>
229
+ (reviewRank(b) - reviewRank(a)) || ((b.recall_count ?? 0) - (a.recall_count ?? 0)));
174
230
  lessons = lessons.slice(0, limit);
175
231
 
176
232
  if (lessons.length === 0) {
@@ -183,11 +239,17 @@ export async function handleTeamTool(
183
239
  for (const l of lessons) {
184
240
  const emoji = l.outcome === 'success' ? '✅' : l.outcome === 'partial' ? '⚠️' : '❌';
185
241
  const sev = l.severity === 'critical' ? '🔴 ' : l.severity === 'major' ? '🟡 ' : '';
242
+ const reviewBadge = l.reviewed_by
243
+ ? (l.review_level === 'senior' ? ' 🛡️' : ' ✔️')
244
+ : '';
186
245
  const authorStr = l.author ? ` · _by ${l.author}_` : '';
246
+ const reviewStr = l.reviewed_by
247
+ ? ` · reviewed by ${l.reviewed_by}${(l.endorsements ?? 0) > 1 ? ` +${(l.endorsements ?? 0) - 1}` : ''}`
248
+ : '';
187
249
  const recallStr = (l.recall_count ?? 0) > 0 ? ` · recalled ${l.recall_count}×` : '';
188
250
  const ago = Math.round((Date.now() - new Date(l.ts).getTime()) / 86400000);
189
251
  const agoStr = ago === 0 ? 'today' : ago === 1 ? 'yesterday' : `${ago}d ago`;
190
- lines.push(`${emoji} ${sev}**\`${l.topic}\`**${authorStr}${recallStr} · ${agoStr}`);
252
+ lines.push(`${emoji} ${sev}**\`${l.topic}\`**${reviewBadge}${authorStr}${reviewStr}${recallStr} · ${agoStr}`);
191
253
  lines.push(` ${l.what_worked.slice(0, 120)}`);
192
254
  lines.push('');
193
255
  }
package/src/rerank.ts CHANGED
@@ -9,8 +9,10 @@
9
9
  *
10
10
  * This module is Cachly's structural advantage made concrete: we re-rank text
11
11
  * matches by the *quality* of the underlying lesson — outcome, confidence,
12
- * proven-ness (recall_count) and severity — so the lesson most likely to help
13
- * surfaces first.
12
+ * proven-ness (recall_count), severity and human review (endorsements) — so the
13
+ * lesson most likely to help surfaces first. A flat-file memory has none of these
14
+ * signals; a senior-confirmed, ten-times-recalled success reads identically to a
15
+ * one-off failed attempt.
14
16
  *
15
17
  * It is intentionally pure (no I/O): the lesson JSON already travels inside each
16
18
  * KeywordMatch.content for `cachly:lesson:best:*` keys, so reranking needs no extra
@@ -29,6 +31,10 @@ export interface LessonQuality {
29
31
  severity?: string; // 'critical' | 'major' | 'minor'
30
32
  ts?: string; // ISO timestamp (for freshness, mild)
31
33
  verified_at?: string;
34
+ // ── Governance (Phase 3): human review raises trust above mere usage ──
35
+ reviewed_by?: string; // handle of the reviewer who endorsed it (latest)
36
+ review_level?: string; // 'senior' | 'peer' (weight differs)
37
+ endorsements?: number; // distinct human endorsements
32
38
  }
33
39
 
34
40
  export interface RerankedMatch extends KeywordMatch {
@@ -53,6 +59,14 @@ const SEVERITY_WEIGHT: Record<string, number> = {
53
59
  };
54
60
  const SEVERITY_DEFAULT = 1.0;
55
61
 
62
+ // Governance: a human-confirmed lesson is more trustworthy than one proven only
63
+ // by automated recall. A senior review weighs more than a peer review; multiple
64
+ // endorsements add a small, saturating bump on top.
65
+ const REVIEW_LEVEL_WEIGHT: Record<string, number> = {
66
+ senior: 1.25,
67
+ peer: 1.1,
68
+ };
69
+
56
70
  // Clamp so a strong text match is never fully buried by quality, and a weak text
57
71
  // match is never catapulted to the top purely on quality.
58
72
  const MIN_MULTIPLIER = 0.5;
@@ -65,7 +79,9 @@ const LESSON_KEY_PREFIX = 'cachly:lesson:best:';
65
79
  * Returns 1.0 (neutral) for documents that carry no quality signal (e.g. context).
66
80
  */
67
81
  export function qualityMultiplier(lesson: LessonQuality | null): number {
68
- if (!lesson || (lesson.outcome === undefined && lesson.confidence === undefined && lesson.recall_count === undefined)) {
82
+ if (!lesson || (lesson.outcome === undefined && lesson.confidence === undefined
83
+ && lesson.recall_count === undefined && lesson.reviewed_by === undefined
84
+ && lesson.endorsements === undefined)) {
69
85
  return 1.0;
70
86
  }
71
87
 
@@ -84,7 +100,16 @@ export function qualityMultiplier(lesson: LessonQuality | null): number {
84
100
  // 4. Severity — when two lessons tie, surface the one guarding the worse failure.
85
101
  const severity = SEVERITY_WEIGHT[lesson.severity ?? ''] ?? SEVERITY_DEFAULT;
86
102
 
87
- const m = outcome * confidenceFactor * provenBoost * severity;
103
+ // 5. Governance — a human-confirmed lesson outranks an unreviewed one.
104
+ // Base weight from the review level, plus a small saturating bump for
105
+ // additional endorsements (rc-style log curve, capped at +0.1).
106
+ const reviewBase = lesson.reviewed_by
107
+ ? (REVIEW_LEVEL_WEIGHT[lesson.review_level ?? 'peer'] ?? REVIEW_LEVEL_WEIGHT.peer)
108
+ : 1.0;
109
+ const extraEndorse = Math.max(0, (lesson.endorsements ?? 0) - 1);
110
+ const reviewBoost = reviewBase * (1 + Math.min(0.1, Math.log1p(extraEndorse) / 20));
111
+
112
+ const m = outcome * confidenceFactor * provenBoost * severity * reviewBoost;
88
113
  return Math.max(MIN_MULTIPLIER, Math.min(MAX_MULTIPLIER, m));
89
114
  }
90
115
 
package/src/tools.ts CHANGED
@@ -840,6 +840,25 @@ const TOOLS = [
840
840
  required: ['instance_id', 'author', 'topic', 'outcome', 'what_worked'],
841
841
  },
842
842
  },
843
+ {
844
+ name: 'team_confirm',
845
+ description:
846
+ 'Endorse (review-confirm) a team lesson so trusted, human-reviewed knowledge ranks above unreviewed auto-learned entries. ' +
847
+ 'A senior review weighs more than a peer review; distinct endorsements add a small boost. ' +
848
+ 'Confirmed lessons surface higher in smart_recall and team_recall and carry a 🛡️/✔️ badge. ' +
849
+ 'Use this in code review or knowledge reviews to bless the canonical solution for a topic.',
850
+ inputSchema: {
851
+ type: 'object',
852
+ properties: {
853
+ instance_id: { type: 'string', description: 'UUID of the shared team brain instance' },
854
+ topic: { type: 'string', description: 'Topic slug of the lesson to confirm (e.g. "deploy:api")' },
855
+ reviewer: { type: 'string', description: 'Your name or handle (the reviewer endorsing this lesson)' },
856
+ level: { type: 'string', enum: ['senior', 'peer'], description: 'Review weight — "senior" ranks higher than "peer" (default: peer)' },
857
+ note: { type: 'string', description: 'Optional review note (kept in the lesson audit trail)' },
858
+ },
859
+ required: ['instance_id', 'topic', 'reviewer'],
860
+ },
861
+ },
843
862
  {
844
863
  name: 'team_recall',
845
864
  description: