@kage-core/kage-graph-mcp 1.1.13 → 1.1.15
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 +89 -11
- 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 +697 -26
- package/dist/registry/index.js +1 -1
- package/dist/release.js +121 -0
- package/package.json +4 -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;
|
|
@@ -896,7 +911,12 @@ const NOISE_PATH_PREFIXES = [
|
|
|
896
911
|
".pub-cache/",
|
|
897
912
|
"elm-stuff/",
|
|
898
913
|
];
|
|
914
|
+
function isReviewableMemoryPath(filePath) {
|
|
915
|
+
return /^\.agent_memory\/(?:packets|pending)\/[^/]+\.json$/.test(filePath);
|
|
916
|
+
}
|
|
899
917
|
function isNoisePath(filePath) {
|
|
918
|
+
if (isReviewableMemoryPath(filePath))
|
|
919
|
+
return false;
|
|
900
920
|
return NOISE_PATH_PREFIXES.some((prefix) => filePath.startsWith(prefix));
|
|
901
921
|
}
|
|
902
922
|
function parsePorcelainStatus(status) {
|
|
@@ -911,7 +931,26 @@ function parsePorcelainPath(line) {
|
|
|
911
931
|
const raw = line.length > 2 && line[2] === " " ? line.slice(3) : line.slice(2);
|
|
912
932
|
return raw.trim();
|
|
913
933
|
}
|
|
934
|
+
function branchDiffStat(projectDir, changedFiles) {
|
|
935
|
+
const diffStats = [
|
|
936
|
+
readGit(projectDir, ["diff", "--stat"]),
|
|
937
|
+
readGit(projectDir, ["diff", "--cached", "--stat"]),
|
|
938
|
+
].filter(Boolean).join("\n").trim();
|
|
939
|
+
const untracked = new Set((readGit(projectDir, ["ls-files", "--others", "--exclude-standard"]) ?? "")
|
|
940
|
+
.split(/\r?\n/)
|
|
941
|
+
.map((path) => path.trim())
|
|
942
|
+
.filter(Boolean)
|
|
943
|
+
.filter((path) => changedFiles.includes(path)));
|
|
944
|
+
const untrackedStats = [...untracked]
|
|
945
|
+
.filter((file) => !diffStats.includes(file))
|
|
946
|
+
.map((file) => `${file} | untracked`)
|
|
947
|
+
.join("\n");
|
|
948
|
+
return [diffStats, untrackedStats].filter(Boolean).join("\n").trim()
|
|
949
|
+
|| changedFiles.map((file) => `${file} | changed`).join("\n");
|
|
950
|
+
}
|
|
914
951
|
function shouldSkipRepoMemoryPath(relativePath) {
|
|
952
|
+
if (isReviewableMemoryPath(relativePath))
|
|
953
|
+
return false;
|
|
915
954
|
return isNoisePath(relativePath) || shouldSkipCodePath(relativePath);
|
|
916
955
|
}
|
|
917
956
|
function migrateLegacyMarkdown(projectDir) {
|
|
@@ -986,6 +1025,15 @@ function createRepoOverviewPacket(projectDir) {
|
|
|
986
1025
|
...((0, node_fs_1.existsSync)(packagePath) ? [{ kind: "file", path: "package.json" }] : []),
|
|
987
1026
|
...((0, node_fs_1.existsSync)(readmePath) ? [{ kind: "file", path: "README.md" }] : []),
|
|
988
1027
|
],
|
|
1028
|
+
context: {
|
|
1029
|
+
fact: "Generated repo overview summarizes package metadata and the README as a navigation aid for agent startup.",
|
|
1030
|
+
why: "Agents need fast repo orientation before deeper recall or code graph queries, but generated overview memory should stay separate from human rationale.",
|
|
1031
|
+
trigger: "Recall when an agent needs first-pass repo purpose, scripts, stack, or README context.",
|
|
1032
|
+
action: "Use this as orientation only, then inspect source-backed memory and code graph facts for implementation decisions.",
|
|
1033
|
+
verification: "Generated from package.json and README.md when present.",
|
|
1034
|
+
risk_if_forgotten: "Agents may waste context rediscovering basic repo purpose or treat generated overview text as deeper semantic memory.",
|
|
1035
|
+
stale_when: "package.json or README.md changes enough that the generated overview no longer matches the repo.",
|
|
1036
|
+
},
|
|
989
1037
|
freshness: {
|
|
990
1038
|
ttl_days: 90,
|
|
991
1039
|
last_verified_at: createdAt.slice(0, 10),
|
|
@@ -1068,6 +1116,15 @@ function createRepoStructurePacket(projectDir) {
|
|
|
1068
1116
|
paths: existing.filter((entry) => pathExistsInRepo(projectDir, entry)),
|
|
1069
1117
|
stack: [],
|
|
1070
1118
|
source_refs: existing.map((path) => ({ kind: "file", path })),
|
|
1119
|
+
context: {
|
|
1120
|
+
fact: "Generated repo structure summarizes top-level files, workflows, and test files as a navigation aid.",
|
|
1121
|
+
why: "Agents need a quick map of repo entry points before choosing which files, workflows, or tests to inspect.",
|
|
1122
|
+
trigger: "Recall when orienting to this repo's layout, CI workflows, or test locations.",
|
|
1123
|
+
action: "Use this as a starting map and verify details against the current filesystem or code graph before editing.",
|
|
1124
|
+
verification: "Generated from files present in the repository.",
|
|
1125
|
+
risk_if_forgotten: "Agents may miss important entry points such as AGENTS.md, workflows, or MCP tests during initial orientation.",
|
|
1126
|
+
stale_when: "Top-level repo structure, workflow files, or test files change.",
|
|
1127
|
+
},
|
|
1071
1128
|
freshness: {
|
|
1072
1129
|
ttl_days: 30,
|
|
1073
1130
|
last_verified_at: createdAt.slice(0, 10),
|
|
@@ -1102,7 +1159,7 @@ function upsertGeneratedPacket(projectDir, packet) {
|
|
|
1102
1159
|
if (existing && existing.quality?.reviewer !== "kage-indexer")
|
|
1103
1160
|
return;
|
|
1104
1161
|
if (existing) {
|
|
1105
|
-
const comparableFields = ["title", "summary", "body", "tags", "paths", "stack", "source_refs", "freshness"];
|
|
1162
|
+
const comparableFields = ["title", "summary", "body", "tags", "paths", "stack", "source_refs", "context", "freshness"];
|
|
1106
1163
|
const same = comparableFields.every((field) => JSON.stringify(existing[field]) === JSON.stringify(packet[field]));
|
|
1107
1164
|
if (same)
|
|
1108
1165
|
return;
|
|
@@ -1170,6 +1227,9 @@ function packetGroundingWarnings(projectDir, packet, source) {
|
|
|
1170
1227
|
const hasGroundedSource = packet.source_refs.some((ref) => {
|
|
1171
1228
|
if (typeof ref.path === "string")
|
|
1172
1229
|
return !shouldSkipRepoMemoryPath(ref.path) && pathExistsInRepo(projectDir, ref.path);
|
|
1230
|
+
if (Array.isArray(ref.changed_files)) {
|
|
1231
|
+
return ref.changed_files.some((path) => typeof path === "string" && !shouldSkipRepoMemoryPath(path) && pathExistsInRepo(projectDir, path));
|
|
1232
|
+
}
|
|
1173
1233
|
if (typeof ref.kind === "string" && ["explicit_capture", "local_public_candidate"].includes(ref.kind))
|
|
1174
1234
|
return true;
|
|
1175
1235
|
return typeof ref.url === "string";
|
|
@@ -1894,6 +1954,54 @@ function parseLspDocumentSymbols(projectDir, path) {
|
|
|
1894
1954
|
}
|
|
1895
1955
|
return { symbols, imports: [], calls: [] };
|
|
1896
1956
|
}
|
|
1957
|
+
function writeLspSymbolIndex(projectDir) {
|
|
1958
|
+
ensureMemoryDirs(projectDir);
|
|
1959
|
+
const outDir = (0, node_path_1.join)(memoryRoot(projectDir), "code_index");
|
|
1960
|
+
ensureDir(outDir);
|
|
1961
|
+
const outPath = (0, node_path_1.join)(outDir, "lsp-symbols.json");
|
|
1962
|
+
const documents = [];
|
|
1963
|
+
let symbolCount = 0;
|
|
1964
|
+
const errors = [];
|
|
1965
|
+
for (const absolutePath of listCodeFiles(projectDir)) {
|
|
1966
|
+
const rel = (0, node_path_1.relative)(projectDir, absolutePath).replace(/\\/g, "/");
|
|
1967
|
+
if (!TS_AST_EXTENSIONS.has(extensionOf(rel)))
|
|
1968
|
+
continue;
|
|
1969
|
+
try {
|
|
1970
|
+
const content = (0, node_fs_1.readFileSync)(absolutePath, "utf8");
|
|
1971
|
+
const symbols = extractSymbols(rel, content).map((symbol) => ({
|
|
1972
|
+
name: symbol.name,
|
|
1973
|
+
kind: symbol.kind,
|
|
1974
|
+
detail: symbol.signature,
|
|
1975
|
+
range: {
|
|
1976
|
+
start: { line: Math.max(0, symbol.line - 1), character: 0 },
|
|
1977
|
+
end: { line: Math.max(0, (symbol.end_line ?? symbol.line) - 1), character: 0 },
|
|
1978
|
+
},
|
|
1979
|
+
}));
|
|
1980
|
+
if (!symbols.length)
|
|
1981
|
+
continue;
|
|
1982
|
+
symbolCount += symbols.length;
|
|
1983
|
+
documents.push({ path: rel, symbols });
|
|
1984
|
+
}
|
|
1985
|
+
catch (error) {
|
|
1986
|
+
errors.push(`${rel}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
writeJson(outPath, {
|
|
1990
|
+
schema_version: 1,
|
|
1991
|
+
generator: "kage-lsp-symbol-index",
|
|
1992
|
+
generated_at: nowIso(),
|
|
1993
|
+
documents,
|
|
1994
|
+
});
|
|
1995
|
+
return {
|
|
1996
|
+
ok: errors.length === 0,
|
|
1997
|
+
project_dir: projectDir,
|
|
1998
|
+
path: outPath,
|
|
1999
|
+
parser: "lsp",
|
|
2000
|
+
documents: documents.length,
|
|
2001
|
+
symbols: symbolCount,
|
|
2002
|
+
errors,
|
|
2003
|
+
};
|
|
2004
|
+
}
|
|
1897
2005
|
function parseLsif(projectDir, path) {
|
|
1898
2006
|
const docs = new Map();
|
|
1899
2007
|
const ranges = new Map();
|
|
@@ -2030,9 +2138,14 @@ function buildCodeGraph(projectDir) {
|
|
|
2030
2138
|
const addSymbol = (symbol) => {
|
|
2031
2139
|
if (!fileByPath.has(symbol.path))
|
|
2032
2140
|
return;
|
|
2033
|
-
if (symbols.some((existing) => existing.id === symbol.id))
|
|
2034
|
-
return;
|
|
2035
2141
|
const file = fileByPath.get(symbol.path);
|
|
2142
|
+
const existing = symbols.find((candidate) => candidate.id === symbol.id);
|
|
2143
|
+
if (existing) {
|
|
2144
|
+
existing.parser = strongerParser(existing.parser, symbol.parser);
|
|
2145
|
+
if (file)
|
|
2146
|
+
file.parser = strongerParser(file.parser, symbol.parser);
|
|
2147
|
+
return;
|
|
2148
|
+
}
|
|
2036
2149
|
if (file)
|
|
2037
2150
|
file.parser = strongerParser(file.parser, symbol.parser);
|
|
2038
2151
|
symbols.push(symbol);
|
|
@@ -2105,6 +2218,7 @@ function buildKnowledgeGraph(projectDir) {
|
|
|
2105
2218
|
const episodes = [];
|
|
2106
2219
|
const repoEntityId = graphEntityId("repo", repoKey(projectDir));
|
|
2107
2220
|
const generatedFrom = packets.map((packet) => packet.updated_at).sort().at(-1) ?? null;
|
|
2221
|
+
const codeGraph = buildCodeGraph(projectDir);
|
|
2108
2222
|
addEntity(entities, {
|
|
2109
2223
|
id: repoEntityId,
|
|
2110
2224
|
type: "repo",
|
|
@@ -2270,6 +2384,118 @@ function buildKnowledgeGraph(projectDir) {
|
|
|
2270
2384
|
evidence: [episodeId],
|
|
2271
2385
|
});
|
|
2272
2386
|
}
|
|
2387
|
+
const context = engineeringContextFor(packet);
|
|
2388
|
+
if (context.verification) {
|
|
2389
|
+
const command = normalizeCommandText(context.verification);
|
|
2390
|
+
if (command) {
|
|
2391
|
+
const commandId = graphEntityId("command", command);
|
|
2392
|
+
addEntity(entities, {
|
|
2393
|
+
id: commandId,
|
|
2394
|
+
type: "command",
|
|
2395
|
+
name: command,
|
|
2396
|
+
summary: `Verification command from structured memory context.`,
|
|
2397
|
+
first_seen_at: packet.created_at,
|
|
2398
|
+
last_seen_at: packet.updated_at,
|
|
2399
|
+
evidence: [episodeId],
|
|
2400
|
+
});
|
|
2401
|
+
addEdge(edges, {
|
|
2402
|
+
from: memoryId,
|
|
2403
|
+
to: commandId,
|
|
2404
|
+
relation: "verified_by",
|
|
2405
|
+
fact: `"${packet.title}" is verified by "${command}".`,
|
|
2406
|
+
confidence: packet.confidence,
|
|
2407
|
+
valid_from: packet.updated_at,
|
|
2408
|
+
invalidated_at: null,
|
|
2409
|
+
branch,
|
|
2410
|
+
commit: head,
|
|
2411
|
+
evidence: [episodeId],
|
|
2412
|
+
});
|
|
2413
|
+
}
|
|
2414
|
+
}
|
|
2415
|
+
const packetTextLower = `${packet.title}\n${packet.summary}\n${packet.body}`.toLowerCase();
|
|
2416
|
+
const packetPathSet = new Set(packet.paths);
|
|
2417
|
+
const symbolRelation = packet.type === "bug_fix"
|
|
2418
|
+
? "fixes_symbol"
|
|
2419
|
+
: packet.type === "decision" || packet.type === "rationale" || packet.type === "constraint"
|
|
2420
|
+
? "informs_symbol"
|
|
2421
|
+
: "explains_symbol";
|
|
2422
|
+
for (const symbol of codeGraph.symbols.filter((symbol) => packetPathSet.has(symbol.path))) {
|
|
2423
|
+
if (packet.type !== "code_explanation" && !packetTextLower.includes(symbol.name.toLowerCase()))
|
|
2424
|
+
continue;
|
|
2425
|
+
const symbolEntityId = graphEntityId("symbol", symbol.id);
|
|
2426
|
+
addEntity(entities, {
|
|
2427
|
+
id: symbolEntityId,
|
|
2428
|
+
type: "symbol",
|
|
2429
|
+
name: symbol.name,
|
|
2430
|
+
aliases: [symbol.id, symbol.path],
|
|
2431
|
+
summary: `${symbol.kind} in ${symbol.path}:${symbol.line}`,
|
|
2432
|
+
first_seen_at: packet.created_at,
|
|
2433
|
+
last_seen_at: packet.updated_at,
|
|
2434
|
+
evidence: [episodeId],
|
|
2435
|
+
});
|
|
2436
|
+
addEdge(edges, {
|
|
2437
|
+
from: memoryId,
|
|
2438
|
+
to: symbolEntityId,
|
|
2439
|
+
relation: symbolRelation,
|
|
2440
|
+
fact: `"${packet.title}" ${symbolRelation.replace(/_/g, " ")} ${symbol.name} in ${symbol.path}.`,
|
|
2441
|
+
confidence: packet.confidence,
|
|
2442
|
+
valid_from: packet.updated_at,
|
|
2443
|
+
invalidated_at: null,
|
|
2444
|
+
branch,
|
|
2445
|
+
commit: head,
|
|
2446
|
+
evidence: [episodeId],
|
|
2447
|
+
});
|
|
2448
|
+
}
|
|
2449
|
+
for (const route of codeGraph.routes.filter((route) => packetPathSet.has(route.file_path) && packetTextLower.includes(route.path.toLowerCase()))) {
|
|
2450
|
+
const routeEntityId = graphEntityId("route", route.id);
|
|
2451
|
+
addEntity(entities, {
|
|
2452
|
+
id: routeEntityId,
|
|
2453
|
+
type: "route",
|
|
2454
|
+
name: `${route.method} ${route.path}`,
|
|
2455
|
+
aliases: [route.id, route.file_path],
|
|
2456
|
+
summary: `${route.framework} route in ${route.file_path}:${route.line}`,
|
|
2457
|
+
first_seen_at: packet.created_at,
|
|
2458
|
+
last_seen_at: packet.updated_at,
|
|
2459
|
+
evidence: [episodeId],
|
|
2460
|
+
});
|
|
2461
|
+
addEdge(edges, {
|
|
2462
|
+
from: memoryId,
|
|
2463
|
+
to: routeEntityId,
|
|
2464
|
+
relation: "applies_to_route",
|
|
2465
|
+
fact: `"${packet.title}" applies to route ${route.method} ${route.path}.`,
|
|
2466
|
+
confidence: packet.confidence,
|
|
2467
|
+
valid_from: packet.updated_at,
|
|
2468
|
+
invalidated_at: null,
|
|
2469
|
+
branch,
|
|
2470
|
+
commit: head,
|
|
2471
|
+
evidence: [episodeId],
|
|
2472
|
+
});
|
|
2473
|
+
}
|
|
2474
|
+
for (const test of codeGraph.tests.filter((test) => packetPathSet.has(test.test_path) || Boolean(test.covers_path && packetPathSet.has(test.covers_path)))) {
|
|
2475
|
+
const testEntityId = graphEntityId("test", test.test_symbol);
|
|
2476
|
+
addEntity(entities, {
|
|
2477
|
+
id: testEntityId,
|
|
2478
|
+
type: "test",
|
|
2479
|
+
name: test.title,
|
|
2480
|
+
aliases: [test.test_symbol, test.test_path],
|
|
2481
|
+
summary: `Test in ${test.test_path}:${test.line}${test.covers_symbol ? ` covers ${test.covers_symbol}` : ""}`,
|
|
2482
|
+
first_seen_at: packet.created_at,
|
|
2483
|
+
last_seen_at: packet.updated_at,
|
|
2484
|
+
evidence: [episodeId],
|
|
2485
|
+
});
|
|
2486
|
+
addEdge(edges, {
|
|
2487
|
+
from: memoryId,
|
|
2488
|
+
to: testEntityId,
|
|
2489
|
+
relation: "verified_by_test",
|
|
2490
|
+
fact: `"${packet.title}" is related to test "${test.title}".`,
|
|
2491
|
+
confidence: packet.confidence,
|
|
2492
|
+
valid_from: packet.updated_at,
|
|
2493
|
+
invalidated_at: null,
|
|
2494
|
+
branch,
|
|
2495
|
+
commit: head,
|
|
2496
|
+
evidence: [episodeId],
|
|
2497
|
+
});
|
|
2498
|
+
}
|
|
2273
2499
|
}
|
|
2274
2500
|
const manifestCommands = npmScriptCommands(projectDir);
|
|
2275
2501
|
if (manifestCommands.length) {
|
|
@@ -2702,38 +2928,148 @@ function scorePacket(queryTerms, packet) {
|
|
|
2702
2928
|
score += 1;
|
|
2703
2929
|
return { score, why: unique(why).slice(0, 8) };
|
|
2704
2930
|
}
|
|
2931
|
+
const BM25_K1 = 1.2;
|
|
2932
|
+
const BM25_B = 0.75;
|
|
2933
|
+
const BM25_FIELD_WEIGHTS = {
|
|
2934
|
+
title: 4,
|
|
2935
|
+
summary: 2.4,
|
|
2936
|
+
tag: 2.8,
|
|
2937
|
+
path: 2.4,
|
|
2938
|
+
type: 1.8,
|
|
2939
|
+
body: 1,
|
|
2940
|
+
};
|
|
2941
|
+
function lexicalStem(term) {
|
|
2942
|
+
if (term.length > 5 && term.endsWith("ing"))
|
|
2943
|
+
return term.slice(0, -3);
|
|
2944
|
+
if (term.length > 4 && term.endsWith("ies"))
|
|
2945
|
+
return `${term.slice(0, -3)}y`;
|
|
2946
|
+
if (term.length > 4 && term.endsWith("es"))
|
|
2947
|
+
return term.slice(0, -2);
|
|
2948
|
+
if (term.length > 3 && term.endsWith("s"))
|
|
2949
|
+
return term.slice(0, -1);
|
|
2950
|
+
return term;
|
|
2951
|
+
}
|
|
2952
|
+
function expandQueryTerms(terms) {
|
|
2953
|
+
return unique(terms.flatMap((term) => unique([term, lexicalStem(term)].filter(Boolean))));
|
|
2954
|
+
}
|
|
2955
|
+
function bm25Document(packet) {
|
|
2956
|
+
const termFrequency = new Map();
|
|
2957
|
+
const fieldHits = new Map();
|
|
2958
|
+
let length = 0;
|
|
2959
|
+
const addField = (field, text) => {
|
|
2960
|
+
const weight = BM25_FIELD_WEIGHTS[field];
|
|
2961
|
+
for (const token of tokenize(text)) {
|
|
2962
|
+
termFrequency.set(token, (termFrequency.get(token) ?? 0) + weight);
|
|
2963
|
+
if (!fieldHits.has(token))
|
|
2964
|
+
fieldHits.set(token, new Set());
|
|
2965
|
+
fieldHits.get(token).add(field);
|
|
2966
|
+
length += weight;
|
|
2967
|
+
}
|
|
2968
|
+
};
|
|
2969
|
+
addField("title", packet.title);
|
|
2970
|
+
addField("summary", packet.summary);
|
|
2971
|
+
addField("tag", packet.tags.join(" "));
|
|
2972
|
+
addField("path", packet.paths.join(" "));
|
|
2973
|
+
addField("type", packet.type);
|
|
2974
|
+
addField("body", packet.body);
|
|
2975
|
+
return { packet, termFrequency, fieldHits, length: Math.max(1, length) };
|
|
2976
|
+
}
|
|
2977
|
+
function scorePacketsBm25(queryTerms, packets) {
|
|
2978
|
+
const terms = expandQueryTerms(queryTerms);
|
|
2979
|
+
const documents = packets.map(bm25Document);
|
|
2980
|
+
const result = new Map();
|
|
2981
|
+
if (!terms.length || !documents.length)
|
|
2982
|
+
return result;
|
|
2983
|
+
const averageLength = documents.reduce((sum, document) => sum + document.length, 0) / documents.length || 1;
|
|
2984
|
+
const documentFrequency = new Map();
|
|
2985
|
+
for (const term of terms) {
|
|
2986
|
+
documentFrequency.set(term, documents.filter((document) => document.termFrequency.has(term)).length);
|
|
2987
|
+
}
|
|
2988
|
+
for (const document of documents) {
|
|
2989
|
+
let score = 0;
|
|
2990
|
+
const why = [];
|
|
2991
|
+
for (const term of terms) {
|
|
2992
|
+
const termFrequency = document.termFrequency.get(term) ?? 0;
|
|
2993
|
+
if (termFrequency <= 0)
|
|
2994
|
+
continue;
|
|
2995
|
+
const df = documentFrequency.get(term) ?? 0;
|
|
2996
|
+
const idf = Math.log(1 + (documents.length - df + 0.5) / (df + 0.5));
|
|
2997
|
+
const denominator = termFrequency + BM25_K1 * (1 - BM25_B + BM25_B * (document.length / averageLength));
|
|
2998
|
+
score += idf * ((termFrequency * (BM25_K1 + 1)) / denominator);
|
|
2999
|
+
const fields = Array.from(document.fieldHits.get(term) ?? []).sort();
|
|
3000
|
+
if (fields.length)
|
|
3001
|
+
why.push(`bm25:${fields.join("+")}:${term}`);
|
|
3002
|
+
}
|
|
3003
|
+
if (score > 0)
|
|
3004
|
+
result.set(document.packet.id, { score: Number(score.toFixed(2)), why: unique(why).slice(0, 8) });
|
|
3005
|
+
}
|
|
3006
|
+
return result;
|
|
3007
|
+
}
|
|
3008
|
+
function recallIntentBoost(queryTerms, packet) {
|
|
3009
|
+
const terms = new Set(expandQueryTerms(queryTerms));
|
|
3010
|
+
const commandIntent = ["run", "test", "tests", "build", "command", "commands"].some((term) => terms.has(term));
|
|
3011
|
+
const debugIntent = ["bug", "fix", "error", "fail", "debug"].some((term) => terms.has(term));
|
|
3012
|
+
const gotchaIntent = terms.has("gotcha");
|
|
3013
|
+
const decisionIntent = terms.has("decision");
|
|
3014
|
+
const packetText = `${packet.title}\n${packet.summary}\n${packet.body}\n${packet.tags.join(" ")}`;
|
|
3015
|
+
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);
|
|
3016
|
+
let score = 0;
|
|
3017
|
+
if (commandIntent) {
|
|
3018
|
+
if (packet.type === "runbook")
|
|
3019
|
+
score += hasCommandEvidence ? 22 : 8;
|
|
3020
|
+
if (packet.type === "repo_map" && hasCommandEvidence)
|
|
3021
|
+
score += 34;
|
|
3022
|
+
if (!["runbook", "repo_map", "workflow"].includes(packet.type) && !debugIntent)
|
|
3023
|
+
score -= 8;
|
|
3024
|
+
if (packet.type === "decision" && /release|verified by|passed|published/i.test(`${packet.title}\n${packet.body}`))
|
|
3025
|
+
score -= 3;
|
|
3026
|
+
}
|
|
3027
|
+
if (debugIntent && packet.type === "bug_fix")
|
|
3028
|
+
score += 10;
|
|
3029
|
+
if (gotchaIntent)
|
|
3030
|
+
score += packet.type === "gotcha" ? 18 : -4;
|
|
3031
|
+
if (decisionIntent)
|
|
3032
|
+
score += packet.type === "decision" ? 12 : 0;
|
|
3033
|
+
return score;
|
|
3034
|
+
}
|
|
2705
3035
|
function recallBreakdown(projectDir, terms, packet, textScore) {
|
|
2706
3036
|
const graph = buildKnowledgeGraph(projectDir);
|
|
2707
3037
|
const packetEntityId = graph.entities.find((entity) => entity.type === "memory" && entity.aliases.includes(packet.id))?.id;
|
|
2708
|
-
const
|
|
3038
|
+
const rawGraphScore = packetEntityId
|
|
2709
3039
|
? graph.edges.filter((edge) => edge.from === packetEntityId || edge.to === packetEntityId).reduce((sum, edge) => sum + scoreText(terms, edge.fact), 0)
|
|
2710
3040
|
: 0;
|
|
3041
|
+
const graphScore = Math.min(rawGraphScore * 0.45, textScore > 0 ? textScore * 1.5 + 12 : 8);
|
|
2711
3042
|
const pathTypeTag = scoreText(terms, `${packet.type} ${packet.tags.join(" ")} ${packet.paths.join(" ")}`, [packet.type, ...packet.tags, ...packet.paths]);
|
|
3043
|
+
const intent = recallIntentBoost(terms, packet);
|
|
2712
3044
|
const freshness = packet.status === "approved" ? 2 : packet.status === "pending" ? 0 : -5;
|
|
2713
3045
|
const quality = Number(packet.quality.score ?? evaluateMemoryQuality(projectDir, packet).score) / 10;
|
|
2714
3046
|
const feedback = packetFeedbackScore(packet);
|
|
2715
3047
|
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 };
|
|
3048
|
+
const final = Number((textScore + graphScore + pathTypeTag * 0.8 + intent + vector + freshness + quality + feedback).toFixed(2));
|
|
3049
|
+
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
3050
|
}
|
|
2719
3051
|
function recall(projectDir, query, limit = 5, explain = false) {
|
|
2720
3052
|
indexProject(projectDir);
|
|
2721
3053
|
const terms = tokenize(query);
|
|
2722
|
-
const
|
|
3054
|
+
const approvedPackets = loadApprovedPackets(projectDir);
|
|
3055
|
+
const lexicalScores = scorePacketsBm25(terms, approvedPackets);
|
|
3056
|
+
const scored = approvedPackets
|
|
2723
3057
|
.map((packet) => {
|
|
2724
|
-
const { score, why } =
|
|
3058
|
+
const { score, why } = lexicalScores.get(packet.id) ?? { score: 0, why: [] };
|
|
2725
3059
|
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:
|
|
3060
|
+
const relevance = score + score_breakdown.graph + score_breakdown.path_type_tag + score_breakdown.intent + score_breakdown.vector;
|
|
3061
|
+
return { packet, score: score_breakdown.final, relevance, why_matched: why, score_breakdown };
|
|
2728
3062
|
})
|
|
2729
3063
|
.filter((entry) => entry.relevance > 0)
|
|
2730
3064
|
.sort((a, b) => b.score - a.score || a.packet.title.localeCompare(b.packet.title))
|
|
2731
3065
|
.slice(0, limit)
|
|
2732
3066
|
.map(({ relevance, ...entry }) => entry);
|
|
2733
3067
|
const pendingSeen = new Set();
|
|
2734
|
-
const
|
|
3068
|
+
const pendingPackets = recallablePendingPackets(projectDir);
|
|
3069
|
+
const pendingLexicalScores = scorePacketsBm25(terms, pendingPackets);
|
|
3070
|
+
const pendingScored = pendingPackets
|
|
2735
3071
|
.map((packet) => {
|
|
2736
|
-
const { score, why } =
|
|
3072
|
+
const { score, why } = pendingLexicalScores.get(packet.id) ?? { score: 0, why: [] };
|
|
2737
3073
|
return { packet, score, why_matched: why };
|
|
2738
3074
|
})
|
|
2739
3075
|
.filter((entry) => entry.score > 0)
|
|
@@ -2788,7 +3124,7 @@ function recall(projectDir, query, limit = 5, explain = false) {
|
|
|
2788
3124
|
? scored.map((entry) => ({
|
|
2789
3125
|
packet_id: entry.packet.id,
|
|
2790
3126
|
title: entry.packet.title,
|
|
2791
|
-
provider: "
|
|
3127
|
+
provider: "bm25",
|
|
2792
3128
|
score_breakdown: entry.score_breakdown,
|
|
2793
3129
|
why_matched: entry.why_matched,
|
|
2794
3130
|
}))
|
|
@@ -3039,6 +3375,206 @@ function kageMetrics(projectDir) {
|
|
|
3039
3375
|
},
|
|
3040
3376
|
};
|
|
3041
3377
|
}
|
|
3378
|
+
function auditProject(projectDir) {
|
|
3379
|
+
ensureMemoryDirs(projectDir);
|
|
3380
|
+
const validation = validateProject(projectDir);
|
|
3381
|
+
const quality = qualityReport(projectDir);
|
|
3382
|
+
const codeGraph = buildCodeGraph(projectDir);
|
|
3383
|
+
const knowledgeGraph = buildKnowledgeGraph(projectDir);
|
|
3384
|
+
const approved = loadApprovedPackets(projectDir);
|
|
3385
|
+
const pending = loadPendingPackets(projectDir);
|
|
3386
|
+
const structuredPackets = approved.filter(hasStructuredEngineeringContext);
|
|
3387
|
+
const preciseParsers = ["scip", "lsif", "lsp"];
|
|
3388
|
+
const astParsers = ["typescript-ast", "tree-sitter"];
|
|
3389
|
+
const indexableFiles = codeGraph.files.filter((file) => file.parser !== "metadata").length;
|
|
3390
|
+
const preciseFiles = codeGraph.files.filter((file) => preciseParsers.includes(file.parser)).length;
|
|
3391
|
+
const astFiles = codeGraph.files.filter((file) => astParsers.includes(file.parser)).length;
|
|
3392
|
+
const fallbackFiles = codeGraph.files.filter((file) => file.parser === "generic-static" || file.parser === "metadata").length;
|
|
3393
|
+
const memoryCodeEdges = knowledgeGraph.edges.filter((edge) => ["explains_symbol", "informs_symbol", "fixes_symbol", "applies_to_route", "verified_by_test"].includes(edge.relation)).length;
|
|
3394
|
+
const stalePackets = quality.totals.stale;
|
|
3395
|
+
const duplicateCandidatesTotal = quality.totals.duplicate;
|
|
3396
|
+
const structuredCoverage = percent(structuredPackets.length, approved.length);
|
|
3397
|
+
const preciseCoverage = percent(preciseFiles, indexableFiles);
|
|
3398
|
+
const memoryCodeCoverage = percent(Math.min(memoryCodeEdges, approved.length), approved.length);
|
|
3399
|
+
const recommendations = [];
|
|
3400
|
+
if (structuredPackets.length < approved.length) {
|
|
3401
|
+
recommendations.push("Add structured context fields to high-value memories: why, verification, risk_if_forgotten, and stale_when.");
|
|
3402
|
+
}
|
|
3403
|
+
if (pending.length) {
|
|
3404
|
+
recommendations.push("Review pending memory inbox packets and approve, reject, merge, or supersede them before handoff.");
|
|
3405
|
+
}
|
|
3406
|
+
if (stalePackets) {
|
|
3407
|
+
recommendations.push("Run kage gc --dry-run and update or deprecate stale memory before trusting recall.");
|
|
3408
|
+
}
|
|
3409
|
+
if (duplicateCandidatesTotal) {
|
|
3410
|
+
recommendations.push("Merge or supersede duplicate memory packets so agents do not receive conflicting context.");
|
|
3411
|
+
}
|
|
3412
|
+
if (preciseFiles < indexableFiles) {
|
|
3413
|
+
recommendations.push("Add or extend SCIP/LSIF/LSP index artifacts in CI for remaining source files; keep AST/static extraction as fallback.");
|
|
3414
|
+
}
|
|
3415
|
+
if (!memoryCodeEdges && approved.length && codeGraph.symbols.length) {
|
|
3416
|
+
recommendations.push("Link memory packets to symbols, routes, and tests with code_explanation, bug_fix, decision, and verification context.");
|
|
3417
|
+
}
|
|
3418
|
+
if (!validation.ok) {
|
|
3419
|
+
recommendations.push("Fix validation errors before relying on Kage in PR or agent-start workflows.");
|
|
3420
|
+
}
|
|
3421
|
+
const trustScore = Math.max(0, Math.min(100, Math.round((validation.ok ? 25 : 0) +
|
|
3422
|
+
quality.useful_memory_ratio_percent * 0.25 +
|
|
3423
|
+
structuredCoverage * 0.2 +
|
|
3424
|
+
memoryCodeCoverage * 0.15 +
|
|
3425
|
+
Math.max(0, 15 - pending.length * 3 - stalePackets * 5 - duplicateCandidatesTotal * 4))));
|
|
3426
|
+
return {
|
|
3427
|
+
schema_version: 1,
|
|
3428
|
+
project_dir: projectDir,
|
|
3429
|
+
generated_at: nowIso(),
|
|
3430
|
+
ok: validation.ok && stalePackets === 0 && duplicateCandidatesTotal === 0,
|
|
3431
|
+
trust_score: trustScore,
|
|
3432
|
+
checks: {
|
|
3433
|
+
validation,
|
|
3434
|
+
memory_inbox: {
|
|
3435
|
+
approved_packets: approved.length,
|
|
3436
|
+
pending_packets: pending.length,
|
|
3437
|
+
stale_packets: stalePackets,
|
|
3438
|
+
duplicate_candidates: duplicateCandidatesTotal,
|
|
3439
|
+
},
|
|
3440
|
+
structured_memory: {
|
|
3441
|
+
total_packets: approved.length,
|
|
3442
|
+
structured_packets: structuredPackets.length,
|
|
3443
|
+
coverage_percent: structuredCoverage,
|
|
3444
|
+
missing_context_packet_ids: approved.filter((packet) => !structuredPackets.includes(packet)).map((packet) => packet.id),
|
|
3445
|
+
},
|
|
3446
|
+
code_graph: {
|
|
3447
|
+
files: codeGraph.files.length,
|
|
3448
|
+
precise_files: preciseFiles,
|
|
3449
|
+
ast_files: astFiles,
|
|
3450
|
+
fallback_files: fallbackFiles,
|
|
3451
|
+
precise_coverage_percent: preciseCoverage,
|
|
3452
|
+
indexer_coverage_percent: percent(codeGraph.files.filter((file) => file.parser !== "metadata").length, indexableFiles),
|
|
3453
|
+
},
|
|
3454
|
+
graph_links: {
|
|
3455
|
+
memory_code_edges: memoryCodeEdges,
|
|
3456
|
+
evidence_coverage_percent: percent(knowledgeGraph.edges.filter((edge) => edge.evidence.length > 0).length, knowledgeGraph.edges.length),
|
|
3457
|
+
},
|
|
3458
|
+
},
|
|
3459
|
+
recommendations,
|
|
3460
|
+
};
|
|
3461
|
+
}
|
|
3462
|
+
function memoryInbox(projectDir) {
|
|
3463
|
+
ensureMemoryDirs(projectDir);
|
|
3464
|
+
const validation = validateProject(projectDir);
|
|
3465
|
+
const quality = qualityReport(projectDir);
|
|
3466
|
+
const approved = loadApprovedPackets(projectDir);
|
|
3467
|
+
const pending = loadPendingPackets(projectDir);
|
|
3468
|
+
const items = [];
|
|
3469
|
+
for (const packet of pending) {
|
|
3470
|
+
const qualityDetails = evaluateMemoryQuality(projectDir, packet);
|
|
3471
|
+
items.push({
|
|
3472
|
+
kind: "pending",
|
|
3473
|
+
severity: "warning",
|
|
3474
|
+
packet_id: packet.id,
|
|
3475
|
+
title: packet.title,
|
|
3476
|
+
type: packet.type,
|
|
3477
|
+
status: packet.status,
|
|
3478
|
+
paths: packet.paths,
|
|
3479
|
+
summary: packet.summary,
|
|
3480
|
+
reasons: [
|
|
3481
|
+
...(qualityDetails.risks ?? []),
|
|
3482
|
+
`quality score ${qualityDetails.score}/100`,
|
|
3483
|
+
],
|
|
3484
|
+
action: "Approve, reject, merge, or keep pending after reviewing source refs and sensitivity.",
|
|
3485
|
+
});
|
|
3486
|
+
}
|
|
3487
|
+
for (const packet of approved) {
|
|
3488
|
+
const reasons = staleMemoryReasons(projectDir, packet);
|
|
3489
|
+
if (reasons.length) {
|
|
3490
|
+
items.push({
|
|
3491
|
+
kind: "stale",
|
|
3492
|
+
severity: "blocker",
|
|
3493
|
+
packet_id: packet.id,
|
|
3494
|
+
title: packet.title,
|
|
3495
|
+
type: packet.type,
|
|
3496
|
+
status: packet.status,
|
|
3497
|
+
paths: packet.paths,
|
|
3498
|
+
summary: packet.summary,
|
|
3499
|
+
reasons,
|
|
3500
|
+
action: `${staleSuggestedAction(reasons)} this packet before trusting recall.`,
|
|
3501
|
+
});
|
|
3502
|
+
}
|
|
3503
|
+
}
|
|
3504
|
+
for (const packet of approved.filter((packet) => !hasStructuredEngineeringContext(packet))) {
|
|
3505
|
+
items.push({
|
|
3506
|
+
kind: "missing_context",
|
|
3507
|
+
severity: "info",
|
|
3508
|
+
packet_id: packet.id,
|
|
3509
|
+
title: packet.title,
|
|
3510
|
+
type: packet.type,
|
|
3511
|
+
status: packet.status,
|
|
3512
|
+
paths: packet.paths,
|
|
3513
|
+
summary: packet.summary,
|
|
3514
|
+
reasons: ["missing explicit why, verification, risk, stale condition, trigger, or action"],
|
|
3515
|
+
action: "Add structured context if this packet carries durable rationale, bug, issue, or code explanation.",
|
|
3516
|
+
});
|
|
3517
|
+
}
|
|
3518
|
+
for (const packet of quality.packets.filter((packet) => packet.classification === "duplicate")) {
|
|
3519
|
+
const source = [...approved, ...pending].find((candidate) => candidate.id === packet.id);
|
|
3520
|
+
items.push({
|
|
3521
|
+
kind: "duplicate",
|
|
3522
|
+
severity: "warning",
|
|
3523
|
+
packet_id: packet.id,
|
|
3524
|
+
title: packet.title,
|
|
3525
|
+
type: packet.type,
|
|
3526
|
+
status: packet.status,
|
|
3527
|
+
paths: source?.paths,
|
|
3528
|
+
summary: source?.summary ?? packet.title,
|
|
3529
|
+
reasons: packet.risks.length ? packet.risks : ["duplicate candidate detected by quality report"],
|
|
3530
|
+
action: "Merge, supersede, or deprecate overlapping memory before handoff.",
|
|
3531
|
+
});
|
|
3532
|
+
}
|
|
3533
|
+
for (const error of validation.errors) {
|
|
3534
|
+
items.push({
|
|
3535
|
+
kind: "validation_error",
|
|
3536
|
+
severity: "blocker",
|
|
3537
|
+
summary: error,
|
|
3538
|
+
reasons: [error],
|
|
3539
|
+
action: "Fix validation errors before relying on Kage in agent or PR workflows.",
|
|
3540
|
+
});
|
|
3541
|
+
}
|
|
3542
|
+
for (const warning of validation.warnings) {
|
|
3543
|
+
items.push({
|
|
3544
|
+
kind: "validation_warning",
|
|
3545
|
+
severity: "warning",
|
|
3546
|
+
summary: warning,
|
|
3547
|
+
reasons: [warning],
|
|
3548
|
+
action: "Review grounding, indexes, generated artifacts, or packet quality.",
|
|
3549
|
+
});
|
|
3550
|
+
}
|
|
3551
|
+
const counts = {
|
|
3552
|
+
approved: approved.length,
|
|
3553
|
+
pending: pending.length,
|
|
3554
|
+
stale: items.filter((item) => item.kind === "stale").length,
|
|
3555
|
+
duplicates: items.filter((item) => item.kind === "duplicate").length,
|
|
3556
|
+
missing_context: items.filter((item) => item.kind === "missing_context").length,
|
|
3557
|
+
validation_errors: validation.errors.length,
|
|
3558
|
+
validation_warnings: validation.warnings.length,
|
|
3559
|
+
};
|
|
3560
|
+
const recommendations = unique([
|
|
3561
|
+
...(counts.pending ? ["Review pending memory packets before handoff."] : []),
|
|
3562
|
+
...(counts.stale ? ["Update, verify, supersede, or deprecate stale memory packets."] : []),
|
|
3563
|
+
...(counts.duplicates ? ["Merge or supersede duplicate memory packets."] : []),
|
|
3564
|
+
...(counts.missing_context ? ["Add structured why, verification, risk, and stale_when context to high-value packets."] : []),
|
|
3565
|
+
...(counts.validation_errors ? ["Fix validation errors before trusting recall."] : []),
|
|
3566
|
+
...(counts.validation_warnings ? ["Review validation warnings so memory remains source-grounded."] : []),
|
|
3567
|
+
]);
|
|
3568
|
+
return {
|
|
3569
|
+
schema_version: 1,
|
|
3570
|
+
project_dir: projectDir,
|
|
3571
|
+
generated_at: nowIso(),
|
|
3572
|
+
ok: counts.pending === 0 && counts.stale === 0 && counts.duplicates === 0 && counts.validation_errors === 0,
|
|
3573
|
+
counts,
|
|
3574
|
+
items,
|
|
3575
|
+
recommendations,
|
|
3576
|
+
};
|
|
3577
|
+
}
|
|
3042
3578
|
function qualityReport(projectDir) {
|
|
3043
3579
|
ensureMemoryDirs(projectDir);
|
|
3044
3580
|
const packets = [...loadPacketsFromDir(packetsDir(projectDir)), ...loadPacketsFromDir(pendingDir(projectDir))];
|
|
@@ -3116,17 +3652,57 @@ function benchmarkProject(projectDir) {
|
|
|
3116
3652
|
const metrics = kageMetricsShallow(projectDir);
|
|
3117
3653
|
const quality = qualityReport(projectDir);
|
|
3118
3654
|
const typeCoverage = quality.memory_type_coverage;
|
|
3655
|
+
const recallHitRate = percent(scenarios.filter((scenario) => scenario.hit).length, scenarios.length);
|
|
3656
|
+
const codeFlowCoverage = metrics.code_graph.files > 0 && metrics.code_graph.symbols > 0 ? 100 : 0;
|
|
3657
|
+
const gates = [
|
|
3658
|
+
{
|
|
3659
|
+
name: "recall_hit_rate",
|
|
3660
|
+
target: 60,
|
|
3661
|
+
actual: recallHitRate,
|
|
3662
|
+
unit: "percent",
|
|
3663
|
+
pass: recallHitRate >= 60,
|
|
3664
|
+
required: true,
|
|
3665
|
+
},
|
|
3666
|
+
{
|
|
3667
|
+
name: "evidence_coverage",
|
|
3668
|
+
target: 80,
|
|
3669
|
+
actual: quality.evidence_coverage_percent,
|
|
3670
|
+
unit: "percent",
|
|
3671
|
+
pass: quality.evidence_coverage_percent >= 80,
|
|
3672
|
+
required: true,
|
|
3673
|
+
},
|
|
3674
|
+
{
|
|
3675
|
+
name: "useful_memory_ratio",
|
|
3676
|
+
target: 70,
|
|
3677
|
+
actual: quality.useful_memory_ratio_percent,
|
|
3678
|
+
unit: "percent",
|
|
3679
|
+
pass: quality.useful_memory_ratio_percent >= 70,
|
|
3680
|
+
required: true,
|
|
3681
|
+
},
|
|
3682
|
+
{
|
|
3683
|
+
name: "code_flow_coverage",
|
|
3684
|
+
target: 100,
|
|
3685
|
+
actual: codeFlowCoverage,
|
|
3686
|
+
unit: "percent",
|
|
3687
|
+
pass: codeFlowCoverage >= 100,
|
|
3688
|
+
required: true,
|
|
3689
|
+
},
|
|
3690
|
+
];
|
|
3691
|
+
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
3692
|
return {
|
|
3120
3693
|
schema_version: 1,
|
|
3121
3694
|
project_dir: projectDir,
|
|
3122
3695
|
generated_at: nowIso(),
|
|
3696
|
+
ok: gates.filter((gate) => gate.required).every((gate) => gate.pass),
|
|
3697
|
+
overall_score: gateScore,
|
|
3698
|
+
gates,
|
|
3123
3699
|
scenarios,
|
|
3124
3700
|
pain_metrics: {
|
|
3125
3701
|
setup_runbook_coverage_percent: typeCoverage.runbook ? 100 : 0,
|
|
3126
3702
|
bug_fix_coverage_percent: typeCoverage.bug_fix ? 100 : 0,
|
|
3127
3703
|
decision_coverage_percent: typeCoverage.decision ? 100 : 0,
|
|
3128
|
-
code_flow_coverage_percent:
|
|
3129
|
-
recall_hit_rate_percent:
|
|
3704
|
+
code_flow_coverage_percent: codeFlowCoverage,
|
|
3705
|
+
recall_hit_rate_percent: recallHitRate,
|
|
3130
3706
|
estimated_rediscovery_avoided: scenarios.filter((scenario) => scenario.hit).length,
|
|
3131
3707
|
estimated_tokens_saved: metrics.savings.estimated_tokens_saved_per_recall,
|
|
3132
3708
|
time_to_first_use_seconds: metrics.harness.policy_installed ? 30 : 90,
|
|
@@ -3296,10 +3872,20 @@ function inferLearningType(input) {
|
|
|
3296
3872
|
if (input.type)
|
|
3297
3873
|
return input.type;
|
|
3298
3874
|
const text = `${input.title ?? ""} ${input.learning}`.toLowerCase();
|
|
3875
|
+
if (/(issue context|issue|hypothesis|blocked|unresolved|attempted fix)/.test(text))
|
|
3876
|
+
return "issue_context";
|
|
3299
3877
|
if (/(bug|fix|error|fail|failure|broken|regression)/.test(text))
|
|
3300
3878
|
return "bug_fix";
|
|
3301
|
-
if (/(
|
|
3879
|
+
if (/(code explanation|explains|data flow|invariant|coupling|module purpose)/.test(text))
|
|
3880
|
+
return "code_explanation";
|
|
3881
|
+
if (/(constraint|external requirement|legal|compliance|performance budget)/.test(text))
|
|
3882
|
+
return "constraint";
|
|
3883
|
+
if (/(negative result|tried|failed because|rejected)/.test(text))
|
|
3884
|
+
return "negative_result";
|
|
3885
|
+
if (/(decided|decision|tradeoff|chose|choose)/.test(text))
|
|
3302
3886
|
return "decision";
|
|
3887
|
+
if (/(why|rationale|because)/.test(text))
|
|
3888
|
+
return "rationale";
|
|
3303
3889
|
if (/(run|command|setup|install|build|test|deploy)/.test(text))
|
|
3304
3890
|
return "runbook";
|
|
3305
3891
|
if (/(convention|always|prefer|avoid|pattern)/.test(text))
|
|
@@ -3312,6 +3898,58 @@ function titleFromLearning(learning) {
|
|
|
3312
3898
|
const sentence = learning.split(/[.!?]\s+/)[0]?.trim() || learning.trim();
|
|
3313
3899
|
return sentence.slice(0, 90) || "Session learning";
|
|
3314
3900
|
}
|
|
3901
|
+
const MEMORY_CONTEXT_FIELD_LABELS = [
|
|
3902
|
+
"Fact",
|
|
3903
|
+
"Decision",
|
|
3904
|
+
"Why",
|
|
3905
|
+
"Rationale",
|
|
3906
|
+
"Because",
|
|
3907
|
+
"When",
|
|
3908
|
+
"Trigger",
|
|
3909
|
+
"Action",
|
|
3910
|
+
"Do",
|
|
3911
|
+
"Use",
|
|
3912
|
+
"Verified by",
|
|
3913
|
+
"Verification",
|
|
3914
|
+
"Evidence",
|
|
3915
|
+
"Risk if forgotten",
|
|
3916
|
+
"Risk",
|
|
3917
|
+
"Stale when",
|
|
3918
|
+
"Invalid when",
|
|
3919
|
+
"Revisit when",
|
|
3920
|
+
"Rejected alternatives",
|
|
3921
|
+
];
|
|
3922
|
+
function labeledMemoryField(text, labels) {
|
|
3923
|
+
const escaped = labels.map((label) => label.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
|
|
3924
|
+
const allLabels = MEMORY_CONTEXT_FIELD_LABELS.map((label) => label.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
|
|
3925
|
+
const match = text.match(new RegExp(`(?:^|\\n|\\b)(?:${escaped})\\s*:\\s*([\\s\\S]*?)(?=(?:\\s|\\n)+(?:${allLabels})\\s*:|$)`, "i"));
|
|
3926
|
+
return match?.[1]?.trim().replace(/\s+$/, "");
|
|
3927
|
+
}
|
|
3928
|
+
function inferEngineeringContext(input) {
|
|
3929
|
+
const body = input.body.trim();
|
|
3930
|
+
const firstParagraph = body
|
|
3931
|
+
.split(/\n\s*\n/)
|
|
3932
|
+
.find((part) => !/^\s*(why|verified by|verification|risk if forgotten|stale when|trigger|action|rejected alternatives)\s*:/i.test(part))
|
|
3933
|
+
?.trim();
|
|
3934
|
+
const context = {
|
|
3935
|
+
fact: input.context?.fact ?? labeledMemoryField(body, ["Fact", "Decision"]) ?? firstParagraph ?? input.title,
|
|
3936
|
+
why: input.context?.why ?? labeledMemoryField(body, ["Why", "Rationale", "Because"]),
|
|
3937
|
+
trigger: input.context?.trigger ?? labeledMemoryField(body, ["When", "Trigger"]),
|
|
3938
|
+
action: input.context?.action ?? labeledMemoryField(body, ["Action", "Do", "Use"]),
|
|
3939
|
+
verification: input.context?.verification ?? labeledMemoryField(body, ["Verified by", "Verification", "Evidence"]),
|
|
3940
|
+
risk_if_forgotten: input.context?.risk_if_forgotten ?? labeledMemoryField(body, ["Risk if forgotten", "Risk"]),
|
|
3941
|
+
stale_when: input.context?.stale_when ?? labeledMemoryField(body, ["Stale when", "Invalid when", "Revisit when"]),
|
|
3942
|
+
rejected_alternatives: input.context?.rejected_alternatives,
|
|
3943
|
+
};
|
|
3944
|
+
return Object.fromEntries(Object.entries(context).filter(([, value]) => Array.isArray(value) ? value.length : Boolean(value)));
|
|
3945
|
+
}
|
|
3946
|
+
function engineeringContextFor(packet) {
|
|
3947
|
+
return inferEngineeringContext({ title: packet.title, body: packet.body, context: packet.context });
|
|
3948
|
+
}
|
|
3949
|
+
function hasStructuredEngineeringContext(packet) {
|
|
3950
|
+
const context = engineeringContextFor(packet);
|
|
3951
|
+
return Boolean(context.why || context.verification || context.risk_if_forgotten || context.stale_when || context.trigger || context.action);
|
|
3952
|
+
}
|
|
3315
3953
|
function learn(input) {
|
|
3316
3954
|
const type = inferLearningType(input);
|
|
3317
3955
|
const title = input.title?.trim() || titleFromLearning(input.learning);
|
|
@@ -3329,6 +3967,7 @@ function learn(input) {
|
|
|
3329
3967
|
tags: unique(["session-learning", ...(input.tags ?? [])]),
|
|
3330
3968
|
paths: input.paths,
|
|
3331
3969
|
stack: input.stack,
|
|
3970
|
+
context: input.context,
|
|
3332
3971
|
});
|
|
3333
3972
|
}
|
|
3334
3973
|
function capture(input) {
|
|
@@ -3366,6 +4005,7 @@ function capture(input) {
|
|
|
3366
4005
|
captured_at: createdAt,
|
|
3367
4006
|
},
|
|
3368
4007
|
],
|
|
4008
|
+
context: inferEngineeringContext({ title: input.title, body: input.body, context: input.context }),
|
|
3369
4009
|
freshness: {
|
|
3370
4010
|
ttl_days: 365,
|
|
3371
4011
|
last_verified_at: createdAt,
|
|
@@ -3903,6 +4543,15 @@ function reusableFileObservation(event) {
|
|
|
3903
4543
|
"dispatch",
|
|
3904
4544
|
"convention",
|
|
3905
4545
|
"decision",
|
|
4546
|
+
"rationale",
|
|
4547
|
+
"root cause",
|
|
4548
|
+
"issue",
|
|
4549
|
+
"hypothesis",
|
|
4550
|
+
"unresolved",
|
|
4551
|
+
"code explanation",
|
|
4552
|
+
"explains",
|
|
4553
|
+
"data flow",
|
|
4554
|
+
"invariant",
|
|
3906
4555
|
"gotcha",
|
|
3907
4556
|
"workflow",
|
|
3908
4557
|
"runbook",
|
|
@@ -3974,9 +4623,20 @@ function reusablePromptObservation(event) {
|
|
|
3974
4623
|
"convention",
|
|
3975
4624
|
"policy",
|
|
3976
4625
|
"gotcha",
|
|
4626
|
+
"bug",
|
|
4627
|
+
"issue",
|
|
4628
|
+
"issue context",
|
|
4629
|
+
"hypothesis",
|
|
4630
|
+
"unresolved",
|
|
4631
|
+
"rationale",
|
|
4632
|
+
"why:",
|
|
4633
|
+
"because",
|
|
4634
|
+
"code explanation",
|
|
4635
|
+
"explains",
|
|
4636
|
+
"data flow",
|
|
4637
|
+
"root cause",
|
|
3977
4638
|
"runbook",
|
|
3978
4639
|
"workflow",
|
|
3979
|
-
"root cause",
|
|
3980
4640
|
"use this",
|
|
3981
4641
|
"always",
|
|
3982
4642
|
"never",
|
|
@@ -3985,7 +4645,7 @@ function reusablePromptObservation(event) {
|
|
|
3985
4645
|
];
|
|
3986
4646
|
if (!durableSignals.some((signal) => lower.includes(signal)))
|
|
3987
4647
|
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))
|
|
4648
|
+
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
4649
|
return "";
|
|
3990
4650
|
return text;
|
|
3991
4651
|
}
|
|
@@ -4112,7 +4772,8 @@ function createDiffChangeMemory(projectDir, summary) {
|
|
|
4112
4772
|
"",
|
|
4113
4773
|
"Improve this packet when more context is known:",
|
|
4114
4774
|
"- The actual feature, fix, or refactor rationale.",
|
|
4115
|
-
"-
|
|
4775
|
+
"- Why the change was made, including relevant bugs, issues, decisions, and code explanations.",
|
|
4776
|
+
"- The package, API, command, or architectural pattern future agents should understand, verify, or reuse.",
|
|
4116
4777
|
"- Any gotchas, follow-up risks, or branch-specific assumptions.",
|
|
4117
4778
|
"",
|
|
4118
4779
|
"Promote beyond this repo only after explicit org/global review.",
|
|
@@ -4143,6 +4804,15 @@ function createDiffChangeMemory(projectDir, summary) {
|
|
|
4143
4804
|
summary_path: (0, node_path_1.join)(reviewDir(projectDir), `branch-summary-${slugify(branch)}.json`),
|
|
4144
4805
|
},
|
|
4145
4806
|
],
|
|
4807
|
+
context: {
|
|
4808
|
+
fact: `Current branch ${branch} changes ${summary.changed_files.length} repo path${summary.changed_files.length === 1 ? "" : "s"}.`,
|
|
4809
|
+
why: "Branch change memory gives future agents durable context from the git diff when they continue, review, or verify this work.",
|
|
4810
|
+
trigger: "Recall when asking what changed on this branch, preparing a PR review, or resuming this work.",
|
|
4811
|
+
action: "Use the changed file list and diff summary as orientation, then inspect the actual diff and source files before making further edits.",
|
|
4812
|
+
verification: "Generated from git diff and refreshed by kage pr summarize or kage propose --from-diff.",
|
|
4813
|
+
risk_if_forgotten: "Future agents may repeat orientation work, miss branch-specific assumptions, or ignore files touched by this change.",
|
|
4814
|
+
stale_when: "The branch diff changes substantially, the branch is merged, or a newer change-memory packet supersedes it.",
|
|
4815
|
+
},
|
|
4146
4816
|
freshness: {
|
|
4147
4817
|
last_verified_at: now,
|
|
4148
4818
|
ttl_days: 180,
|
|
@@ -4176,7 +4846,7 @@ function proposeFromDiff(projectDir) {
|
|
|
4176
4846
|
const changedFiles = parsePorcelainStatus(status);
|
|
4177
4847
|
if (changedFiles.length === 0)
|
|
4178
4848
|
return { ok: false, changedFiles: [], errors: ["No changed files found."] };
|
|
4179
|
-
const stat =
|
|
4849
|
+
const stat = branchDiffStat(projectDir, changedFiles);
|
|
4180
4850
|
const branch = gitBranch(projectDir);
|
|
4181
4851
|
const summary = {
|
|
4182
4852
|
schema_version: 1,
|
|
@@ -4435,9 +5105,10 @@ function loadOrgInboxPackets(projectDir, org) {
|
|
|
4435
5105
|
}
|
|
4436
5106
|
function recallFromPackets(query, packets, limit, label) {
|
|
4437
5107
|
const terms = tokenize(query);
|
|
5108
|
+
const lexicalScores = scorePacketsBm25(terms, packets);
|
|
4438
5109
|
const scored = packets
|
|
4439
5110
|
.map((packet) => {
|
|
4440
|
-
const { score, why } =
|
|
5111
|
+
const { score, why } = lexicalScores.get(packet.id) ?? { score: 0, why: [] };
|
|
4441
5112
|
return { packet, score, why_matched: why };
|
|
4442
5113
|
})
|
|
4443
5114
|
.filter((result) => result.score > 0)
|