@cachly-dev/mcp-server 0.10.68 → 0.10.70
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.
- package/README.md +1 -1
- package/dist/src/ckg.d.ts +11 -0
- package/dist/src/ckg.js +19 -0
- package/dist/src/ckg.js.map +1 -1
- package/dist/src/handlers/brain.js +187 -6
- package/dist/src/handlers/brain.js.map +1 -1
- package/dist/src/index.js +2 -2
- package/dist/src/tools.d.ts +33 -0
- package/dist/src/tools.js +31 -0
- package/dist/src/tools.js.map +1 -1
- package/package.json +2 -2
- package/server.json +3 -3
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
<img src="https://img.shields.io/badge/GDPR-EU%20servers-green" alt="GDPR: EU servers" />
|
|
23
23
|
</a>
|
|
24
24
|
|
|
25
|
-
<img src="https://img.shields.io/badge/
|
|
25
|
+
<img src="https://img.shields.io/badge/102%20MCP%20tools-violet" alt="102 MCP tools" />
|
|
26
26
|
|
|
27
27
|
<img src="https://img.shields.io/badge/License-Apache--2.0-yellow" alt="License: Apache-2.0" />
|
|
28
28
|
</p>
|
package/dist/src/ckg.d.ts
CHANGED
|
@@ -31,6 +31,15 @@ export type FileNode = {
|
|
|
31
31
|
count: number;
|
|
32
32
|
ts: string;
|
|
33
33
|
};
|
|
34
|
+
export type ServiceNode = {
|
|
35
|
+
id: string;
|
|
36
|
+
name: string;
|
|
37
|
+
kind: 'service' | 'system';
|
|
38
|
+
domain: string;
|
|
39
|
+
type: 'service';
|
|
40
|
+
count: number;
|
|
41
|
+
last_active: string;
|
|
42
|
+
};
|
|
34
43
|
export declare function ckgSlug(text: string): string;
|
|
35
44
|
/** Extract 1-3 significant keywords from free text for a problem concept */
|
|
36
45
|
export declare function extractProblemConcept(text: string): string | null;
|
|
@@ -39,5 +48,7 @@ export declare function ckgUpsertNode(redis: Redis, id: string, domain: string,
|
|
|
39
48
|
export declare function ckgUpsertPersonNode(redis: Redis, handle: string, domain: string): Promise<string>;
|
|
40
49
|
/** Upsert a File node; returns the node id ("file:{slug}"). */
|
|
41
50
|
export declare function ckgUpsertFileNode(redis: Redis, filePath: string): Promise<string>;
|
|
51
|
+
/** Upsert a Service/System node; returns the node id ("service:{slug}"). */
|
|
52
|
+
export declare function ckgUpsertServiceNode(redis: Redis, name: string, domain: string, kind?: 'service' | 'system'): Promise<string>;
|
|
42
53
|
export declare function ckgUpdateEdge(redis: Redis, from: string, edgeType: string, to: string, success: boolean, partial?: boolean): Promise<void>;
|
|
43
54
|
export declare function ckgRecordCollaboration(redis: Redis, fileId: string, personId: string): Promise<void>;
|
package/dist/src/ckg.js
CHANGED
|
@@ -49,6 +49,25 @@ export async function ckgUpsertFileNode(redis, filePath) {
|
|
|
49
49
|
await redis.set(key, JSON.stringify(node));
|
|
50
50
|
return id;
|
|
51
51
|
}
|
|
52
|
+
/** Upsert a Service/System node; returns the node id ("service:{slug}"). */
|
|
53
|
+
export async function ckgUpsertServiceNode(redis, name, domain, kind = 'service') {
|
|
54
|
+
const id = `service:${ckgSlug(name)}`;
|
|
55
|
+
const key = `cachly:ckg:node:${id}`;
|
|
56
|
+
const raw = await redis.get(key);
|
|
57
|
+
const node = raw
|
|
58
|
+
? JSON.parse(raw)
|
|
59
|
+
: { id, name, kind, domain, type: 'service', count: 0, last_active: new Date().toISOString() };
|
|
60
|
+
node.count = (node.count || 0) + 1;
|
|
61
|
+
node.last_active = new Date().toISOString();
|
|
62
|
+
if (!node.domain)
|
|
63
|
+
node.domain = domain;
|
|
64
|
+
// A node may start life as a 'service' and later be classified as 'system' (or vice
|
|
65
|
+
// versa) by an explicit caller — honour the latest non-default classification.
|
|
66
|
+
if (kind === 'system')
|
|
67
|
+
node.kind = 'system';
|
|
68
|
+
await redis.set(key, JSON.stringify(node));
|
|
69
|
+
return id;
|
|
70
|
+
}
|
|
52
71
|
export async function ckgUpdateEdge(redis, from, edgeType, to, success, partial = false) {
|
|
53
72
|
const key = `cachly:ckg:edge:${from}:${edgeType}:${to}`;
|
|
54
73
|
const raw = await redis.get(key);
|
package/dist/src/ckg.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ckg.js","sourceRoot":"","sources":["../../src/ckg.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"ckg.js","sourceRoot":"","sources":["../../src/ckg.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,13 +1,26 @@
|
|
|
1
1
|
import { execSync } from 'node:child_process';
|
|
2
2
|
import { calculateConfidence, confidenceBadge, STRUCTURED_TEMPLATES, CONFIDENCE_WARN_VALUE, CONFIDENCE_STALE_VALUE, CONFIDENCE_WARN_DAYS } from '../confidence.js';
|
|
3
|
-
import { ckgSlug, extractProblemConcept, ckgUpsertNode, ckgUpdateEdge, ckgUpsertPersonNode, ckgUpsertFileNode, ckgRecordCollaboration } from '../ckg.js';
|
|
3
|
+
import { ckgSlug, extractProblemConcept, ckgUpsertNode, ckgUpdateEdge, ckgUpsertPersonNode, ckgUpsertFileNode, ckgRecordCollaboration, ckgUpsertServiceNode } from '../ckg.js';
|
|
4
4
|
import { safeJsonParse, scanKeys } from '../utils.js';
|
|
5
5
|
import { keywordSearch, tokenize, splitMultiQuery, levenshtein, indexVocab as _indexVocab } from '../search.js';
|
|
6
6
|
import { rerankByQuality } from '../rerank.js';
|
|
7
7
|
import { computeEmbedding, hasEmbedProvider } from '../embeddings.js';
|
|
8
8
|
// ── Changelog (shown once per version in session_start) ──────────────────────
|
|
9
|
-
const MCP_VERSION = '0.10.
|
|
9
|
+
const MCP_VERSION = '0.10.70';
|
|
10
10
|
const WHATS_NEW = {
|
|
11
|
+
'0.10.70': [
|
|
12
|
+
`🛰️ **Service/System nodes in the graph (Phase 3)** — the brain now models running systems`,
|
|
13
|
+
` 🆕 \`brain_service_map(service="prometheus")\` — who owns it + every failure/fix known`,
|
|
14
|
+
` 🏷️ Tag lessons with \`service="..."\` (\`service_kind="system"\` for infra) — person→operates, file→runs_in edges`,
|
|
15
|
+
` 🚨 Incident triage: a restarting pod → instant "who knows this and what's broken before"`,
|
|
16
|
+
` 📊 102 MCP tools`,
|
|
17
|
+
],
|
|
18
|
+
'0.10.69': [
|
|
19
|
+
`📁 **Personalized context-aware recall (Phase 3)** — pass \`context_files\` to \`smart_recall\``,
|
|
20
|
+
` 🎯 Lessons learned on your current files bubble up — even when the query doesn't name the file`,
|
|
21
|
+
` 🏷️ \`📁 context match\` badge on every file-boosted result so you know WHY it ranked up`,
|
|
22
|
+
` ✅ Works alongside keyword, semantic, CKG, and governance signals — a 6th ranking dimension`,
|
|
23
|
+
],
|
|
11
24
|
'0.10.68': [
|
|
12
25
|
`🤝 **Collaboration graph (Phase 3)** — person↔person edges from shared files`,
|
|
13
26
|
` 👥 \`brain_who_knows\` now shows who the top expert frequently works with`,
|
|
@@ -81,12 +94,12 @@ export const BRAIN_TOOL_NAMES = new Set([
|
|
|
81
94
|
'learn_from_attempts', 'recall_best_solution', 'smart_recall',
|
|
82
95
|
'session_start', 'session_end', 'session_ping', 'session_handoff', 'auto_learn_session',
|
|
83
96
|
'brain_who_knows', 'brain_file_map', 'team_expertise_map',
|
|
84
|
-
'skill_gaps', 'brain_coverage', 'brain_metrics',
|
|
97
|
+
'skill_gaps', 'brain_coverage', 'brain_metrics', 'brain_service_map',
|
|
85
98
|
]);
|
|
86
99
|
export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
87
100
|
switch (name) {
|
|
88
101
|
case 'learn_from_attempts': {
|
|
89
|
-
const { instance_id, topic, outcome, what_worked, what_failed = '', context: ctx = '', severity = 'major', file_paths = [], commands = [], tags = [], depends_on = [], author = '', visibility = 'team', } = args;
|
|
102
|
+
const { instance_id, topic, outcome, what_worked, what_failed = '', context: ctx = '', severity = 'major', file_paths = [], commands = [], tags = [], depends_on = [], author = '', visibility = 'team', service = '', service_kind = 'service', } = args;
|
|
90
103
|
const redis = await getConnection(instance_id);
|
|
91
104
|
const ts = new Date().toISOString();
|
|
92
105
|
// ── Structured template hints ──────────────────────────────────────────
|
|
@@ -204,6 +217,7 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
204
217
|
tags,
|
|
205
218
|
depends_on,
|
|
206
219
|
...(author ? { author } : {}),
|
|
220
|
+
...(service ? { service } : {}),
|
|
207
221
|
visibility,
|
|
208
222
|
recall_count: recallCount,
|
|
209
223
|
ts,
|
|
@@ -372,6 +386,30 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
372
386
|
}
|
|
373
387
|
catch { /* non-critical */ }
|
|
374
388
|
}
|
|
389
|
+
// ── Phase 3: Service/System nodes ─────────────────────────────────────────
|
|
390
|
+
// When a lesson names the service/system it concerns (e.g. "prometheus",
|
|
391
|
+
// "cachly-web"), wire it into the graph: person→operates→service,
|
|
392
|
+
// concept→affects→service, file→runs_in→service. This is what lets
|
|
393
|
+
// brain_service_map answer "who owns X and what's gone wrong with X before".
|
|
394
|
+
if (service && typeof service === 'string' && service.trim()) {
|
|
395
|
+
try {
|
|
396
|
+
const domain = topic.split(':')[0] ?? 'unknown';
|
|
397
|
+
const kind = service_kind === 'system' ? 'system' : 'service';
|
|
398
|
+
const serviceId = await ckgUpsertServiceNode(redis, service.trim(), domain, kind);
|
|
399
|
+
const conceptId = ckgSlug(topic);
|
|
400
|
+
// concept → affects → service (outcome decides edge confidence)
|
|
401
|
+
await ckgUpdateEdge(redis, conceptId, 'affects', serviceId, outcome === 'success', outcome === 'partial');
|
|
402
|
+
if (author) {
|
|
403
|
+
const personId = `person:${ckgSlug(author)}`;
|
|
404
|
+
await ckgUpdateEdge(redis, personId, 'operates', serviceId, true);
|
|
405
|
+
}
|
|
406
|
+
for (const fp of file_paths.slice(0, 8)) {
|
|
407
|
+
const fileId = `file:${ckgSlug(fp)}`;
|
|
408
|
+
await ckgUpdateEdge(redis, fileId, 'runs_in', serviceId, true);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
catch { /* non-critical */ }
|
|
412
|
+
}
|
|
375
413
|
// ── Ambient team propagation: if author is set + outcome is success,
|
|
376
414
|
// auto-store as a team lesson so knowledge is shared without a separate
|
|
377
415
|
// team_learn call. Tags gain 'team'; the lesson is attributed to the author.
|
|
@@ -527,7 +565,10 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
527
565
|
return `🔍 **Partial matches for \`${topic}\`:**\n\n${results.join('\n\n')}`;
|
|
528
566
|
}
|
|
529
567
|
case 'smart_recall': {
|
|
530
|
-
const { instance_id, query, threshold = 0.78, author: requester = '', } = args;
|
|
568
|
+
const { instance_id, query, threshold = 0.78, author: requester = '', context_files: rawContextFiles = [], } = args;
|
|
569
|
+
const contextFiles = Array.isArray(rawContextFiles)
|
|
570
|
+
? rawContextFiles.filter((f) => typeof f === 'string')
|
|
571
|
+
: [];
|
|
531
572
|
const redis = await getConnection(instance_id);
|
|
532
573
|
// ── Layer 1: Keyword search across ALL brain data (always works, no embedding) ──
|
|
533
574
|
const rawMatches = await keywordSearch(redis, ['cachly:ctx:*', 'cachly:lesson:best:*', 'cachly:idx:*'], query, 10);
|
|
@@ -682,6 +723,26 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
682
723
|
}
|
|
683
724
|
}
|
|
684
725
|
catch { /* non-critical — keyword + semantic always available */ }
|
|
726
|
+
// ── Layer 4: File-context personalization ────────────────────────────────
|
|
727
|
+
// When the caller provides the files they are currently working on, lessons
|
|
728
|
+
// that were learned in the context of those files get a score boost. This
|
|
729
|
+
// surfaces file-specific institutional knowledge even when the query words
|
|
730
|
+
// don't mention the file name — the structural advantage of a graph brain.
|
|
731
|
+
if (contextFiles.length > 0) {
|
|
732
|
+
const ctxSet = new Set(contextFiles.map(f => f.replace(/\\/g, '/')));
|
|
733
|
+
for (const r of hybridMap.values()) {
|
|
734
|
+
if (!r.key.startsWith('cachly:lesson:best:'))
|
|
735
|
+
continue;
|
|
736
|
+
const ld = safeJsonParse(r.content, {});
|
|
737
|
+
const fps = Array.isArray(ld.file_paths)
|
|
738
|
+
? ld.file_paths.filter((f) => typeof f === 'string').map(f => f.replace(/\\/g, '/'))
|
|
739
|
+
: [];
|
|
740
|
+
if (fps.some(f => ctxSet.has(f))) {
|
|
741
|
+
r.hybridScore *= 1.15;
|
|
742
|
+
r.contextBoost = true;
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
}
|
|
685
746
|
// Filter private lessons — they are only accessible via exact recall_best_solution
|
|
686
747
|
const hybridResults = [...hybridMap.values()]
|
|
687
748
|
.filter(r => {
|
|
@@ -702,6 +763,11 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
702
763
|
if (crossAuthorThisCall > 0) {
|
|
703
764
|
lines.push(`> 👥 **Team knowledge reuse** — ${crossAuthorThisCall} of these lesson${crossAuthorThisCall !== 1 ? 's were' : ' was'} written by a teammate. This is the value only a shared brain delivers.\n`);
|
|
704
765
|
}
|
|
766
|
+
// File-context personalization banner
|
|
767
|
+
const ctxBoosted = hybridResults.filter(r => r.contextBoost).length;
|
|
768
|
+
if (ctxBoosted > 0) {
|
|
769
|
+
lines.push(`> 📁 **Personalized** — ${ctxBoosted} lesson${ctxBoosted !== 1 ? 's' : ''} boosted because ${ctxBoosted !== 1 ? 'they match' : 'it matches'} your current file context (${contextFiles.slice(0, 3).join(', ')}${contextFiles.length > 3 ? ', …' : ''})\n`);
|
|
770
|
+
}
|
|
705
771
|
// Show sub-query info if multi-topic was detected
|
|
706
772
|
const subQueries = splitMultiQuery(query);
|
|
707
773
|
if (subQueries.length > 1) {
|
|
@@ -759,13 +825,14 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
759
825
|
if (ld.author)
|
|
760
826
|
authorBadge = ` · 👤 ${ld.author}`;
|
|
761
827
|
}
|
|
828
|
+
const contextBadge = r.contextBoost ? ` · 📁 context match` : '';
|
|
762
829
|
const scorePart = r.matchType === 'hybrid'
|
|
763
830
|
? `BM25: ${(r.bm25Score ?? 0).toFixed(2)}, sem: ${((r.semScore ?? 0) * 100).toFixed(0)}%, 🔀 hybrid`
|
|
764
831
|
: r.matchType === 'semantic'
|
|
765
832
|
? `sem: ${((r.semScore ?? 0) * 100).toFixed(0)}%, 🎯 semantic`
|
|
766
833
|
: `BM25: ${(r.bm25Score ?? 0).toFixed(2)}, matched: ${r.matchedWords?.join(', ')}`;
|
|
767
834
|
const preview = r.content.slice(0, 400).replace(/\n/g, ' ');
|
|
768
|
-
lines.push(`**${label}**${authorBadge} _(${scorePart})_`);
|
|
835
|
+
lines.push(`**${label}**${authorBadge}${contextBadge} _(${scorePart})_`);
|
|
769
836
|
lines.push(`> ${preview}${r.content.length > 400 ? '…' : ''}\n`);
|
|
770
837
|
}
|
|
771
838
|
}
|
|
@@ -2382,6 +2449,120 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
2382
2449
|
].filter(Boolean);
|
|
2383
2450
|
return lines.join('\n');
|
|
2384
2451
|
}
|
|
2452
|
+
// ── brain_service_map ───────────────────────────────────────────────────────
|
|
2453
|
+
// Phase 3: "Who owns this service, and what's gone wrong with it before?"
|
|
2454
|
+
// Bridges a running system (e.g. a restarting prometheus pod) to the people who
|
|
2455
|
+
// operate it, the files that run in it, and the lessons learned operating it.
|
|
2456
|
+
case 'brain_service_map': {
|
|
2457
|
+
const { instance_id, service, } = args;
|
|
2458
|
+
if (!service || typeof service !== 'string' || !service.trim()) {
|
|
2459
|
+
return '⚠️ `brain_service_map` requires a non-empty `service`. Example: `brain_service_map(service="prometheus")`.';
|
|
2460
|
+
}
|
|
2461
|
+
const redis = await getConnection(instance_id);
|
|
2462
|
+
const serviceId = `service:${ckgSlug(service.trim())}`;
|
|
2463
|
+
const nodeRaw = await redis.get(`cachly:ckg:node:${serviceId}`).catch(() => null);
|
|
2464
|
+
const node = nodeRaw ? safeJsonParse(nodeRaw, null) : null;
|
|
2465
|
+
// Inbound edges: person→operates, concept→affects, file→runs_in
|
|
2466
|
+
const inbound = await redis.smembers(`cachly:ckg:idx:to:${serviceId}`).catch(() => []);
|
|
2467
|
+
const operators = [];
|
|
2468
|
+
const files = [];
|
|
2469
|
+
const concepts = [];
|
|
2470
|
+
for (const ek of inbound) {
|
|
2471
|
+
const er = await redis.get(ek).catch(() => null);
|
|
2472
|
+
if (!er)
|
|
2473
|
+
continue;
|
|
2474
|
+
const edge = safeJsonParse(er, null);
|
|
2475
|
+
if (!edge)
|
|
2476
|
+
continue;
|
|
2477
|
+
if (edge.edgeType === 'operates') {
|
|
2478
|
+
const pr = await redis.get(`cachly:ckg:node:${edge.from}`).catch(() => null);
|
|
2479
|
+
const pn = pr ? safeJsonParse(pr, null) : null;
|
|
2480
|
+
if (pn?.handle)
|
|
2481
|
+
operators.push({ handle: pn.handle, trials: edge.trials });
|
|
2482
|
+
}
|
|
2483
|
+
else if (edge.edgeType === 'runs_in') {
|
|
2484
|
+
const fr = await redis.get(`cachly:ckg:node:${edge.from}`).catch(() => null);
|
|
2485
|
+
const fn = fr ? safeJsonParse(fr, null) : null;
|
|
2486
|
+
if (fn?.path)
|
|
2487
|
+
files.push({ path: fn.path, trials: edge.trials });
|
|
2488
|
+
}
|
|
2489
|
+
else if (edge.edgeType === 'affects') {
|
|
2490
|
+
concepts.push({ id: edge.from, confidence: edge.confidence });
|
|
2491
|
+
}
|
|
2492
|
+
}
|
|
2493
|
+
operators.sort((a, b) => b.trials - a.trials);
|
|
2494
|
+
files.sort((a, b) => b.trials - a.trials);
|
|
2495
|
+
concepts.sort((a, b) => b.confidence - a.confidence);
|
|
2496
|
+
const lessons = [];
|
|
2497
|
+
const lessonKeys = await scanKeys(redis, 'cachly:lesson:best:*', { max: 1500 });
|
|
2498
|
+
const svcLower = service.trim().toLowerCase();
|
|
2499
|
+
for (const k of lessonKeys.slice(0, 400)) {
|
|
2500
|
+
const raw = await redis.get(k).catch(() => null);
|
|
2501
|
+
if (!raw)
|
|
2502
|
+
continue;
|
|
2503
|
+
const ld = safeJsonParse(raw, {});
|
|
2504
|
+
if (ld.visibility === 'private')
|
|
2505
|
+
continue;
|
|
2506
|
+
if ((ld.service ?? '').toLowerCase() !== svcLower)
|
|
2507
|
+
continue;
|
|
2508
|
+
lessons.push({ ...ld, topic: k.replace('cachly:lesson:best:', '') });
|
|
2509
|
+
}
|
|
2510
|
+
if (!node && lessons.length === 0 && operators.length === 0) {
|
|
2511
|
+
return [
|
|
2512
|
+
`## 🛰️ Service Map — \`${service}\``,
|
|
2513
|
+
``,
|
|
2514
|
+
`Nothing known about this service yet.`,
|
|
2515
|
+
``,
|
|
2516
|
+
`Tag lessons with the service they concern so the brain can build this map:`,
|
|
2517
|
+
`\`learn_from_attempts(topic="...", service="${service}", author="you", file_paths=[...])\``,
|
|
2518
|
+
].join('\n');
|
|
2519
|
+
}
|
|
2520
|
+
const kindLabel = node?.kind === 'system' ? '🖥️ system' : '🛰️ service';
|
|
2521
|
+
const lines = [
|
|
2522
|
+
`## ${node?.kind === 'system' ? '🖥️' : '🛰️'} Service Map — \`${service}\``,
|
|
2523
|
+
``,
|
|
2524
|
+
node ? `_${kindLabel} · ${node.count} lesson${node.count !== 1 ? 's' : ''} referenced · domain: ${node.domain}_` : `_No graph node yet — built from tagged lessons below._`,
|
|
2525
|
+
``,
|
|
2526
|
+
];
|
|
2527
|
+
// Operators (who owns it)
|
|
2528
|
+
if (operators.length > 0) {
|
|
2529
|
+
const owners = operators.slice(0, 5).map((o, i) => {
|
|
2530
|
+
const medal = i === 0 ? '🥇' : i === 1 ? '🥈' : i === 2 ? '🥉' : `${i + 1}.`;
|
|
2531
|
+
return `${medal} **${o.handle}** (${o.trials}×)`;
|
|
2532
|
+
}).join(' · ');
|
|
2533
|
+
lines.push(`**Operators:** ${owners}`, ``);
|
|
2534
|
+
}
|
|
2535
|
+
else {
|
|
2536
|
+
lines.push(`**Operators:** _None attributed — add \`author\` when learning about this service_`, ``);
|
|
2537
|
+
}
|
|
2538
|
+
// Recent failures first — the triage gold for an incident
|
|
2539
|
+
const failures = lessons.filter(l => l.outcome === 'failure');
|
|
2540
|
+
const successes = lessons.filter(l => l.outcome !== 'failure');
|
|
2541
|
+
if (failures.length > 0) {
|
|
2542
|
+
lines.push(`**⚠️ Known failures (${failures.length}):**`);
|
|
2543
|
+
for (const l of failures.slice(0, 6)) {
|
|
2544
|
+
const sev = l.severity === 'critical' ? ' 🔴' : l.severity === 'major' ? ' 🟡' : '';
|
|
2545
|
+
const by = l.author ? ` · 👤 ${l.author}` : '';
|
|
2546
|
+
lines.push(` - ❌ \`${l.topic}\`${sev}${by} — ${(l.what_worked ?? '').slice(0, 100)}`);
|
|
2547
|
+
}
|
|
2548
|
+
lines.push(``);
|
|
2549
|
+
}
|
|
2550
|
+
if (successes.length > 0) {
|
|
2551
|
+
lines.push(`**✅ Proven fixes (${successes.length}):**`);
|
|
2552
|
+
for (const l of successes.slice(0, 6)) {
|
|
2553
|
+
const emoji = l.outcome === 'partial' ? '⚠️' : '✅';
|
|
2554
|
+
const by = l.author ? ` · 👤 ${l.author}` : '';
|
|
2555
|
+
lines.push(` - ${emoji} \`${l.topic}\`${by} — ${(l.what_worked ?? '').slice(0, 100)}`);
|
|
2556
|
+
}
|
|
2557
|
+
lines.push(``);
|
|
2558
|
+
}
|
|
2559
|
+
// Files that run in this service
|
|
2560
|
+
if (files.length > 0) {
|
|
2561
|
+
lines.push(`**Files in this service:** ${files.slice(0, 8).map(f => `\`${f.path}\``).join(', ')}`, ``);
|
|
2562
|
+
}
|
|
2563
|
+
lines.push(`---`, `_Tag lessons with \`service="${service}"\` to keep this map current. For infra, pass \`service_kind="system"\`._`);
|
|
2564
|
+
return lines.join('\n');
|
|
2565
|
+
}
|
|
2385
2566
|
// ── sync_file_changes ─────────────────────────────────────────────────────
|
|
2386
2567
|
default:
|
|
2387
2568
|
return null;
|