@kage-core/kage-graph-mcp 1.1.12 → 1.1.14
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 +71 -12
- package/dist/cli.js +102 -0
- package/dist/daemon.js +11 -3
- package/dist/graph-registry.js +167 -0
- package/dist/index.js +73 -0
- package/dist/kernel.js +672 -25
- package/dist/registry/index.js +1 -1
- package/package.json +2 -2
- package/viewer/app.js +124 -8
- package/viewer/index.html +3 -3
- package/viewer/styles.css +1 -0
package/dist/kernel.js
CHANGED
|
@@ -40,6 +40,7 @@ exports.pendingDir = pendingDir;
|
|
|
40
40
|
exports.publicCandidatesDir = publicCandidatesDir;
|
|
41
41
|
exports.indexesDir = indexesDir;
|
|
42
42
|
exports.graphDir = graphDir;
|
|
43
|
+
exports.graphRegistryDir = graphRegistryDir;
|
|
43
44
|
exports.codeGraphDir = codeGraphDir;
|
|
44
45
|
exports.branchesDir = branchesDir;
|
|
45
46
|
exports.reviewDir = reviewDir;
|
|
@@ -62,6 +63,7 @@ exports.catalogDomainNodeCount = catalogDomainNodeCount;
|
|
|
62
63
|
exports.ensureMemoryDirs = ensureMemoryDirs;
|
|
63
64
|
exports.loadApprovedPackets = loadApprovedPackets;
|
|
64
65
|
exports.loadPendingPackets = loadPendingPackets;
|
|
66
|
+
exports.writeLspSymbolIndex = writeLspSymbolIndex;
|
|
65
67
|
exports.buildCodeGraph = buildCodeGraph;
|
|
66
68
|
exports.buildKnowledgeGraph = buildKnowledgeGraph;
|
|
67
69
|
exports.buildIndexes = buildIndexes;
|
|
@@ -74,6 +76,8 @@ exports.queryCodeGraph = queryCodeGraph;
|
|
|
74
76
|
exports.queryGraph = queryGraph;
|
|
75
77
|
exports.graphMermaid = graphMermaid;
|
|
76
78
|
exports.kageMetrics = kageMetrics;
|
|
79
|
+
exports.auditProject = auditProject;
|
|
80
|
+
exports.memoryInbox = memoryInbox;
|
|
77
81
|
exports.qualityReport = qualityReport;
|
|
78
82
|
exports.benchmarkProject = benchmarkProject;
|
|
79
83
|
exports.benchmarkTaskComparison = benchmarkTaskComparison;
|
|
@@ -119,11 +123,16 @@ exports.MEMORY_TYPES = [
|
|
|
119
123
|
"runbook",
|
|
120
124
|
"bug_fix",
|
|
121
125
|
"decision",
|
|
126
|
+
"rationale",
|
|
122
127
|
"convention",
|
|
123
128
|
"workflow",
|
|
124
129
|
"gotcha",
|
|
125
130
|
"reference",
|
|
126
131
|
"policy",
|
|
132
|
+
"issue_context",
|
|
133
|
+
"code_explanation",
|
|
134
|
+
"negative_result",
|
|
135
|
+
"constraint",
|
|
127
136
|
];
|
|
128
137
|
exports.SETUP_AGENTS = [
|
|
129
138
|
"codex",
|
|
@@ -174,10 +183,13 @@ Capture examples:
|
|
|
174
183
|
- A bug cause and verified fix.
|
|
175
184
|
- A convention future agents should follow.
|
|
176
185
|
- A decision and its rationale.
|
|
186
|
+
- Why code, architecture, product, or release behavior ended up this way.
|
|
187
|
+
- A non-obvious issue state, failed approach, or code explanation.
|
|
177
188
|
- A gotcha that caused rediscovery or wasted time.
|
|
178
189
|
- A path-specific workflow or dependency relationship.
|
|
179
190
|
|
|
180
|
-
Keep captures concise
|
|
191
|
+
Keep captures concise, source-backed, and useful for future understanding,
|
|
192
|
+
decisions, debugging, explanation, or action. Do not store raw transcripts.
|
|
181
193
|
|
|
182
194
|
## End-Of-Task Proposal
|
|
183
195
|
|
|
@@ -264,6 +276,9 @@ function indexesDir(projectDir) {
|
|
|
264
276
|
function graphDir(projectDir) {
|
|
265
277
|
return (0, node_path_1.join)(memoryRoot(projectDir), "graph");
|
|
266
278
|
}
|
|
279
|
+
function graphRegistryDir(projectDir) {
|
|
280
|
+
return (0, node_path_1.join)(memoryRoot(projectDir), "graph_registry");
|
|
281
|
+
}
|
|
267
282
|
function codeGraphDir(projectDir) {
|
|
268
283
|
return (0, node_path_1.join)(memoryRoot(projectDir), "code_graph");
|
|
269
284
|
}
|
|
@@ -526,7 +541,7 @@ function evaluateMemoryQuality(projectDir, packet) {
|
|
|
526
541
|
const bodyTokens = tokenize(packet.body);
|
|
527
542
|
const hasEvidence = packet.source_refs.length > 0;
|
|
528
543
|
const hasPaths = packet.paths.length > 0;
|
|
529
|
-
const highValueType = ["runbook", "bug_fix", "decision", "convention", "workflow", "gotcha", "policy"].includes(packet.type);
|
|
544
|
+
const highValueType = ["runbook", "bug_fix", "decision", "rationale", "convention", "workflow", "gotcha", "policy", "issue_context", "code_explanation", "negative_result", "constraint"].includes(packet.type);
|
|
530
545
|
if (highValueType) {
|
|
531
546
|
score += 14;
|
|
532
547
|
reasons.push("high-value memory type");
|
|
@@ -589,7 +604,7 @@ function evaluateMemoryAdmission(projectDir, packet) {
|
|
|
589
604
|
const risks = [];
|
|
590
605
|
const text = `${packet.title}\n${packet.summary}\n${packet.body}`.toLowerCase();
|
|
591
606
|
let score = 0;
|
|
592
|
-
if (["runbook", "bug_fix", "decision", "convention", "workflow", "gotcha", "policy"].includes(packet.type)) {
|
|
607
|
+
if (["runbook", "bug_fix", "decision", "rationale", "convention", "workflow", "gotcha", "policy", "issue_context", "code_explanation", "negative_result", "constraint"].includes(packet.type)) {
|
|
593
608
|
score += 18;
|
|
594
609
|
reasons.push("durable memory type");
|
|
595
610
|
}
|
|
@@ -601,9 +616,9 @@ function evaluateMemoryAdmission(projectDir, packet) {
|
|
|
601
616
|
score += 12;
|
|
602
617
|
reasons.push("repo scoped or path grounded");
|
|
603
618
|
}
|
|
604
|
-
if (/(when|after|before|because|requires|must|avoid|prefer|use this|run this|root cause|decision|convention|gotcha|workaround|fix|policy)/i.test(packet.body)) {
|
|
619
|
+
if (/(when|after|before|because|requires|must|avoid|prefer|use this|run this|root cause|rationale|decision|convention|gotcha|workaround|fix|policy|issue|hypothesis|unresolved|explains?|data flow|invariant|coupling|constraint)/i.test(packet.body)) {
|
|
605
620
|
score += 18;
|
|
606
|
-
reasons.push("has
|
|
621
|
+
reasons.push("has durable trigger, rationale, issue context, or explanation");
|
|
607
622
|
}
|
|
608
623
|
if (/(verified by|evidence:|test passed|reproduced|root cause)/i.test(packet.body)) {
|
|
609
624
|
score += 10;
|
|
@@ -986,6 +1001,15 @@ function createRepoOverviewPacket(projectDir) {
|
|
|
986
1001
|
...((0, node_fs_1.existsSync)(packagePath) ? [{ kind: "file", path: "package.json" }] : []),
|
|
987
1002
|
...((0, node_fs_1.existsSync)(readmePath) ? [{ kind: "file", path: "README.md" }] : []),
|
|
988
1003
|
],
|
|
1004
|
+
context: {
|
|
1005
|
+
fact: "Generated repo overview summarizes package metadata and the README as a navigation aid for agent startup.",
|
|
1006
|
+
why: "Agents need fast repo orientation before deeper recall or code graph queries, but generated overview memory should stay separate from human rationale.",
|
|
1007
|
+
trigger: "Recall when an agent needs first-pass repo purpose, scripts, stack, or README context.",
|
|
1008
|
+
action: "Use this as orientation only, then inspect source-backed memory and code graph facts for implementation decisions.",
|
|
1009
|
+
verification: "Generated from package.json and README.md when present.",
|
|
1010
|
+
risk_if_forgotten: "Agents may waste context rediscovering basic repo purpose or treat generated overview text as deeper semantic memory.",
|
|
1011
|
+
stale_when: "package.json or README.md changes enough that the generated overview no longer matches the repo.",
|
|
1012
|
+
},
|
|
989
1013
|
freshness: {
|
|
990
1014
|
ttl_days: 90,
|
|
991
1015
|
last_verified_at: createdAt.slice(0, 10),
|
|
@@ -1068,6 +1092,15 @@ function createRepoStructurePacket(projectDir) {
|
|
|
1068
1092
|
paths: existing.filter((entry) => pathExistsInRepo(projectDir, entry)),
|
|
1069
1093
|
stack: [],
|
|
1070
1094
|
source_refs: existing.map((path) => ({ kind: "file", path })),
|
|
1095
|
+
context: {
|
|
1096
|
+
fact: "Generated repo structure summarizes top-level files, workflows, and test files as a navigation aid.",
|
|
1097
|
+
why: "Agents need a quick map of repo entry points before choosing which files, workflows, or tests to inspect.",
|
|
1098
|
+
trigger: "Recall when orienting to this repo's layout, CI workflows, or test locations.",
|
|
1099
|
+
action: "Use this as a starting map and verify details against the current filesystem or code graph before editing.",
|
|
1100
|
+
verification: "Generated from files present in the repository.",
|
|
1101
|
+
risk_if_forgotten: "Agents may miss important entry points such as AGENTS.md, workflows, or MCP tests during initial orientation.",
|
|
1102
|
+
stale_when: "Top-level repo structure, workflow files, or test files change.",
|
|
1103
|
+
},
|
|
1071
1104
|
freshness: {
|
|
1072
1105
|
ttl_days: 30,
|
|
1073
1106
|
last_verified_at: createdAt.slice(0, 10),
|
|
@@ -1102,7 +1135,7 @@ function upsertGeneratedPacket(projectDir, packet) {
|
|
|
1102
1135
|
if (existing && existing.quality?.reviewer !== "kage-indexer")
|
|
1103
1136
|
return;
|
|
1104
1137
|
if (existing) {
|
|
1105
|
-
const comparableFields = ["title", "summary", "body", "tags", "paths", "stack", "source_refs", "freshness"];
|
|
1138
|
+
const comparableFields = ["title", "summary", "body", "tags", "paths", "stack", "source_refs", "context", "freshness"];
|
|
1106
1139
|
const same = comparableFields.every((field) => JSON.stringify(existing[field]) === JSON.stringify(packet[field]));
|
|
1107
1140
|
if (same)
|
|
1108
1141
|
return;
|
|
@@ -1170,6 +1203,9 @@ function packetGroundingWarnings(projectDir, packet, source) {
|
|
|
1170
1203
|
const hasGroundedSource = packet.source_refs.some((ref) => {
|
|
1171
1204
|
if (typeof ref.path === "string")
|
|
1172
1205
|
return !shouldSkipRepoMemoryPath(ref.path) && pathExistsInRepo(projectDir, ref.path);
|
|
1206
|
+
if (Array.isArray(ref.changed_files)) {
|
|
1207
|
+
return ref.changed_files.some((path) => typeof path === "string" && !shouldSkipRepoMemoryPath(path) && pathExistsInRepo(projectDir, path));
|
|
1208
|
+
}
|
|
1173
1209
|
if (typeof ref.kind === "string" && ["explicit_capture", "local_public_candidate"].includes(ref.kind))
|
|
1174
1210
|
return true;
|
|
1175
1211
|
return typeof ref.url === "string";
|
|
@@ -1894,6 +1930,54 @@ function parseLspDocumentSymbols(projectDir, path) {
|
|
|
1894
1930
|
}
|
|
1895
1931
|
return { symbols, imports: [], calls: [] };
|
|
1896
1932
|
}
|
|
1933
|
+
function writeLspSymbolIndex(projectDir) {
|
|
1934
|
+
ensureMemoryDirs(projectDir);
|
|
1935
|
+
const outDir = (0, node_path_1.join)(memoryRoot(projectDir), "code_index");
|
|
1936
|
+
ensureDir(outDir);
|
|
1937
|
+
const outPath = (0, node_path_1.join)(outDir, "lsp-symbols.json");
|
|
1938
|
+
const documents = [];
|
|
1939
|
+
let symbolCount = 0;
|
|
1940
|
+
const errors = [];
|
|
1941
|
+
for (const absolutePath of listCodeFiles(projectDir)) {
|
|
1942
|
+
const rel = (0, node_path_1.relative)(projectDir, absolutePath).replace(/\\/g, "/");
|
|
1943
|
+
if (!TS_AST_EXTENSIONS.has(extensionOf(rel)))
|
|
1944
|
+
continue;
|
|
1945
|
+
try {
|
|
1946
|
+
const content = (0, node_fs_1.readFileSync)(absolutePath, "utf8");
|
|
1947
|
+
const symbols = extractSymbols(rel, content).map((symbol) => ({
|
|
1948
|
+
name: symbol.name,
|
|
1949
|
+
kind: symbol.kind,
|
|
1950
|
+
detail: symbol.signature,
|
|
1951
|
+
range: {
|
|
1952
|
+
start: { line: Math.max(0, symbol.line - 1), character: 0 },
|
|
1953
|
+
end: { line: Math.max(0, (symbol.end_line ?? symbol.line) - 1), character: 0 },
|
|
1954
|
+
},
|
|
1955
|
+
}));
|
|
1956
|
+
if (!symbols.length)
|
|
1957
|
+
continue;
|
|
1958
|
+
symbolCount += symbols.length;
|
|
1959
|
+
documents.push({ path: rel, symbols });
|
|
1960
|
+
}
|
|
1961
|
+
catch (error) {
|
|
1962
|
+
errors.push(`${rel}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
writeJson(outPath, {
|
|
1966
|
+
schema_version: 1,
|
|
1967
|
+
generator: "kage-lsp-symbol-index",
|
|
1968
|
+
generated_at: nowIso(),
|
|
1969
|
+
documents,
|
|
1970
|
+
});
|
|
1971
|
+
return {
|
|
1972
|
+
ok: errors.length === 0,
|
|
1973
|
+
project_dir: projectDir,
|
|
1974
|
+
path: outPath,
|
|
1975
|
+
parser: "lsp",
|
|
1976
|
+
documents: documents.length,
|
|
1977
|
+
symbols: symbolCount,
|
|
1978
|
+
errors,
|
|
1979
|
+
};
|
|
1980
|
+
}
|
|
1897
1981
|
function parseLsif(projectDir, path) {
|
|
1898
1982
|
const docs = new Map();
|
|
1899
1983
|
const ranges = new Map();
|
|
@@ -2030,9 +2114,14 @@ function buildCodeGraph(projectDir) {
|
|
|
2030
2114
|
const addSymbol = (symbol) => {
|
|
2031
2115
|
if (!fileByPath.has(symbol.path))
|
|
2032
2116
|
return;
|
|
2033
|
-
if (symbols.some((existing) => existing.id === symbol.id))
|
|
2034
|
-
return;
|
|
2035
2117
|
const file = fileByPath.get(symbol.path);
|
|
2118
|
+
const existing = symbols.find((candidate) => candidate.id === symbol.id);
|
|
2119
|
+
if (existing) {
|
|
2120
|
+
existing.parser = strongerParser(existing.parser, symbol.parser);
|
|
2121
|
+
if (file)
|
|
2122
|
+
file.parser = strongerParser(file.parser, symbol.parser);
|
|
2123
|
+
return;
|
|
2124
|
+
}
|
|
2036
2125
|
if (file)
|
|
2037
2126
|
file.parser = strongerParser(file.parser, symbol.parser);
|
|
2038
2127
|
symbols.push(symbol);
|
|
@@ -2105,6 +2194,7 @@ function buildKnowledgeGraph(projectDir) {
|
|
|
2105
2194
|
const episodes = [];
|
|
2106
2195
|
const repoEntityId = graphEntityId("repo", repoKey(projectDir));
|
|
2107
2196
|
const generatedFrom = packets.map((packet) => packet.updated_at).sort().at(-1) ?? null;
|
|
2197
|
+
const codeGraph = buildCodeGraph(projectDir);
|
|
2108
2198
|
addEntity(entities, {
|
|
2109
2199
|
id: repoEntityId,
|
|
2110
2200
|
type: "repo",
|
|
@@ -2270,6 +2360,118 @@ function buildKnowledgeGraph(projectDir) {
|
|
|
2270
2360
|
evidence: [episodeId],
|
|
2271
2361
|
});
|
|
2272
2362
|
}
|
|
2363
|
+
const context = engineeringContextFor(packet);
|
|
2364
|
+
if (context.verification) {
|
|
2365
|
+
const command = normalizeCommandText(context.verification);
|
|
2366
|
+
if (command) {
|
|
2367
|
+
const commandId = graphEntityId("command", command);
|
|
2368
|
+
addEntity(entities, {
|
|
2369
|
+
id: commandId,
|
|
2370
|
+
type: "command",
|
|
2371
|
+
name: command,
|
|
2372
|
+
summary: `Verification command from structured memory context.`,
|
|
2373
|
+
first_seen_at: packet.created_at,
|
|
2374
|
+
last_seen_at: packet.updated_at,
|
|
2375
|
+
evidence: [episodeId],
|
|
2376
|
+
});
|
|
2377
|
+
addEdge(edges, {
|
|
2378
|
+
from: memoryId,
|
|
2379
|
+
to: commandId,
|
|
2380
|
+
relation: "verified_by",
|
|
2381
|
+
fact: `"${packet.title}" is verified by "${command}".`,
|
|
2382
|
+
confidence: packet.confidence,
|
|
2383
|
+
valid_from: packet.updated_at,
|
|
2384
|
+
invalidated_at: null,
|
|
2385
|
+
branch,
|
|
2386
|
+
commit: head,
|
|
2387
|
+
evidence: [episodeId],
|
|
2388
|
+
});
|
|
2389
|
+
}
|
|
2390
|
+
}
|
|
2391
|
+
const packetTextLower = `${packet.title}\n${packet.summary}\n${packet.body}`.toLowerCase();
|
|
2392
|
+
const packetPathSet = new Set(packet.paths);
|
|
2393
|
+
const symbolRelation = packet.type === "bug_fix"
|
|
2394
|
+
? "fixes_symbol"
|
|
2395
|
+
: packet.type === "decision" || packet.type === "rationale" || packet.type === "constraint"
|
|
2396
|
+
? "informs_symbol"
|
|
2397
|
+
: "explains_symbol";
|
|
2398
|
+
for (const symbol of codeGraph.symbols.filter((symbol) => packetPathSet.has(symbol.path))) {
|
|
2399
|
+
if (packet.type !== "code_explanation" && !packetTextLower.includes(symbol.name.toLowerCase()))
|
|
2400
|
+
continue;
|
|
2401
|
+
const symbolEntityId = graphEntityId("symbol", symbol.id);
|
|
2402
|
+
addEntity(entities, {
|
|
2403
|
+
id: symbolEntityId,
|
|
2404
|
+
type: "symbol",
|
|
2405
|
+
name: symbol.name,
|
|
2406
|
+
aliases: [symbol.id, symbol.path],
|
|
2407
|
+
summary: `${symbol.kind} in ${symbol.path}:${symbol.line}`,
|
|
2408
|
+
first_seen_at: packet.created_at,
|
|
2409
|
+
last_seen_at: packet.updated_at,
|
|
2410
|
+
evidence: [episodeId],
|
|
2411
|
+
});
|
|
2412
|
+
addEdge(edges, {
|
|
2413
|
+
from: memoryId,
|
|
2414
|
+
to: symbolEntityId,
|
|
2415
|
+
relation: symbolRelation,
|
|
2416
|
+
fact: `"${packet.title}" ${symbolRelation.replace(/_/g, " ")} ${symbol.name} in ${symbol.path}.`,
|
|
2417
|
+
confidence: packet.confidence,
|
|
2418
|
+
valid_from: packet.updated_at,
|
|
2419
|
+
invalidated_at: null,
|
|
2420
|
+
branch,
|
|
2421
|
+
commit: head,
|
|
2422
|
+
evidence: [episodeId],
|
|
2423
|
+
});
|
|
2424
|
+
}
|
|
2425
|
+
for (const route of codeGraph.routes.filter((route) => packetPathSet.has(route.file_path) && packetTextLower.includes(route.path.toLowerCase()))) {
|
|
2426
|
+
const routeEntityId = graphEntityId("route", route.id);
|
|
2427
|
+
addEntity(entities, {
|
|
2428
|
+
id: routeEntityId,
|
|
2429
|
+
type: "route",
|
|
2430
|
+
name: `${route.method} ${route.path}`,
|
|
2431
|
+
aliases: [route.id, route.file_path],
|
|
2432
|
+
summary: `${route.framework} route in ${route.file_path}:${route.line}`,
|
|
2433
|
+
first_seen_at: packet.created_at,
|
|
2434
|
+
last_seen_at: packet.updated_at,
|
|
2435
|
+
evidence: [episodeId],
|
|
2436
|
+
});
|
|
2437
|
+
addEdge(edges, {
|
|
2438
|
+
from: memoryId,
|
|
2439
|
+
to: routeEntityId,
|
|
2440
|
+
relation: "applies_to_route",
|
|
2441
|
+
fact: `"${packet.title}" applies to route ${route.method} ${route.path}.`,
|
|
2442
|
+
confidence: packet.confidence,
|
|
2443
|
+
valid_from: packet.updated_at,
|
|
2444
|
+
invalidated_at: null,
|
|
2445
|
+
branch,
|
|
2446
|
+
commit: head,
|
|
2447
|
+
evidence: [episodeId],
|
|
2448
|
+
});
|
|
2449
|
+
}
|
|
2450
|
+
for (const test of codeGraph.tests.filter((test) => packetPathSet.has(test.test_path) || Boolean(test.covers_path && packetPathSet.has(test.covers_path)))) {
|
|
2451
|
+
const testEntityId = graphEntityId("test", test.test_symbol);
|
|
2452
|
+
addEntity(entities, {
|
|
2453
|
+
id: testEntityId,
|
|
2454
|
+
type: "test",
|
|
2455
|
+
name: test.title,
|
|
2456
|
+
aliases: [test.test_symbol, test.test_path],
|
|
2457
|
+
summary: `Test in ${test.test_path}:${test.line}${test.covers_symbol ? ` covers ${test.covers_symbol}` : ""}`,
|
|
2458
|
+
first_seen_at: packet.created_at,
|
|
2459
|
+
last_seen_at: packet.updated_at,
|
|
2460
|
+
evidence: [episodeId],
|
|
2461
|
+
});
|
|
2462
|
+
addEdge(edges, {
|
|
2463
|
+
from: memoryId,
|
|
2464
|
+
to: testEntityId,
|
|
2465
|
+
relation: "verified_by_test",
|
|
2466
|
+
fact: `"${packet.title}" is related to test "${test.title}".`,
|
|
2467
|
+
confidence: packet.confidence,
|
|
2468
|
+
valid_from: packet.updated_at,
|
|
2469
|
+
invalidated_at: null,
|
|
2470
|
+
branch,
|
|
2471
|
+
commit: head,
|
|
2472
|
+
evidence: [episodeId],
|
|
2473
|
+
});
|
|
2474
|
+
}
|
|
2273
2475
|
}
|
|
2274
2476
|
const manifestCommands = npmScriptCommands(projectDir);
|
|
2275
2477
|
if (manifestCommands.length) {
|
|
@@ -2702,38 +2904,148 @@ function scorePacket(queryTerms, packet) {
|
|
|
2702
2904
|
score += 1;
|
|
2703
2905
|
return { score, why: unique(why).slice(0, 8) };
|
|
2704
2906
|
}
|
|
2907
|
+
const BM25_K1 = 1.2;
|
|
2908
|
+
const BM25_B = 0.75;
|
|
2909
|
+
const BM25_FIELD_WEIGHTS = {
|
|
2910
|
+
title: 4,
|
|
2911
|
+
summary: 2.4,
|
|
2912
|
+
tag: 2.8,
|
|
2913
|
+
path: 2.4,
|
|
2914
|
+
type: 1.8,
|
|
2915
|
+
body: 1,
|
|
2916
|
+
};
|
|
2917
|
+
function lexicalStem(term) {
|
|
2918
|
+
if (term.length > 5 && term.endsWith("ing"))
|
|
2919
|
+
return term.slice(0, -3);
|
|
2920
|
+
if (term.length > 4 && term.endsWith("ies"))
|
|
2921
|
+
return `${term.slice(0, -3)}y`;
|
|
2922
|
+
if (term.length > 4 && term.endsWith("es"))
|
|
2923
|
+
return term.slice(0, -2);
|
|
2924
|
+
if (term.length > 3 && term.endsWith("s"))
|
|
2925
|
+
return term.slice(0, -1);
|
|
2926
|
+
return term;
|
|
2927
|
+
}
|
|
2928
|
+
function expandQueryTerms(terms) {
|
|
2929
|
+
return unique(terms.flatMap((term) => unique([term, lexicalStem(term)].filter(Boolean))));
|
|
2930
|
+
}
|
|
2931
|
+
function bm25Document(packet) {
|
|
2932
|
+
const termFrequency = new Map();
|
|
2933
|
+
const fieldHits = new Map();
|
|
2934
|
+
let length = 0;
|
|
2935
|
+
const addField = (field, text) => {
|
|
2936
|
+
const weight = BM25_FIELD_WEIGHTS[field];
|
|
2937
|
+
for (const token of tokenize(text)) {
|
|
2938
|
+
termFrequency.set(token, (termFrequency.get(token) ?? 0) + weight);
|
|
2939
|
+
if (!fieldHits.has(token))
|
|
2940
|
+
fieldHits.set(token, new Set());
|
|
2941
|
+
fieldHits.get(token).add(field);
|
|
2942
|
+
length += weight;
|
|
2943
|
+
}
|
|
2944
|
+
};
|
|
2945
|
+
addField("title", packet.title);
|
|
2946
|
+
addField("summary", packet.summary);
|
|
2947
|
+
addField("tag", packet.tags.join(" "));
|
|
2948
|
+
addField("path", packet.paths.join(" "));
|
|
2949
|
+
addField("type", packet.type);
|
|
2950
|
+
addField("body", packet.body);
|
|
2951
|
+
return { packet, termFrequency, fieldHits, length: Math.max(1, length) };
|
|
2952
|
+
}
|
|
2953
|
+
function scorePacketsBm25(queryTerms, packets) {
|
|
2954
|
+
const terms = expandQueryTerms(queryTerms);
|
|
2955
|
+
const documents = packets.map(bm25Document);
|
|
2956
|
+
const result = new Map();
|
|
2957
|
+
if (!terms.length || !documents.length)
|
|
2958
|
+
return result;
|
|
2959
|
+
const averageLength = documents.reduce((sum, document) => sum + document.length, 0) / documents.length || 1;
|
|
2960
|
+
const documentFrequency = new Map();
|
|
2961
|
+
for (const term of terms) {
|
|
2962
|
+
documentFrequency.set(term, documents.filter((document) => document.termFrequency.has(term)).length);
|
|
2963
|
+
}
|
|
2964
|
+
for (const document of documents) {
|
|
2965
|
+
let score = 0;
|
|
2966
|
+
const why = [];
|
|
2967
|
+
for (const term of terms) {
|
|
2968
|
+
const termFrequency = document.termFrequency.get(term) ?? 0;
|
|
2969
|
+
if (termFrequency <= 0)
|
|
2970
|
+
continue;
|
|
2971
|
+
const df = documentFrequency.get(term) ?? 0;
|
|
2972
|
+
const idf = Math.log(1 + (documents.length - df + 0.5) / (df + 0.5));
|
|
2973
|
+
const denominator = termFrequency + BM25_K1 * (1 - BM25_B + BM25_B * (document.length / averageLength));
|
|
2974
|
+
score += idf * ((termFrequency * (BM25_K1 + 1)) / denominator);
|
|
2975
|
+
const fields = Array.from(document.fieldHits.get(term) ?? []).sort();
|
|
2976
|
+
if (fields.length)
|
|
2977
|
+
why.push(`bm25:${fields.join("+")}:${term}`);
|
|
2978
|
+
}
|
|
2979
|
+
if (score > 0)
|
|
2980
|
+
result.set(document.packet.id, { score: Number(score.toFixed(2)), why: unique(why).slice(0, 8) });
|
|
2981
|
+
}
|
|
2982
|
+
return result;
|
|
2983
|
+
}
|
|
2984
|
+
function recallIntentBoost(queryTerms, packet) {
|
|
2985
|
+
const terms = new Set(expandQueryTerms(queryTerms));
|
|
2986
|
+
const commandIntent = ["run", "test", "tests", "build", "command", "commands"].some((term) => terms.has(term));
|
|
2987
|
+
const debugIntent = ["bug", "fix", "error", "fail", "debug"].some((term) => terms.has(term));
|
|
2988
|
+
const gotchaIntent = terms.has("gotcha");
|
|
2989
|
+
const decisionIntent = terms.has("decision");
|
|
2990
|
+
const packetText = `${packet.title}\n${packet.summary}\n${packet.body}\n${packet.tags.join(" ")}`;
|
|
2991
|
+
const hasCommandEvidence = /\b(?:npm|pnpm|yarn|bun|node|python|pytest|vitest|cargo|go)\s+(?:run\s+)?(?:test|tests|build|dev|start)\b|package\.json|scripts?/i.test(packetText);
|
|
2992
|
+
let score = 0;
|
|
2993
|
+
if (commandIntent) {
|
|
2994
|
+
if (packet.type === "runbook")
|
|
2995
|
+
score += hasCommandEvidence ? 22 : 8;
|
|
2996
|
+
if (packet.type === "repo_map" && hasCommandEvidence)
|
|
2997
|
+
score += 34;
|
|
2998
|
+
if (!["runbook", "repo_map", "workflow"].includes(packet.type) && !debugIntent)
|
|
2999
|
+
score -= 8;
|
|
3000
|
+
if (packet.type === "decision" && /release|verified by|passed|published/i.test(`${packet.title}\n${packet.body}`))
|
|
3001
|
+
score -= 3;
|
|
3002
|
+
}
|
|
3003
|
+
if (debugIntent && packet.type === "bug_fix")
|
|
3004
|
+
score += 10;
|
|
3005
|
+
if (gotchaIntent)
|
|
3006
|
+
score += packet.type === "gotcha" ? 18 : -4;
|
|
3007
|
+
if (decisionIntent)
|
|
3008
|
+
score += packet.type === "decision" ? 12 : 0;
|
|
3009
|
+
return score;
|
|
3010
|
+
}
|
|
2705
3011
|
function recallBreakdown(projectDir, terms, packet, textScore) {
|
|
2706
3012
|
const graph = buildKnowledgeGraph(projectDir);
|
|
2707
3013
|
const packetEntityId = graph.entities.find((entity) => entity.type === "memory" && entity.aliases.includes(packet.id))?.id;
|
|
2708
|
-
const
|
|
3014
|
+
const rawGraphScore = packetEntityId
|
|
2709
3015
|
? graph.edges.filter((edge) => edge.from === packetEntityId || edge.to === packetEntityId).reduce((sum, edge) => sum + scoreText(terms, edge.fact), 0)
|
|
2710
3016
|
: 0;
|
|
3017
|
+
const graphScore = Math.min(rawGraphScore * 0.45, textScore > 0 ? textScore * 1.5 + 12 : 8);
|
|
2711
3018
|
const pathTypeTag = scoreText(terms, `${packet.type} ${packet.tags.join(" ")} ${packet.paths.join(" ")}`, [packet.type, ...packet.tags, ...packet.paths]);
|
|
3019
|
+
const intent = recallIntentBoost(terms, packet);
|
|
2712
3020
|
const freshness = packet.status === "approved" ? 2 : packet.status === "pending" ? 0 : -5;
|
|
2713
3021
|
const quality = Number(packet.quality.score ?? evaluateMemoryQuality(projectDir, packet).score) / 10;
|
|
2714
3022
|
const feedback = packetFeedbackScore(packet);
|
|
2715
3023
|
const vector = 0;
|
|
2716
|
-
const final = Number((textScore + graphScore
|
|
2717
|
-
return { text: textScore, graph: graphScore, path_type_tag: pathTypeTag, vector, freshness, quality: Number(quality.toFixed(2)), feedback, final };
|
|
3024
|
+
const final = Number((textScore + graphScore + pathTypeTag * 0.8 + intent + vector + freshness + quality + feedback).toFixed(2));
|
|
3025
|
+
return { bm25: textScore, text: textScore, graph: Number(graphScore.toFixed(2)), path_type_tag: pathTypeTag, intent, vector, freshness, quality: Number(quality.toFixed(2)), feedback, final };
|
|
2718
3026
|
}
|
|
2719
3027
|
function recall(projectDir, query, limit = 5, explain = false) {
|
|
2720
3028
|
indexProject(projectDir);
|
|
2721
3029
|
const terms = tokenize(query);
|
|
2722
|
-
const
|
|
3030
|
+
const approvedPackets = loadApprovedPackets(projectDir);
|
|
3031
|
+
const lexicalScores = scorePacketsBm25(terms, approvedPackets);
|
|
3032
|
+
const scored = approvedPackets
|
|
2723
3033
|
.map((packet) => {
|
|
2724
|
-
const { score, why } =
|
|
3034
|
+
const { score, why } = lexicalScores.get(packet.id) ?? { score: 0, why: [] };
|
|
2725
3035
|
const score_breakdown = recallBreakdown(projectDir, terms, packet, score);
|
|
2726
|
-
const relevance = score + score_breakdown.graph + score_breakdown.path_type_tag + score_breakdown.vector;
|
|
2727
|
-
return { packet, score:
|
|
3036
|
+
const relevance = score + score_breakdown.graph + score_breakdown.path_type_tag + score_breakdown.intent + score_breakdown.vector;
|
|
3037
|
+
return { packet, score: score_breakdown.final, relevance, why_matched: why, score_breakdown };
|
|
2728
3038
|
})
|
|
2729
3039
|
.filter((entry) => entry.relevance > 0)
|
|
2730
3040
|
.sort((a, b) => b.score - a.score || a.packet.title.localeCompare(b.packet.title))
|
|
2731
3041
|
.slice(0, limit)
|
|
2732
3042
|
.map(({ relevance, ...entry }) => entry);
|
|
2733
3043
|
const pendingSeen = new Set();
|
|
2734
|
-
const
|
|
3044
|
+
const pendingPackets = recallablePendingPackets(projectDir);
|
|
3045
|
+
const pendingLexicalScores = scorePacketsBm25(terms, pendingPackets);
|
|
3046
|
+
const pendingScored = pendingPackets
|
|
2735
3047
|
.map((packet) => {
|
|
2736
|
-
const { score, why } =
|
|
3048
|
+
const { score, why } = pendingLexicalScores.get(packet.id) ?? { score: 0, why: [] };
|
|
2737
3049
|
return { packet, score, why_matched: why };
|
|
2738
3050
|
})
|
|
2739
3051
|
.filter((entry) => entry.score > 0)
|
|
@@ -2788,7 +3100,7 @@ function recall(projectDir, query, limit = 5, explain = false) {
|
|
|
2788
3100
|
? scored.map((entry) => ({
|
|
2789
3101
|
packet_id: entry.packet.id,
|
|
2790
3102
|
title: entry.packet.title,
|
|
2791
|
-
provider: "
|
|
3103
|
+
provider: "bm25",
|
|
2792
3104
|
score_breakdown: entry.score_breakdown,
|
|
2793
3105
|
why_matched: entry.why_matched,
|
|
2794
3106
|
}))
|
|
@@ -3039,6 +3351,206 @@ function kageMetrics(projectDir) {
|
|
|
3039
3351
|
},
|
|
3040
3352
|
};
|
|
3041
3353
|
}
|
|
3354
|
+
function auditProject(projectDir) {
|
|
3355
|
+
ensureMemoryDirs(projectDir);
|
|
3356
|
+
const validation = validateProject(projectDir);
|
|
3357
|
+
const quality = qualityReport(projectDir);
|
|
3358
|
+
const codeGraph = buildCodeGraph(projectDir);
|
|
3359
|
+
const knowledgeGraph = buildKnowledgeGraph(projectDir);
|
|
3360
|
+
const approved = loadApprovedPackets(projectDir);
|
|
3361
|
+
const pending = loadPendingPackets(projectDir);
|
|
3362
|
+
const structuredPackets = approved.filter(hasStructuredEngineeringContext);
|
|
3363
|
+
const preciseParsers = ["scip", "lsif", "lsp"];
|
|
3364
|
+
const astParsers = ["typescript-ast", "tree-sitter"];
|
|
3365
|
+
const indexableFiles = codeGraph.files.filter((file) => file.parser !== "metadata").length;
|
|
3366
|
+
const preciseFiles = codeGraph.files.filter((file) => preciseParsers.includes(file.parser)).length;
|
|
3367
|
+
const astFiles = codeGraph.files.filter((file) => astParsers.includes(file.parser)).length;
|
|
3368
|
+
const fallbackFiles = codeGraph.files.filter((file) => file.parser === "generic-static" || file.parser === "metadata").length;
|
|
3369
|
+
const memoryCodeEdges = knowledgeGraph.edges.filter((edge) => ["explains_symbol", "informs_symbol", "fixes_symbol", "applies_to_route", "verified_by_test"].includes(edge.relation)).length;
|
|
3370
|
+
const stalePackets = quality.totals.stale;
|
|
3371
|
+
const duplicateCandidatesTotal = quality.totals.duplicate;
|
|
3372
|
+
const structuredCoverage = percent(structuredPackets.length, approved.length);
|
|
3373
|
+
const preciseCoverage = percent(preciseFiles, indexableFiles);
|
|
3374
|
+
const memoryCodeCoverage = percent(Math.min(memoryCodeEdges, approved.length), approved.length);
|
|
3375
|
+
const recommendations = [];
|
|
3376
|
+
if (structuredPackets.length < approved.length) {
|
|
3377
|
+
recommendations.push("Add structured context fields to high-value memories: why, verification, risk_if_forgotten, and stale_when.");
|
|
3378
|
+
}
|
|
3379
|
+
if (pending.length) {
|
|
3380
|
+
recommendations.push("Review pending memory inbox packets and approve, reject, merge, or supersede them before handoff.");
|
|
3381
|
+
}
|
|
3382
|
+
if (stalePackets) {
|
|
3383
|
+
recommendations.push("Run kage gc --dry-run and update or deprecate stale memory before trusting recall.");
|
|
3384
|
+
}
|
|
3385
|
+
if (duplicateCandidatesTotal) {
|
|
3386
|
+
recommendations.push("Merge or supersede duplicate memory packets so agents do not receive conflicting context.");
|
|
3387
|
+
}
|
|
3388
|
+
if (preciseFiles < indexableFiles) {
|
|
3389
|
+
recommendations.push("Add or extend SCIP/LSIF/LSP index artifacts in CI for remaining source files; keep AST/static extraction as fallback.");
|
|
3390
|
+
}
|
|
3391
|
+
if (!memoryCodeEdges && approved.length && codeGraph.symbols.length) {
|
|
3392
|
+
recommendations.push("Link memory packets to symbols, routes, and tests with code_explanation, bug_fix, decision, and verification context.");
|
|
3393
|
+
}
|
|
3394
|
+
if (!validation.ok) {
|
|
3395
|
+
recommendations.push("Fix validation errors before relying on Kage in PR or agent-start workflows.");
|
|
3396
|
+
}
|
|
3397
|
+
const trustScore = Math.max(0, Math.min(100, Math.round((validation.ok ? 25 : 0) +
|
|
3398
|
+
quality.useful_memory_ratio_percent * 0.25 +
|
|
3399
|
+
structuredCoverage * 0.2 +
|
|
3400
|
+
memoryCodeCoverage * 0.15 +
|
|
3401
|
+
Math.max(0, 15 - pending.length * 3 - stalePackets * 5 - duplicateCandidatesTotal * 4))));
|
|
3402
|
+
return {
|
|
3403
|
+
schema_version: 1,
|
|
3404
|
+
project_dir: projectDir,
|
|
3405
|
+
generated_at: nowIso(),
|
|
3406
|
+
ok: validation.ok && stalePackets === 0 && duplicateCandidatesTotal === 0,
|
|
3407
|
+
trust_score: trustScore,
|
|
3408
|
+
checks: {
|
|
3409
|
+
validation,
|
|
3410
|
+
memory_inbox: {
|
|
3411
|
+
approved_packets: approved.length,
|
|
3412
|
+
pending_packets: pending.length,
|
|
3413
|
+
stale_packets: stalePackets,
|
|
3414
|
+
duplicate_candidates: duplicateCandidatesTotal,
|
|
3415
|
+
},
|
|
3416
|
+
structured_memory: {
|
|
3417
|
+
total_packets: approved.length,
|
|
3418
|
+
structured_packets: structuredPackets.length,
|
|
3419
|
+
coverage_percent: structuredCoverage,
|
|
3420
|
+
missing_context_packet_ids: approved.filter((packet) => !structuredPackets.includes(packet)).map((packet) => packet.id),
|
|
3421
|
+
},
|
|
3422
|
+
code_graph: {
|
|
3423
|
+
files: codeGraph.files.length,
|
|
3424
|
+
precise_files: preciseFiles,
|
|
3425
|
+
ast_files: astFiles,
|
|
3426
|
+
fallback_files: fallbackFiles,
|
|
3427
|
+
precise_coverage_percent: preciseCoverage,
|
|
3428
|
+
indexer_coverage_percent: percent(codeGraph.files.filter((file) => file.parser !== "metadata").length, indexableFiles),
|
|
3429
|
+
},
|
|
3430
|
+
graph_links: {
|
|
3431
|
+
memory_code_edges: memoryCodeEdges,
|
|
3432
|
+
evidence_coverage_percent: percent(knowledgeGraph.edges.filter((edge) => edge.evidence.length > 0).length, knowledgeGraph.edges.length),
|
|
3433
|
+
},
|
|
3434
|
+
},
|
|
3435
|
+
recommendations,
|
|
3436
|
+
};
|
|
3437
|
+
}
|
|
3438
|
+
function memoryInbox(projectDir) {
|
|
3439
|
+
ensureMemoryDirs(projectDir);
|
|
3440
|
+
const validation = validateProject(projectDir);
|
|
3441
|
+
const quality = qualityReport(projectDir);
|
|
3442
|
+
const approved = loadApprovedPackets(projectDir);
|
|
3443
|
+
const pending = loadPendingPackets(projectDir);
|
|
3444
|
+
const items = [];
|
|
3445
|
+
for (const packet of pending) {
|
|
3446
|
+
const qualityDetails = evaluateMemoryQuality(projectDir, packet);
|
|
3447
|
+
items.push({
|
|
3448
|
+
kind: "pending",
|
|
3449
|
+
severity: "warning",
|
|
3450
|
+
packet_id: packet.id,
|
|
3451
|
+
title: packet.title,
|
|
3452
|
+
type: packet.type,
|
|
3453
|
+
status: packet.status,
|
|
3454
|
+
paths: packet.paths,
|
|
3455
|
+
summary: packet.summary,
|
|
3456
|
+
reasons: [
|
|
3457
|
+
...(qualityDetails.risks ?? []),
|
|
3458
|
+
`quality score ${qualityDetails.score}/100`,
|
|
3459
|
+
],
|
|
3460
|
+
action: "Approve, reject, merge, or keep pending after reviewing source refs and sensitivity.",
|
|
3461
|
+
});
|
|
3462
|
+
}
|
|
3463
|
+
for (const packet of approved) {
|
|
3464
|
+
const reasons = staleMemoryReasons(projectDir, packet);
|
|
3465
|
+
if (reasons.length) {
|
|
3466
|
+
items.push({
|
|
3467
|
+
kind: "stale",
|
|
3468
|
+
severity: "blocker",
|
|
3469
|
+
packet_id: packet.id,
|
|
3470
|
+
title: packet.title,
|
|
3471
|
+
type: packet.type,
|
|
3472
|
+
status: packet.status,
|
|
3473
|
+
paths: packet.paths,
|
|
3474
|
+
summary: packet.summary,
|
|
3475
|
+
reasons,
|
|
3476
|
+
action: `${staleSuggestedAction(reasons)} this packet before trusting recall.`,
|
|
3477
|
+
});
|
|
3478
|
+
}
|
|
3479
|
+
}
|
|
3480
|
+
for (const packet of approved.filter((packet) => !hasStructuredEngineeringContext(packet))) {
|
|
3481
|
+
items.push({
|
|
3482
|
+
kind: "missing_context",
|
|
3483
|
+
severity: "info",
|
|
3484
|
+
packet_id: packet.id,
|
|
3485
|
+
title: packet.title,
|
|
3486
|
+
type: packet.type,
|
|
3487
|
+
status: packet.status,
|
|
3488
|
+
paths: packet.paths,
|
|
3489
|
+
summary: packet.summary,
|
|
3490
|
+
reasons: ["missing explicit why, verification, risk, stale condition, trigger, or action"],
|
|
3491
|
+
action: "Add structured context if this packet carries durable rationale, bug, issue, or code explanation.",
|
|
3492
|
+
});
|
|
3493
|
+
}
|
|
3494
|
+
for (const packet of quality.packets.filter((packet) => packet.classification === "duplicate")) {
|
|
3495
|
+
const source = [...approved, ...pending].find((candidate) => candidate.id === packet.id);
|
|
3496
|
+
items.push({
|
|
3497
|
+
kind: "duplicate",
|
|
3498
|
+
severity: "warning",
|
|
3499
|
+
packet_id: packet.id,
|
|
3500
|
+
title: packet.title,
|
|
3501
|
+
type: packet.type,
|
|
3502
|
+
status: packet.status,
|
|
3503
|
+
paths: source?.paths,
|
|
3504
|
+
summary: source?.summary ?? packet.title,
|
|
3505
|
+
reasons: packet.risks.length ? packet.risks : ["duplicate candidate detected by quality report"],
|
|
3506
|
+
action: "Merge, supersede, or deprecate overlapping memory before handoff.",
|
|
3507
|
+
});
|
|
3508
|
+
}
|
|
3509
|
+
for (const error of validation.errors) {
|
|
3510
|
+
items.push({
|
|
3511
|
+
kind: "validation_error",
|
|
3512
|
+
severity: "blocker",
|
|
3513
|
+
summary: error,
|
|
3514
|
+
reasons: [error],
|
|
3515
|
+
action: "Fix validation errors before relying on Kage in agent or PR workflows.",
|
|
3516
|
+
});
|
|
3517
|
+
}
|
|
3518
|
+
for (const warning of validation.warnings) {
|
|
3519
|
+
items.push({
|
|
3520
|
+
kind: "validation_warning",
|
|
3521
|
+
severity: "warning",
|
|
3522
|
+
summary: warning,
|
|
3523
|
+
reasons: [warning],
|
|
3524
|
+
action: "Review grounding, indexes, generated artifacts, or packet quality.",
|
|
3525
|
+
});
|
|
3526
|
+
}
|
|
3527
|
+
const counts = {
|
|
3528
|
+
approved: approved.length,
|
|
3529
|
+
pending: pending.length,
|
|
3530
|
+
stale: items.filter((item) => item.kind === "stale").length,
|
|
3531
|
+
duplicates: items.filter((item) => item.kind === "duplicate").length,
|
|
3532
|
+
missing_context: items.filter((item) => item.kind === "missing_context").length,
|
|
3533
|
+
validation_errors: validation.errors.length,
|
|
3534
|
+
validation_warnings: validation.warnings.length,
|
|
3535
|
+
};
|
|
3536
|
+
const recommendations = unique([
|
|
3537
|
+
...(counts.pending ? ["Review pending memory packets before handoff."] : []),
|
|
3538
|
+
...(counts.stale ? ["Update, verify, supersede, or deprecate stale memory packets."] : []),
|
|
3539
|
+
...(counts.duplicates ? ["Merge or supersede duplicate memory packets."] : []),
|
|
3540
|
+
...(counts.missing_context ? ["Add structured why, verification, risk, and stale_when context to high-value packets."] : []),
|
|
3541
|
+
...(counts.validation_errors ? ["Fix validation errors before trusting recall."] : []),
|
|
3542
|
+
...(counts.validation_warnings ? ["Review validation warnings so memory remains source-grounded."] : []),
|
|
3543
|
+
]);
|
|
3544
|
+
return {
|
|
3545
|
+
schema_version: 1,
|
|
3546
|
+
project_dir: projectDir,
|
|
3547
|
+
generated_at: nowIso(),
|
|
3548
|
+
ok: counts.pending === 0 && counts.stale === 0 && counts.duplicates === 0 && counts.validation_errors === 0,
|
|
3549
|
+
counts,
|
|
3550
|
+
items,
|
|
3551
|
+
recommendations,
|
|
3552
|
+
};
|
|
3553
|
+
}
|
|
3042
3554
|
function qualityReport(projectDir) {
|
|
3043
3555
|
ensureMemoryDirs(projectDir);
|
|
3044
3556
|
const packets = [...loadPacketsFromDir(packetsDir(projectDir)), ...loadPacketsFromDir(pendingDir(projectDir))];
|
|
@@ -3116,17 +3628,57 @@ function benchmarkProject(projectDir) {
|
|
|
3116
3628
|
const metrics = kageMetricsShallow(projectDir);
|
|
3117
3629
|
const quality = qualityReport(projectDir);
|
|
3118
3630
|
const typeCoverage = quality.memory_type_coverage;
|
|
3631
|
+
const recallHitRate = percent(scenarios.filter((scenario) => scenario.hit).length, scenarios.length);
|
|
3632
|
+
const codeFlowCoverage = metrics.code_graph.files > 0 && metrics.code_graph.symbols > 0 ? 100 : 0;
|
|
3633
|
+
const gates = [
|
|
3634
|
+
{
|
|
3635
|
+
name: "recall_hit_rate",
|
|
3636
|
+
target: 60,
|
|
3637
|
+
actual: recallHitRate,
|
|
3638
|
+
unit: "percent",
|
|
3639
|
+
pass: recallHitRate >= 60,
|
|
3640
|
+
required: true,
|
|
3641
|
+
},
|
|
3642
|
+
{
|
|
3643
|
+
name: "evidence_coverage",
|
|
3644
|
+
target: 80,
|
|
3645
|
+
actual: quality.evidence_coverage_percent,
|
|
3646
|
+
unit: "percent",
|
|
3647
|
+
pass: quality.evidence_coverage_percent >= 80,
|
|
3648
|
+
required: true,
|
|
3649
|
+
},
|
|
3650
|
+
{
|
|
3651
|
+
name: "useful_memory_ratio",
|
|
3652
|
+
target: 70,
|
|
3653
|
+
actual: quality.useful_memory_ratio_percent,
|
|
3654
|
+
unit: "percent",
|
|
3655
|
+
pass: quality.useful_memory_ratio_percent >= 70,
|
|
3656
|
+
required: true,
|
|
3657
|
+
},
|
|
3658
|
+
{
|
|
3659
|
+
name: "code_flow_coverage",
|
|
3660
|
+
target: 100,
|
|
3661
|
+
actual: codeFlowCoverage,
|
|
3662
|
+
unit: "percent",
|
|
3663
|
+
pass: codeFlowCoverage >= 100,
|
|
3664
|
+
required: true,
|
|
3665
|
+
},
|
|
3666
|
+
];
|
|
3667
|
+
const gateScore = Math.round(gates.reduce((sum, gate) => sum + Math.min(100, Math.round((gate.actual / Math.max(1, gate.target)) * 100)), 0) / gates.length);
|
|
3119
3668
|
return {
|
|
3120
3669
|
schema_version: 1,
|
|
3121
3670
|
project_dir: projectDir,
|
|
3122
3671
|
generated_at: nowIso(),
|
|
3672
|
+
ok: gates.filter((gate) => gate.required).every((gate) => gate.pass),
|
|
3673
|
+
overall_score: gateScore,
|
|
3674
|
+
gates,
|
|
3123
3675
|
scenarios,
|
|
3124
3676
|
pain_metrics: {
|
|
3125
3677
|
setup_runbook_coverage_percent: typeCoverage.runbook ? 100 : 0,
|
|
3126
3678
|
bug_fix_coverage_percent: typeCoverage.bug_fix ? 100 : 0,
|
|
3127
3679
|
decision_coverage_percent: typeCoverage.decision ? 100 : 0,
|
|
3128
|
-
code_flow_coverage_percent:
|
|
3129
|
-
recall_hit_rate_percent:
|
|
3680
|
+
code_flow_coverage_percent: codeFlowCoverage,
|
|
3681
|
+
recall_hit_rate_percent: recallHitRate,
|
|
3130
3682
|
estimated_rediscovery_avoided: scenarios.filter((scenario) => scenario.hit).length,
|
|
3131
3683
|
estimated_tokens_saved: metrics.savings.estimated_tokens_saved_per_recall,
|
|
3132
3684
|
time_to_first_use_seconds: metrics.harness.policy_installed ? 30 : 90,
|
|
@@ -3296,10 +3848,20 @@ function inferLearningType(input) {
|
|
|
3296
3848
|
if (input.type)
|
|
3297
3849
|
return input.type;
|
|
3298
3850
|
const text = `${input.title ?? ""} ${input.learning}`.toLowerCase();
|
|
3851
|
+
if (/(issue context|issue|hypothesis|blocked|unresolved|attempted fix)/.test(text))
|
|
3852
|
+
return "issue_context";
|
|
3299
3853
|
if (/(bug|fix|error|fail|failure|broken|regression)/.test(text))
|
|
3300
3854
|
return "bug_fix";
|
|
3301
|
-
if (/(
|
|
3855
|
+
if (/(code explanation|explains|data flow|invariant|coupling|module purpose)/.test(text))
|
|
3856
|
+
return "code_explanation";
|
|
3857
|
+
if (/(constraint|external requirement|legal|compliance|performance budget)/.test(text))
|
|
3858
|
+
return "constraint";
|
|
3859
|
+
if (/(negative result|tried|failed because|rejected)/.test(text))
|
|
3860
|
+
return "negative_result";
|
|
3861
|
+
if (/(decided|decision|tradeoff|chose|choose)/.test(text))
|
|
3302
3862
|
return "decision";
|
|
3863
|
+
if (/(why|rationale|because)/.test(text))
|
|
3864
|
+
return "rationale";
|
|
3303
3865
|
if (/(run|command|setup|install|build|test|deploy)/.test(text))
|
|
3304
3866
|
return "runbook";
|
|
3305
3867
|
if (/(convention|always|prefer|avoid|pattern)/.test(text))
|
|
@@ -3312,6 +3874,58 @@ function titleFromLearning(learning) {
|
|
|
3312
3874
|
const sentence = learning.split(/[.!?]\s+/)[0]?.trim() || learning.trim();
|
|
3313
3875
|
return sentence.slice(0, 90) || "Session learning";
|
|
3314
3876
|
}
|
|
3877
|
+
const MEMORY_CONTEXT_FIELD_LABELS = [
|
|
3878
|
+
"Fact",
|
|
3879
|
+
"Decision",
|
|
3880
|
+
"Why",
|
|
3881
|
+
"Rationale",
|
|
3882
|
+
"Because",
|
|
3883
|
+
"When",
|
|
3884
|
+
"Trigger",
|
|
3885
|
+
"Action",
|
|
3886
|
+
"Do",
|
|
3887
|
+
"Use",
|
|
3888
|
+
"Verified by",
|
|
3889
|
+
"Verification",
|
|
3890
|
+
"Evidence",
|
|
3891
|
+
"Risk if forgotten",
|
|
3892
|
+
"Risk",
|
|
3893
|
+
"Stale when",
|
|
3894
|
+
"Invalid when",
|
|
3895
|
+
"Revisit when",
|
|
3896
|
+
"Rejected alternatives",
|
|
3897
|
+
];
|
|
3898
|
+
function labeledMemoryField(text, labels) {
|
|
3899
|
+
const escaped = labels.map((label) => label.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
|
|
3900
|
+
const allLabels = MEMORY_CONTEXT_FIELD_LABELS.map((label) => label.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
|
|
3901
|
+
const match = text.match(new RegExp(`(?:^|\\n|\\b)(?:${escaped})\\s*:\\s*([\\s\\S]*?)(?=(?:\\s|\\n)+(?:${allLabels})\\s*:|$)`, "i"));
|
|
3902
|
+
return match?.[1]?.trim().replace(/\s+$/, "");
|
|
3903
|
+
}
|
|
3904
|
+
function inferEngineeringContext(input) {
|
|
3905
|
+
const body = input.body.trim();
|
|
3906
|
+
const firstParagraph = body
|
|
3907
|
+
.split(/\n\s*\n/)
|
|
3908
|
+
.find((part) => !/^\s*(why|verified by|verification|risk if forgotten|stale when|trigger|action|rejected alternatives)\s*:/i.test(part))
|
|
3909
|
+
?.trim();
|
|
3910
|
+
const context = {
|
|
3911
|
+
fact: input.context?.fact ?? labeledMemoryField(body, ["Fact", "Decision"]) ?? firstParagraph ?? input.title,
|
|
3912
|
+
why: input.context?.why ?? labeledMemoryField(body, ["Why", "Rationale", "Because"]),
|
|
3913
|
+
trigger: input.context?.trigger ?? labeledMemoryField(body, ["When", "Trigger"]),
|
|
3914
|
+
action: input.context?.action ?? labeledMemoryField(body, ["Action", "Do", "Use"]),
|
|
3915
|
+
verification: input.context?.verification ?? labeledMemoryField(body, ["Verified by", "Verification", "Evidence"]),
|
|
3916
|
+
risk_if_forgotten: input.context?.risk_if_forgotten ?? labeledMemoryField(body, ["Risk if forgotten", "Risk"]),
|
|
3917
|
+
stale_when: input.context?.stale_when ?? labeledMemoryField(body, ["Stale when", "Invalid when", "Revisit when"]),
|
|
3918
|
+
rejected_alternatives: input.context?.rejected_alternatives,
|
|
3919
|
+
};
|
|
3920
|
+
return Object.fromEntries(Object.entries(context).filter(([, value]) => Array.isArray(value) ? value.length : Boolean(value)));
|
|
3921
|
+
}
|
|
3922
|
+
function engineeringContextFor(packet) {
|
|
3923
|
+
return inferEngineeringContext({ title: packet.title, body: packet.body, context: packet.context });
|
|
3924
|
+
}
|
|
3925
|
+
function hasStructuredEngineeringContext(packet) {
|
|
3926
|
+
const context = engineeringContextFor(packet);
|
|
3927
|
+
return Boolean(context.why || context.verification || context.risk_if_forgotten || context.stale_when || context.trigger || context.action);
|
|
3928
|
+
}
|
|
3315
3929
|
function learn(input) {
|
|
3316
3930
|
const type = inferLearningType(input);
|
|
3317
3931
|
const title = input.title?.trim() || titleFromLearning(input.learning);
|
|
@@ -3329,6 +3943,7 @@ function learn(input) {
|
|
|
3329
3943
|
tags: unique(["session-learning", ...(input.tags ?? [])]),
|
|
3330
3944
|
paths: input.paths,
|
|
3331
3945
|
stack: input.stack,
|
|
3946
|
+
context: input.context,
|
|
3332
3947
|
});
|
|
3333
3948
|
}
|
|
3334
3949
|
function capture(input) {
|
|
@@ -3366,6 +3981,7 @@ function capture(input) {
|
|
|
3366
3981
|
captured_at: createdAt,
|
|
3367
3982
|
},
|
|
3368
3983
|
],
|
|
3984
|
+
context: inferEngineeringContext({ title: input.title, body: input.body, context: input.context }),
|
|
3369
3985
|
freshness: {
|
|
3370
3986
|
ttl_days: 365,
|
|
3371
3987
|
last_verified_at: createdAt,
|
|
@@ -3903,6 +4519,15 @@ function reusableFileObservation(event) {
|
|
|
3903
4519
|
"dispatch",
|
|
3904
4520
|
"convention",
|
|
3905
4521
|
"decision",
|
|
4522
|
+
"rationale",
|
|
4523
|
+
"root cause",
|
|
4524
|
+
"issue",
|
|
4525
|
+
"hypothesis",
|
|
4526
|
+
"unresolved",
|
|
4527
|
+
"code explanation",
|
|
4528
|
+
"explains",
|
|
4529
|
+
"data flow",
|
|
4530
|
+
"invariant",
|
|
3906
4531
|
"gotcha",
|
|
3907
4532
|
"workflow",
|
|
3908
4533
|
"runbook",
|
|
@@ -3974,9 +4599,20 @@ function reusablePromptObservation(event) {
|
|
|
3974
4599
|
"convention",
|
|
3975
4600
|
"policy",
|
|
3976
4601
|
"gotcha",
|
|
4602
|
+
"bug",
|
|
4603
|
+
"issue",
|
|
4604
|
+
"issue context",
|
|
4605
|
+
"hypothesis",
|
|
4606
|
+
"unresolved",
|
|
4607
|
+
"rationale",
|
|
4608
|
+
"why:",
|
|
4609
|
+
"because",
|
|
4610
|
+
"code explanation",
|
|
4611
|
+
"explains",
|
|
4612
|
+
"data flow",
|
|
4613
|
+
"root cause",
|
|
3977
4614
|
"runbook",
|
|
3978
4615
|
"workflow",
|
|
3979
|
-
"root cause",
|
|
3980
4616
|
"use this",
|
|
3981
4617
|
"always",
|
|
3982
4618
|
"never",
|
|
@@ -3985,7 +4621,7 @@ function reusablePromptObservation(event) {
|
|
|
3985
4621
|
];
|
|
3986
4622
|
if (!durableSignals.some((signal) => lower.includes(signal)))
|
|
3987
4623
|
return "";
|
|
3988
|
-
if (/^(fix|build|create|implement|update|continue|show me|what is|why is|can you)\b/i.test(text) && !/(decision|convention|policy|gotcha|remember|prefer|avoid)/i.test(text))
|
|
4624
|
+
if (/^(fix|build|create|implement|update|continue|show me|what is|why is|can you)\b/i.test(text) && !/(decision|convention|policy|gotcha|remember|prefer|avoid|bug|issue|hypothesis|rationale|because|root cause|code explanation|explains)/i.test(text))
|
|
3989
4625
|
return "";
|
|
3990
4626
|
return text;
|
|
3991
4627
|
}
|
|
@@ -4112,7 +4748,8 @@ function createDiffChangeMemory(projectDir, summary) {
|
|
|
4112
4748
|
"",
|
|
4113
4749
|
"Improve this packet when more context is known:",
|
|
4114
4750
|
"- The actual feature, fix, or refactor rationale.",
|
|
4115
|
-
"-
|
|
4751
|
+
"- Why the change was made, including relevant bugs, issues, decisions, and code explanations.",
|
|
4752
|
+
"- The package, API, command, or architectural pattern future agents should understand, verify, or reuse.",
|
|
4116
4753
|
"- Any gotchas, follow-up risks, or branch-specific assumptions.",
|
|
4117
4754
|
"",
|
|
4118
4755
|
"Promote beyond this repo only after explicit org/global review.",
|
|
@@ -4143,6 +4780,15 @@ function createDiffChangeMemory(projectDir, summary) {
|
|
|
4143
4780
|
summary_path: (0, node_path_1.join)(reviewDir(projectDir), `branch-summary-${slugify(branch)}.json`),
|
|
4144
4781
|
},
|
|
4145
4782
|
],
|
|
4783
|
+
context: {
|
|
4784
|
+
fact: `Current branch ${branch} changes ${summary.changed_files.length} repo path${summary.changed_files.length === 1 ? "" : "s"}.`,
|
|
4785
|
+
why: "Branch change memory gives future agents durable context from the git diff when they continue, review, or verify this work.",
|
|
4786
|
+
trigger: "Recall when asking what changed on this branch, preparing a PR review, or resuming this work.",
|
|
4787
|
+
action: "Use the changed file list and diff summary as orientation, then inspect the actual diff and source files before making further edits.",
|
|
4788
|
+
verification: "Generated from git diff and refreshed by kage pr summarize or kage propose --from-diff.",
|
|
4789
|
+
risk_if_forgotten: "Future agents may repeat orientation work, miss branch-specific assumptions, or ignore files touched by this change.",
|
|
4790
|
+
stale_when: "The branch diff changes substantially, the branch is merged, or a newer change-memory packet supersedes it.",
|
|
4791
|
+
},
|
|
4146
4792
|
freshness: {
|
|
4147
4793
|
last_verified_at: now,
|
|
4148
4794
|
ttl_days: 180,
|
|
@@ -4435,9 +5081,10 @@ function loadOrgInboxPackets(projectDir, org) {
|
|
|
4435
5081
|
}
|
|
4436
5082
|
function recallFromPackets(query, packets, limit, label) {
|
|
4437
5083
|
const terms = tokenize(query);
|
|
5084
|
+
const lexicalScores = scorePacketsBm25(terms, packets);
|
|
4438
5085
|
const scored = packets
|
|
4439
5086
|
.map((packet) => {
|
|
4440
|
-
const { score, why } =
|
|
5087
|
+
const { score, why } = lexicalScores.get(packet.id) ?? { score: 0, why: [] };
|
|
4441
5088
|
return { packet, score, why_matched: why };
|
|
4442
5089
|
})
|
|
4443
5090
|
.filter((result) => result.score > 0)
|