@cachly-dev/mcp-server 0.10.59 → 0.10.61
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/CHANGELOG.md +43 -0
- package/README.md +1 -1
- package/dist/src/__tests__/brain-flow.test.js +174 -9
- package/dist/src/__tests__/brain-flow.test.js.map +1 -1
- package/dist/src/handlers/brain.js +386 -7
- package/dist/src/handlers/brain.js.map +1 -1
- package/dist/src/handlers/fedbrain.js +37 -10
- package/dist/src/handlers/fedbrain.js.map +1 -1
- package/dist/src/index.js +3 -3
- package/dist/src/index.js.map +1 -1
- package/dist/src/tools.d.ts +77 -1
- package/dist/src/tools.js +73 -1
- package/dist/src/tools.js.map +1 -1
- package/package.json +2 -2
- package/server.json +3 -3
- package/src/__tests__/brain-flow.test.ts +202 -9
- package/src/handlers/brain.ts +428 -7
- package/src/handlers/fedbrain.ts +38 -10
- package/src/index.ts +3 -3
- package/src/tools.ts +77 -1
|
@@ -6,14 +6,27 @@ import { keywordSearch, tokenize, splitMultiQuery, levenshtein, indexVocab as _i
|
|
|
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.61';
|
|
10
10
|
const WHATS_NEW = {
|
|
11
|
+
'0.10.61': [
|
|
12
|
+
`🎯 **Phase 3C: 100 MCP tools milestone — zero-setup knowledge graph**`,
|
|
13
|
+
` 🔄 \`brain_from_git\` now auto-builds Person+File nodes from git history (zero setup!)`,
|
|
14
|
+
` 🔍 \`skill_gaps()\` — find domains with unresolved failures, missing attribution`,
|
|
15
|
+
` 📊 \`brain_coverage()\` — scored 0-100 health report: lessons · attribution · file coverage`,
|
|
16
|
+
],
|
|
17
|
+
'0.10.60': [
|
|
18
|
+
`🆕 **Phase 3B: File map + team overview + visibility**`,
|
|
19
|
+
` 📁 \`brain_file_map(file_paths=[...])\` — experts + lessons per file before you touch it`,
|
|
20
|
+
` 🗺️ \`team_expertise_map()\` — full team skills matrix in one table`,
|
|
21
|
+
` 🔒 \`visibility: "private"\` on \`learn_from_attempts\` — private notes never leak to smart_recall`,
|
|
22
|
+
` 📊 98 MCP tools`,
|
|
23
|
+
],
|
|
11
24
|
'0.10.59': [
|
|
12
25
|
`🆕 **Phase 3A: Org-wide knowledge graph**`,
|
|
13
26
|
` 👥 \`brain_who_knows(topic="...")\` — find your team's experts on any topic instantly`,
|
|
14
27
|
` 🕸️ Person + File nodes auto-built from \`learn_from_attempts(author="...", file_paths=[...])\``,
|
|
15
28
|
` 👤 Author attribution now shown inline in \`smart_recall\` results`,
|
|
16
|
-
` 📊 96 MCP tools ·
|
|
29
|
+
` 📊 96 MCP tools · 386 tests green`,
|
|
17
30
|
],
|
|
18
31
|
'0.10.58': [
|
|
19
32
|
`🧹 Zero lint warnings — all unused imports cleaned across every handler`,
|
|
@@ -55,12 +68,13 @@ const WHATS_NEW = {
|
|
|
55
68
|
export const BRAIN_TOOL_NAMES = new Set([
|
|
56
69
|
'learn_from_attempts', 'recall_best_solution', 'smart_recall',
|
|
57
70
|
'session_start', 'session_end', 'session_ping', 'session_handoff', 'auto_learn_session',
|
|
58
|
-
'brain_who_knows',
|
|
71
|
+
'brain_who_knows', 'brain_file_map', 'team_expertise_map',
|
|
72
|
+
'skill_gaps', 'brain_coverage',
|
|
59
73
|
]);
|
|
60
74
|
export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
61
75
|
switch (name) {
|
|
62
76
|
case 'learn_from_attempts': {
|
|
63
|
-
const { instance_id, topic, outcome, what_worked, what_failed = '', context: ctx = '', severity = 'major', file_paths = [], commands = [], tags = [], depends_on = [], author = '', } = args;
|
|
77
|
+
const { instance_id, topic, outcome, what_worked, what_failed = '', context: ctx = '', severity = 'major', file_paths = [], commands = [], tags = [], depends_on = [], author = '', visibility = 'team', } = args;
|
|
64
78
|
const redis = await getConnection(instance_id);
|
|
65
79
|
const ts = new Date().toISOString();
|
|
66
80
|
// ── Structured template hints ──────────────────────────────────────────
|
|
@@ -178,6 +192,7 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
178
192
|
tags,
|
|
179
193
|
depends_on,
|
|
180
194
|
...(author ? { author } : {}),
|
|
195
|
+
visibility,
|
|
181
196
|
recall_count: recallCount,
|
|
182
197
|
ts,
|
|
183
198
|
verified_at: outcome === 'success' || outcome === 'partial' ? ts : undefined,
|
|
@@ -527,9 +542,9 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
527
542
|
}
|
|
528
543
|
}
|
|
529
544
|
// ── Layer 2: Semantic search (parallel, optional) ────────────────────────
|
|
530
|
-
const inst = await apiFetch(`/api/v1/instances/${instance_id}`);
|
|
545
|
+
const inst = await apiFetch(`/api/v1/instances/${instance_id}`).catch(() => null);
|
|
531
546
|
const semHits = [];
|
|
532
|
-
if (inst
|
|
547
|
+
if (inst?.vector_token && hasEmbedProvider()) {
|
|
533
548
|
try {
|
|
534
549
|
const embedding = await computeEmbedding(query);
|
|
535
550
|
const vectorUrl = `https://api.cachly.dev/v1/sem/${inst.vector_token}`;
|
|
@@ -634,7 +649,15 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
634
649
|
}
|
|
635
650
|
}
|
|
636
651
|
catch { /* non-critical — keyword + semantic always available */ }
|
|
637
|
-
|
|
652
|
+
// Filter private lessons — they are only accessible via exact recall_best_solution
|
|
653
|
+
const hybridResults = [...hybridMap.values()]
|
|
654
|
+
.filter(r => {
|
|
655
|
+
if (!r.key.startsWith('cachly:lesson:best:'))
|
|
656
|
+
return true;
|
|
657
|
+
const ld = safeJsonParse(r.content, {});
|
|
658
|
+
return ld.visibility !== 'private';
|
|
659
|
+
})
|
|
660
|
+
.sort((a, b) => b.hybridScore - a.hybridScore);
|
|
638
661
|
// ── Build output ──────────────────────────────────────────────────────────
|
|
639
662
|
const lines = [`🧠 **Smart Recall** for: _"${query}"_\n`];
|
|
640
663
|
// "Brain saved you here" banner — surfaces value inline for proven recalled lessons
|
|
@@ -1911,6 +1934,362 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
1911
1934
|
lines.push(`---`, `_Attribution grows with every \`learn_from_attempts(author="...", ...)\` call._`);
|
|
1912
1935
|
return lines.join('\n');
|
|
1913
1936
|
}
|
|
1937
|
+
// ── brain_file_map ────────────────────────────────────────────────────────
|
|
1938
|
+
// Phase 3B: "What do we know about these files?" — experts + lessons per file.
|
|
1939
|
+
case 'brain_file_map': {
|
|
1940
|
+
const { instance_id, file_paths = [], } = args;
|
|
1941
|
+
if (!file_paths.length)
|
|
1942
|
+
return '⚠️ Pass at least one file path.';
|
|
1943
|
+
const redis = await getConnection(instance_id);
|
|
1944
|
+
const lines = [
|
|
1945
|
+
`## 📁 File Knowledge Map`,
|
|
1946
|
+
``,
|
|
1947
|
+
`_What cachly knows about the files you're about to touch:_`,
|
|
1948
|
+
``,
|
|
1949
|
+
];
|
|
1950
|
+
for (const fp of file_paths.slice(0, 10)) {
|
|
1951
|
+
lines.push(`### \`${fp}\``);
|
|
1952
|
+
// ── Experts via CKG file nodes ─────────────────────────────────────
|
|
1953
|
+
const fileId = `file:${ckgSlug(fp)}`;
|
|
1954
|
+
const inbound = await redis.smembers(`cachly:ckg:idx:to:${fileId}`);
|
|
1955
|
+
const experts = [];
|
|
1956
|
+
for (const ek of inbound) {
|
|
1957
|
+
const er = await redis.get(ek);
|
|
1958
|
+
if (!er)
|
|
1959
|
+
continue;
|
|
1960
|
+
const edge = safeJsonParse(er, null);
|
|
1961
|
+
if (!edge || edge.edgeType !== 'touched')
|
|
1962
|
+
continue;
|
|
1963
|
+
const personRaw = await redis.get(`cachly:ckg:node:${edge.from}`);
|
|
1964
|
+
if (!personRaw)
|
|
1965
|
+
continue;
|
|
1966
|
+
const pn = safeJsonParse(personRaw, null);
|
|
1967
|
+
if (!pn || pn.type !== 'person')
|
|
1968
|
+
continue;
|
|
1969
|
+
experts.push({ handle: pn.handle, touches: edge.trials, lastSeen: edge.last_updated });
|
|
1970
|
+
}
|
|
1971
|
+
experts.sort((a, b) => b.touches - a.touches);
|
|
1972
|
+
if (experts.length > 0) {
|
|
1973
|
+
const expLine = experts.slice(0, 5).map((e, i) => {
|
|
1974
|
+
const medal = i === 0 ? '🥇' : i === 1 ? '🥈' : i === 2 ? '🥉' : `${i + 1}.`;
|
|
1975
|
+
const days = Math.floor((Date.now() - new Date(e.lastSeen).getTime()) / 86400000);
|
|
1976
|
+
const ago = days < 1 ? 'today' : days < 7 ? `${days}d ago` : `${Math.floor(days / 7)}w ago`;
|
|
1977
|
+
return `${medal} **${e.handle}** (${e.touches}× · ${ago})`;
|
|
1978
|
+
}).join(' · ');
|
|
1979
|
+
lines.push(`**Experts:** ${expLine}`);
|
|
1980
|
+
}
|
|
1981
|
+
else {
|
|
1982
|
+
lines.push(`**Experts:** _None yet — add \`author\` to \`learn_from_attempts\` calls_`);
|
|
1983
|
+
}
|
|
1984
|
+
// ── Related lessons via BM25 on filename tokens ────────────────────
|
|
1985
|
+
const tokens = fp.replace(/[^a-z0-9]/gi, ' ').split(/\s+/).filter(t => t.length >= 4);
|
|
1986
|
+
const lessonQuery = tokens.slice(0, 3).join(' ');
|
|
1987
|
+
const relatedLessons = [];
|
|
1988
|
+
if (lessonQuery) {
|
|
1989
|
+
try {
|
|
1990
|
+
const hits = await keywordSearch(redis, ['cachly:lesson:best:*'], lessonQuery, 4);
|
|
1991
|
+
for (const h of hits) {
|
|
1992
|
+
const ld = safeJsonParse(h.content, {});
|
|
1993
|
+
if (ld.visibility === 'private')
|
|
1994
|
+
continue;
|
|
1995
|
+
const emoji = ld.outcome === 'success' ? '✅' : ld.outcome === 'partial' ? '⚠️' : '❌';
|
|
1996
|
+
const sev = ld.severity === 'critical' ? ' 🔴' : ld.severity === 'major' ? ' 🟡' : '';
|
|
1997
|
+
const by = ld.author ? ` · 👤 ${ld.author}` : '';
|
|
1998
|
+
const topic = h.key.replace('cachly:lesson:best:', '');
|
|
1999
|
+
relatedLessons.push(` - ${emoji} \`${topic}\`${sev}${by} — ${(ld.what_worked ?? '').slice(0, 100)}`);
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
catch { /* non-critical */ }
|
|
2003
|
+
}
|
|
2004
|
+
// Also: exact file_paths match via lesson scan
|
|
2005
|
+
if (relatedLessons.length < 2) {
|
|
2006
|
+
const scanKeys = [];
|
|
2007
|
+
const stream = redis.scanStream({ match: 'cachly:lesson:best:*', count: 200 });
|
|
2008
|
+
await new Promise((res, rej) => {
|
|
2009
|
+
stream.on('data', (b) => scanKeys.push(...b));
|
|
2010
|
+
stream.on('end', res);
|
|
2011
|
+
stream.on('error', rej);
|
|
2012
|
+
});
|
|
2013
|
+
for (const k of scanKeys.slice(0, 300)) {
|
|
2014
|
+
const raw = await redis.get(k);
|
|
2015
|
+
if (!raw)
|
|
2016
|
+
continue;
|
|
2017
|
+
const ld = safeJsonParse(raw, {});
|
|
2018
|
+
if (ld.visibility === 'private')
|
|
2019
|
+
continue;
|
|
2020
|
+
if (!(ld.file_paths ?? []).includes(fp))
|
|
2021
|
+
continue;
|
|
2022
|
+
const topic = k.replace('cachly:lesson:best:', '');
|
|
2023
|
+
if (relatedLessons.some(l => l.includes(`\`${topic}\``)))
|
|
2024
|
+
continue;
|
|
2025
|
+
const emoji = ld.outcome === 'success' ? '✅' : ld.outcome === 'partial' ? '⚠️' : '❌';
|
|
2026
|
+
const sev = ld.severity === 'critical' ? ' 🔴' : ld.severity === 'major' ? ' 🟡' : '';
|
|
2027
|
+
const by = ld.author ? ` · 👤 ${ld.author}` : '';
|
|
2028
|
+
relatedLessons.push(` - ${emoji} \`${topic}\`${sev}${by} — ${(ld.what_worked ?? '').slice(0, 100)}`);
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
if (relatedLessons.length > 0) {
|
|
2032
|
+
lines.push(`**Related lessons:**`);
|
|
2033
|
+
lines.push(...relatedLessons.slice(0, 5));
|
|
2034
|
+
}
|
|
2035
|
+
else {
|
|
2036
|
+
lines.push(`**Related lessons:** _None yet_`);
|
|
2037
|
+
}
|
|
2038
|
+
lines.push(``);
|
|
2039
|
+
}
|
|
2040
|
+
lines.push(`---`, `_Attribution builds with \`learn_from_attempts(file_paths=[...], author="...")\`. Run this before committing to track what changed and why._`);
|
|
2041
|
+
return lines.join('\n');
|
|
2042
|
+
}
|
|
2043
|
+
// ── team_expertise_map ────────────────────────────────────────────────────
|
|
2044
|
+
// Phase 3B: Full team expertise overview — who knows what, at a glance.
|
|
2045
|
+
case 'team_expertise_map': {
|
|
2046
|
+
const { instance_id, top_n = 20, } = args;
|
|
2047
|
+
const redis = await getConnection(instance_id);
|
|
2048
|
+
// Scan all person nodes
|
|
2049
|
+
const personKeys = [];
|
|
2050
|
+
const pStream = redis.scanStream({ match: 'cachly:ckg:node:person:*', count: 200 });
|
|
2051
|
+
await new Promise((res, rej) => {
|
|
2052
|
+
pStream.on('data', (b) => personKeys.push(...b));
|
|
2053
|
+
pStream.on('end', res);
|
|
2054
|
+
pStream.on('error', rej);
|
|
2055
|
+
});
|
|
2056
|
+
if (personKeys.length === 0) {
|
|
2057
|
+
return [
|
|
2058
|
+
`## 🗺️ Team Expertise Map`,
|
|
2059
|
+
``,
|
|
2060
|
+
`No contributors yet.`,
|
|
2061
|
+
``,
|
|
2062
|
+
`Add \`author="handle"\` to any \`learn_from_attempts\` call to start building the map.`,
|
|
2063
|
+
].join('\n');
|
|
2064
|
+
}
|
|
2065
|
+
const people = [];
|
|
2066
|
+
for (const pk of personKeys) {
|
|
2067
|
+
const raw = await redis.get(pk);
|
|
2068
|
+
if (!raw)
|
|
2069
|
+
continue;
|
|
2070
|
+
const pn = safeJsonParse(raw, null);
|
|
2071
|
+
if (!pn || pn.type !== 'person')
|
|
2072
|
+
continue;
|
|
2073
|
+
// Count authored lessons + collect domains from authored edges
|
|
2074
|
+
const outEdgeKeys = await redis.smembers(`cachly:ckg:idx:from:${pn.id}`);
|
|
2075
|
+
const domainCounts = new Map();
|
|
2076
|
+
let lessonCount = 0;
|
|
2077
|
+
for (const ek of outEdgeKeys) {
|
|
2078
|
+
const er = await redis.get(ek);
|
|
2079
|
+
if (!er)
|
|
2080
|
+
continue;
|
|
2081
|
+
const edge = safeJsonParse(er, null);
|
|
2082
|
+
if (!edge || edge.edgeType !== 'authored')
|
|
2083
|
+
continue;
|
|
2084
|
+
lessonCount++;
|
|
2085
|
+
const domain = edge.to.split(':')[0] ?? 'unknown';
|
|
2086
|
+
domainCounts.set(domain, (domainCounts.get(domain) ?? 0) + 1);
|
|
2087
|
+
}
|
|
2088
|
+
people.push({ handle: pn.handle, lessonCount, domains: domainCounts, lastActive: pn.last_active });
|
|
2089
|
+
}
|
|
2090
|
+
people.sort((a, b) => b.lessonCount - a.lessonCount);
|
|
2091
|
+
const topPeople = people.slice(0, top_n);
|
|
2092
|
+
const lines = [
|
|
2093
|
+
`## 🗺️ Team Expertise Map`,
|
|
2094
|
+
``,
|
|
2095
|
+
`**${topPeople.length}** contributor${topPeople.length !== 1 ? 's' : ''} tracked:`,
|
|
2096
|
+
``,
|
|
2097
|
+
`| # | Handle | Lessons | Top domains | Last active |`,
|
|
2098
|
+
`|---|--------|---------|-------------|-------------|`,
|
|
2099
|
+
];
|
|
2100
|
+
for (const [i, p] of topPeople.entries()) {
|
|
2101
|
+
const medal = i === 0 ? '🥇' : i === 1 ? '🥈' : i === 2 ? '🥉' : `${i + 1}`;
|
|
2102
|
+
const days = Math.floor((Date.now() - new Date(p.lastActive).getTime()) / 86400000);
|
|
2103
|
+
const ago = days < 1 ? 'today' : days < 7 ? `${days}d ago` : days < 30 ? `${Math.floor(days / 7)}w ago` : `${Math.floor(days / 30)}mo ago`;
|
|
2104
|
+
const domainStr = [...p.domains.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3).map(([d, c]) => `${d}(${c})`).join(', ') || '—';
|
|
2105
|
+
lines.push(`| ${medal} | **${p.handle}** | ${p.lessonCount} | ${domainStr} | ${ago} |`);
|
|
2106
|
+
}
|
|
2107
|
+
lines.push(``, `---`, `_Use \`brain_who_knows(topic="...")\` to find the expert for a specific problem._`);
|
|
2108
|
+
return lines.join('\n');
|
|
2109
|
+
}
|
|
2110
|
+
// ── skill_gaps ────────────────────────────────────────────────────────────
|
|
2111
|
+
// Phase 3C: Show knowledge blind spots — domains/files with no success lessons,
|
|
2112
|
+
// no attribution, or only failures.
|
|
2113
|
+
case 'skill_gaps': {
|
|
2114
|
+
const { instance_id, min_failures = 1, } = args;
|
|
2115
|
+
const redis = await getConnection(instance_id);
|
|
2116
|
+
// Scan all best lessons
|
|
2117
|
+
const lessonKeys = [];
|
|
2118
|
+
const lStream = redis.scanStream({ match: 'cachly:lesson:best:*', count: 500 });
|
|
2119
|
+
await new Promise((res, rej) => {
|
|
2120
|
+
lStream.on('data', (b) => lessonKeys.push(...b));
|
|
2121
|
+
lStream.on('end', res);
|
|
2122
|
+
lStream.on('error', rej);
|
|
2123
|
+
});
|
|
2124
|
+
const domainMap = new Map();
|
|
2125
|
+
let totalAttributed = 0;
|
|
2126
|
+
let totalPrivate = 0;
|
|
2127
|
+
for (const k of lessonKeys) {
|
|
2128
|
+
const raw = await redis.get(k);
|
|
2129
|
+
if (!raw)
|
|
2130
|
+
continue;
|
|
2131
|
+
const l = safeJsonParse(raw, {});
|
|
2132
|
+
const domain = (l.topic ?? '').split(':')[0] ?? 'unknown';
|
|
2133
|
+
const s = domainMap.get(domain) ?? { successes: 0, failures: 0, attributed: 0, topics: [] };
|
|
2134
|
+
if (l.outcome === 'success' || l.outcome === 'partial')
|
|
2135
|
+
s.successes++;
|
|
2136
|
+
else
|
|
2137
|
+
s.failures++;
|
|
2138
|
+
if (l.author) {
|
|
2139
|
+
s.attributed++;
|
|
2140
|
+
totalAttributed++;
|
|
2141
|
+
}
|
|
2142
|
+
if (l.visibility === 'private')
|
|
2143
|
+
totalPrivate++;
|
|
2144
|
+
s.topics.push(l.topic ?? '');
|
|
2145
|
+
domainMap.set(domain, s);
|
|
2146
|
+
}
|
|
2147
|
+
const gaps = [];
|
|
2148
|
+
for (const [domain, s] of domainMap) {
|
|
2149
|
+
if (s.failures >= min_failures && s.successes === 0) {
|
|
2150
|
+
gaps.push({
|
|
2151
|
+
domain, type: 'unresolved failures',
|
|
2152
|
+
severity: s.failures >= 3 ? '🔴 critical' : '🟡 warn',
|
|
2153
|
+
detail: `${s.failures} failure${s.failures !== 1 ? 's' : ''}, 0 solutions — use \`learn_from_attempts\` to store what fixed it`,
|
|
2154
|
+
});
|
|
2155
|
+
}
|
|
2156
|
+
if (s.successes + s.failures >= 3 && s.attributed === 0) {
|
|
2157
|
+
gaps.push({
|
|
2158
|
+
domain, type: 'no attribution',
|
|
2159
|
+
severity: '🔵 info',
|
|
2160
|
+
detail: `${s.successes + s.failures} lessons with no \`author\` field — attribution lost, \`brain_who_knows\` can't help here`,
|
|
2161
|
+
});
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2164
|
+
// Check for domains with person nodes but no lessons (contributors without lessons)
|
|
2165
|
+
const personKeys = [];
|
|
2166
|
+
const pStream = redis.scanStream({ match: 'cachly:ckg:node:person:*', count: 200 });
|
|
2167
|
+
await new Promise((res, rej) => {
|
|
2168
|
+
pStream.on('data', (b) => personKeys.push(...b));
|
|
2169
|
+
pStream.on('end', res);
|
|
2170
|
+
pStream.on('error', rej);
|
|
2171
|
+
});
|
|
2172
|
+
const lines = [
|
|
2173
|
+
`## 🔍 Skill Gaps — Knowledge Blind Spots`,
|
|
2174
|
+
``,
|
|
2175
|
+
`**${lessonKeys.length}** lessons scanned · **${totalAttributed}** attributed · **${gaps.length}** gap${gaps.length !== 1 ? 's' : ''} found`,
|
|
2176
|
+
totalPrivate > 0 ? `_(${totalPrivate} private lessons excluded from gap analysis)_` : '',
|
|
2177
|
+
``,
|
|
2178
|
+
].filter(Boolean);
|
|
2179
|
+
if (gaps.length === 0) {
|
|
2180
|
+
lines.push(`✅ No significant knowledge gaps detected. All domains with failures have at least one success lesson.`);
|
|
2181
|
+
}
|
|
2182
|
+
else {
|
|
2183
|
+
const bySev = [
|
|
2184
|
+
...gaps.filter(g => g.severity.includes('critical')),
|
|
2185
|
+
...gaps.filter(g => g.severity.includes('warn')),
|
|
2186
|
+
...gaps.filter(g => g.severity.includes('info')),
|
|
2187
|
+
];
|
|
2188
|
+
for (const g of bySev.slice(0, 15)) {
|
|
2189
|
+
lines.push(`${g.severity} **\`${g.domain}\`** — ${g.type}`);
|
|
2190
|
+
lines.push(` ${g.detail}`);
|
|
2191
|
+
lines.push(``);
|
|
2192
|
+
}
|
|
2193
|
+
if (gaps.length > 15)
|
|
2194
|
+
lines.push(`_…and ${gaps.length - 15} more gaps. Fix critical ones first._`);
|
|
2195
|
+
}
|
|
2196
|
+
lines.push(`---`, `_Fix gaps with \`learn_from_attempts(topic="${gaps[0]?.domain ?? 'your:topic'}", outcome="success", what_worked="...")\`_`, `_Add \`author="name"\` to every call to enable \`brain_who_knows\` + \`team_expertise_map\`._`);
|
|
2197
|
+
return lines.join('\n');
|
|
2198
|
+
}
|
|
2199
|
+
// ── brain_coverage ────────────────────────────────────────────────────────
|
|
2200
|
+
// Phase 3C: Knowledge-coverage health score for the repository.
|
|
2201
|
+
case 'brain_coverage': {
|
|
2202
|
+
const { instance_id, repo_path = '.', } = args;
|
|
2203
|
+
const redis = await getConnection(instance_id);
|
|
2204
|
+
// Count lessons
|
|
2205
|
+
const lessonKeys = [];
|
|
2206
|
+
const lStream = redis.scanStream({ match: 'cachly:lesson:best:*', count: 500 });
|
|
2207
|
+
await new Promise((res, rej) => {
|
|
2208
|
+
lStream.on('data', (b) => lessonKeys.push(...b));
|
|
2209
|
+
lStream.on('end', res);
|
|
2210
|
+
lStream.on('error', rej);
|
|
2211
|
+
});
|
|
2212
|
+
// Count person nodes
|
|
2213
|
+
const personKeys = [];
|
|
2214
|
+
const pStream = redis.scanStream({ match: 'cachly:ckg:node:person:*', count: 200 });
|
|
2215
|
+
await new Promise((res, rej) => {
|
|
2216
|
+
pStream.on('data', (b) => personKeys.push(...b));
|
|
2217
|
+
pStream.on('end', res);
|
|
2218
|
+
pStream.on('error', rej);
|
|
2219
|
+
});
|
|
2220
|
+
// Count file nodes in CKG
|
|
2221
|
+
const fileKeys = [];
|
|
2222
|
+
const fStream = redis.scanStream({ match: 'cachly:ckg:node:file:*', count: 500 });
|
|
2223
|
+
await new Promise((res, rej) => {
|
|
2224
|
+
fStream.on('data', (b) => fileKeys.push(...b));
|
|
2225
|
+
fStream.on('end', res);
|
|
2226
|
+
fStream.on('error', rej);
|
|
2227
|
+
});
|
|
2228
|
+
// Get git file count for comparison (optional — non-critical if not a git repo)
|
|
2229
|
+
let gitFileCount = 0;
|
|
2230
|
+
let _gitFileCountLabel = 'n/a';
|
|
2231
|
+
try {
|
|
2232
|
+
const { execSync } = await import('node:child_process');
|
|
2233
|
+
const { resolve } = await import('node:path');
|
|
2234
|
+
const repoDir = resolve(repo_path);
|
|
2235
|
+
const output = execSync('git ls-files', { cwd: repoDir, encoding: 'utf-8', stdio: 'pipe' });
|
|
2236
|
+
gitFileCount = output.trim().split('\n').filter(Boolean).length;
|
|
2237
|
+
_gitFileCountLabel = String(gitFileCount);
|
|
2238
|
+
}
|
|
2239
|
+
catch { /* not a git repo — skip */ }
|
|
2240
|
+
// Lesson quality breakdown
|
|
2241
|
+
let successCount = 0;
|
|
2242
|
+
let failureCount = 0;
|
|
2243
|
+
let attributedCount = 0;
|
|
2244
|
+
const domainSet = new Set();
|
|
2245
|
+
for (const k of lessonKeys) {
|
|
2246
|
+
const raw = await redis.get(k);
|
|
2247
|
+
if (!raw)
|
|
2248
|
+
continue;
|
|
2249
|
+
const l = safeJsonParse(raw, {});
|
|
2250
|
+
if (l.outcome === 'success' || l.outcome === 'partial')
|
|
2251
|
+
successCount++;
|
|
2252
|
+
else
|
|
2253
|
+
failureCount++;
|
|
2254
|
+
if (l.author)
|
|
2255
|
+
attributedCount++;
|
|
2256
|
+
domainSet.add((l.topic ?? '').split(':')[0] ?? 'unknown');
|
|
2257
|
+
}
|
|
2258
|
+
const fileCoverage = gitFileCount > 0
|
|
2259
|
+
? `${Math.round((fileKeys.length / gitFileCount) * 100)}% (${fileKeys.length}/${gitFileCount} files)`
|
|
2260
|
+
: `${fileKeys.length} files tracked`;
|
|
2261
|
+
const attributionPct = lessonKeys.length > 0
|
|
2262
|
+
? Math.round((attributedCount / lessonKeys.length) * 100)
|
|
2263
|
+
: 0;
|
|
2264
|
+
const overallScore = Math.round((Math.min(1, lessonKeys.length / 50) * 25) + // lesson volume (25 pts)
|
|
2265
|
+
(Math.min(1, successCount / Math.max(1, lessonKeys.length)) * 25) + // success ratio (25 pts)
|
|
2266
|
+
(Math.min(1, attributionPct / 100) * 25) + // attribution completeness (25 pts)
|
|
2267
|
+
(Math.min(1, personKeys.length / 5) * 25) // team engagement (25 pts)
|
|
2268
|
+
);
|
|
2269
|
+
const scoreEmoji = overallScore >= 80 ? '🟢' : overallScore >= 50 ? '🟡' : '🔴';
|
|
2270
|
+
return [
|
|
2271
|
+
`## 📊 Brain Coverage Report`,
|
|
2272
|
+
``,
|
|
2273
|
+
`${scoreEmoji} **Overall score: ${overallScore}/100**`,
|
|
2274
|
+
``,
|
|
2275
|
+
`| Metric | Value |`,
|
|
2276
|
+
`|--------|-------|`,
|
|
2277
|
+
`| Total lessons | **${lessonKeys.length}** (${successCount} success · ${failureCount} failure) |`,
|
|
2278
|
+
`| Problem domains | **${domainSet.size}** |`,
|
|
2279
|
+
`| Attribution | **${attributedCount}/${lessonKeys.length}** (${attributionPct}%) |`,
|
|
2280
|
+
`| Contributors | **${personKeys.length}** tracked in knowledge graph |`,
|
|
2281
|
+
`| File coverage | **${fileCoverage}** |`,
|
|
2282
|
+
``,
|
|
2283
|
+
`**Score breakdown (each 0–25):**`,
|
|
2284
|
+
`- Lesson volume: **${Math.round(Math.min(1, lessonKeys.length / 50) * 25)}/25** _(target: 50+ lessons)_`,
|
|
2285
|
+
`- Success ratio: **${Math.round(Math.min(1, successCount / Math.max(1, lessonKeys.length)) * 25)}/25** _(target: all lessons resolved)_`,
|
|
2286
|
+
`- Attribution: **${Math.round(Math.min(1, attributionPct / 100) * 25)}/25** _(target: 100% attributed)_`,
|
|
2287
|
+
`- Team engagement: **${Math.round(Math.min(1, personKeys.length / 5) * 25)}/25** _(target: 5+ contributors)_`,
|
|
2288
|
+
``,
|
|
2289
|
+
`---`,
|
|
2290
|
+
`_Boost score: run \`brain_from_git\` · add \`author\` to \`learn_from_attempts\` · fix gaps with \`skill_gaps\`_`,
|
|
2291
|
+
].join('\n');
|
|
2292
|
+
}
|
|
1914
2293
|
// ── sync_file_changes ─────────────────────────────────────────────────────
|
|
1915
2294
|
default:
|
|
1916
2295
|
return null;
|